TIPTAP toolkit
Tiptap - Dev Toolkit Editor Suite
Tiptap is a suite of content editing & real time collaboration tools. Build editor experiences like Notion in weeks, not years.
tiptap.dev
1단계
1-1. 설치
npm install @tiptap/react @tiptap/starter-kit
StarterKit은 Document, Paragraph, Text, Bold, Italic 등의 기본 Extension을 모두 포함합니다.
1-2. 기본 개념
1) Editor 인스턴스
- 에디터 상태와 모든 명령(command)을 포함한 핵심 객체
- 리액트에서는 useEditor()로 생성
2) EditorContent 컴포넌트
- 실제 텍스트 입력 영역을 렌더링함
- editor 인스턴스를 props로 전달
3) extensions
- 기능(예: Bold, Italic, Heading 등)을 추가하는 플러그인 목록
- StarterKit이 대부분 포함
1-3. 사용 예제
// Editor.tsx
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
export const BasicEditor = () => {
const editor = useEditor({
extensions: [StarterKit],
content: '<p>Hello <strong>World!</strong></p>',
});
return (
<div className="space-y-4">
<h2 className="text-lg font-semibold">📝 Basic Tiptap Editor</h2>
<EditorContent editor={editor} className="border p-4 rounded-md min-h-[150px]" />
</div>
);
};
1-4. 확장 목록 (StarterKit 내부 포함 기능)
| Document | 최상위 노드 (필수) | ✅ |
| Paragraph | 일반 텍스트 블록 | ✅ |
| Text | 실제 텍스트 노드 | ✅ |
| Bold | 굵게 (<strong>) | ✅ |
| Italic | 기울임 (<em>) | ✅ |
| Strike | 취소선 (<s>) | ✅ |
| BulletList | 순서 없는 리스트 (<ul>) | ✅ |
| OrderedList | 번호 리스트 (<ol>) | ✅ |
| Heading | 제목 (<h1>~<h6>) | ✅ |
| Blockquote | 인용구 (<blockquote>) | ✅ |
| CodeBlock | 코드 블록 (<pre><code>) | ✅ |
| History | undo/redo 기능 | ✅ |
1-5. 상태 보기 / 디버깅
<button onClick={() => console.log(editor?.getJSON())}>
현재 상태 콘솔 출력
</button>
목표
- Bold / Italic 토글 버튼
- editor.getHTML() / editor.getJSON() 확인용 버튼
- 초기 content 없이 빈 에디터로 시작
코드 예제
// components/BasicEditor.tsx
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
export const BasicEditor = () => {
// 3. 초기 content 없이 빈 상태로 시작
const editor = useEditor({
extensions: [StarterKit],
content: '', // 빈 content
});
if (!editor) return null;
return (
<div className="space-y-4 p-4 border rounded-md">
<h2 className="text-xl font-bold">📝 Tiptap Editor Demo</h2>
{/* 1. Bold / Italic 토글 버튼 */}
<div className="flex gap-3">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={`px-3 py-1 rounded border ${editor.isActive('bold') ? 'bg-black text-white' : ''}`}
>
Bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={`px-3 py-1 rounded border ${editor.isActive('italic') ? 'bg-black text-white' : ''}`}
>
Italic
</button>
</div>
{/* 2. 실제 에디터 UI */}
<EditorContent editor={editor} className="min-h-[150px] border p-3 rounded" />
{/* 2. 상태 출력 버튼 */}
<div className="flex gap-3">
<button
onClick={() => console.log('HTML:', editor.getHTML())}
className="text-blue-600 underline"
>
👉 getHTML() 출력
</button>
<button
onClick={() => console.log('JSON:', editor.getJSON())}
className="text-green-600 underline"
>
👉 getJSON() 출력
</button>
</div>
</div>
);
};
사용 방법
위 컴포넌트를 프로젝트 내에서 import해서 사용
import { BasicEditor } from './components/BasicEditor';
export default function Page() {
return (
<div className="max-w-2xl mx-auto mt-10">
<BasicEditor />
</div>
);
}
브라우저에서 개발자 콘솔 열고 getHTML()이나 getJSON() 버튼 클릭 시 결과 확인 가능
2단계
- useForm과 Tiptap 에디터 연결
- editor.getHTML()을 form의 description 필드로 등록
- 기본적인 Tailwind 스타일 + 다크모드 대응
- 제출 시 console.log로 HTML 출력 확인
useEditor 주요 인자 정리
const editor = useEditor({
content, // 초기 콘텐츠 (HTML, JSON, Text 등)
extensions, // 사용하고 싶은 확장 기능 (Bold, Image 등)
editable, // 에디터 편집 가능 여부
autofocus, // 자동 포커스 여부 (boolean 또는 'start' 등)
injectCSS, // Tiptap 기본 CSS를 자동 삽입할지
editorProps, // ProseMirror의 추가 옵션 (attributes 등)
onUpdate, // 내용이 업데이트될 때 실행할 콜백
onCreate, // editor 생성 직후 실행할 콜백
onTransaction, // 트랜잭션 발생 시 실행할 콜백
});
1. content – 초기 콘텐츠
content: '<p>Hello World</p>'
// 또는
content: {
type: 'doc',
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] },
],
}
2. extensions – 사용할 확장 목록
extensions: [
StarterKit,
Underline,
Link.configure({ openOnClick: false }),
Image.configure({ inline: false }),
]
3. editable – 읽기 전용 모드 토글
editable: true // 또는 false
4. autofocus – 자동 포커스 위치
autofocus: true // 기본 true
autofocus: 'start' // 커서 시작 위치
5. editorProps – ProseMirror 속성 설정
editorProps: {
attributes: {
class: 'prose min-h-[300px] px-4 py-2 focus:outline-none',
},
}
6. onUpdate – 내용 변경 시 콜백
onUpdate: ({ editor }) => {
const html = editor.getHTML();
const json = editor.getJSON();
console.log('내용 변경됨:', html, json);
}
7. onCreate, onTransaction
- onCreate: 에디터가 처음 생성되었을 때
- onTransaction: 내부 트랜잭션 발생 시 (커서 이동 포함)
onCreate: ({ editor }) => { ... }
onTransaction: ({ editor, transaction }) => { ... }
코드 예제
// components/RichTextEditorField.tsx
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useEffect } from 'react';
interface RichTextEditorFieldProps {
onChange: (value: string) => void;
value?: string;
}
export const RichTextEditorField = ({ onChange, value }: RichTextEditorFieldProps) => {
const editor = useEditor({
extensions: [StarterKit],
content: value ?? '',
editorProps: {
attributes: {
class:
'min-h-[200px] w-full p-4 rounded-md border bg-white dark:bg-neutral-900 dark:text-white text-black shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500',
},
},
onUpdate: ({ editor }) => {
onChange(editor.getHTML()); // 에디터 업데이트 시 form 상태 반영
},
});
// 외부 value가 변경될 경우 반영
useEffect(() => {
if (editor && value && editor.getHTML() !== value) {
editor.commands.setContent(value);
}
}, [value]);
return <EditorContent editor={editor} />;
};
통합 예시
// pages/FormPage.tsx
import { useForm } from 'react-hook-form';
import { RichTextEditorField } from '@/components/RichTextEditorField';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
type FormValues = {
title: string;
description: string;
};
export default function FormPage() {
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<FormValues>({
defaultValues: {
title: '',
description: '',
},
});
const onSubmit = (data: FormValues) => {
console.log('폼 제출:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-6">
{/* title input */}
<div>
<label className="block text-sm font-medium mb-1">Title</label>
<Input placeholder="제목 입력" {...register('title', { required: true })} />
{errors.title && <p className="text-red-500 text-sm mt-1">제목은 필수입니다.</p>}
</div>
{/* description (tiptap) */}
<div>
<label className="block text-sm font-medium mb-1">Description</label>
<RichTextEditorField
value={watch('description')}
onChange={(val) => setValue('description', val)}
/>
{errors.description && <p className="text-red-500 text-sm mt-1">내용을 입력해주세요.</p>}
</div>
<Button type="submit" className="w-full">제출</Button>
</form>
);
}
| onUpdate | 에디터 변경 시 getHTML()을 onChange로 전달해 form에 저장 |
| useForm | register, setValue, watch 사용 |
| Tailwind 스타일 | EditorContent에 기본 입력 스타일 적용 (dark/light 포함) |
| 초기화/동기화 | 외부 상태 변경 시 setContent로 반영 |
3단계
- 이미지를 포함한 노트 작성 가능
- 링크/하이라이트/언더라인 등 풍부한 마크업 지원
- 사용자가 선택 시 나타나는 BubbleMenu, 항상 보이는 FloatingMenu로 툴바 구성
1. 필수 확장 기능 설치
npm install @tiptap/extension-image @tiptap/extension-link @tiptap/extension-placeholder @tiptap/extension-underline @tiptap/extension-highlight
2. 확장 포함한 에디터 구성 예제
// components/RichTextEditor.tsx
import {
useEditor,
EditorContent,
BubbleMenu,
FloatingMenu,
} from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Placeholder from '@tiptap/extension-placeholder';
import Underline from '@tiptap/extension-underline';
import Highlight from '@tiptap/extension-highlight';
import Link from '@tiptap/extension-link';
import Image from '@tiptap/extension-image';
interface Props {
value: string;
onChange: (val: string) => void;
}
export const RichTextEditor = ({ value, onChange }: Props) => {
const editor = useEditor({
content: value,
extensions: [
StarterKit,
Underline,
Highlight,
Link.configure({
openOnClick: false,
}),
Image,
Placeholder.configure({
placeholder: '내용을 입력하세요...',
}),
],
editorProps: {
attributes: {
class:
'min-h-[200px] w-full p-4 rounded-md border bg-white dark:bg-neutral-900 text-black dark:text-white shadow-sm focus:outline-none',
},
},
onUpdate({ editor }) {
onChange(editor.getHTML());
},
});
if (!editor) return null;
return (
<div className="relative">
{/* Floating Toolbar (상단 고정 툴바) */}
<FloatingMenu
editor={editor}
className="flex gap-2 bg-gray-100 dark:bg-neutral-800 p-2 rounded shadow-md mb-2"
>
<button onClick={() => editor.chain().focus().toggleBold().run()}>
Bold
</button>
<button onClick={() => editor.chain().focus().toggleItalic().run()}>
Italic
</button>
<button onClick={() => editor.chain().focus().toggleUnderline().run()}>
Underline
</button>
<button
onClick={() => editor.chain().focus().toggleHighlight().run()}
>
Highlight
</button>
<button
onClick={() => {
const url = prompt('URL 입력:');
if (url) {
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
}
}}
>
Link
</button>
<button
onClick={() => {
const url = prompt('이미지 주소 입력:');
if (url) {
editor.chain().focus().setImage({ src: url }).run();
}
}}
>
Image
</button>
</FloatingMenu>
{/* BubbleMenu (선택 시 메뉴) */}
<BubbleMenu
editor={editor}
tippyOptions={{ duration: 100 }}
className="flex gap-2 bg-gray-200 dark:bg-neutral-700 p-2 rounded shadow"
>
<button onClick={() => editor.chain().focus().toggleBold().run()}>
B
</button>
<button onClick={() => editor.chain().focus().toggleItalic().run()}>
I
</button>
<button onClick={() => editor.chain().focus().toggleUnderline().run()}>
U
</button>
</BubbleMenu>
<EditorContent editor={editor} />
</div>
);
};
3. 멘션(Mention) / 해시태그(Tag) 도입 (추후 단계)
- @tiptap/extension-mention → 사용자 목록, 태그 목록에서 선택 지원
- @tiptap/extension-character-count → 글자수 제한 UI
이건 다음 4단계에서 따로 다루겠습니다.
4. 실용 팁
| BubbleMenu | 텍스트 선택 시 나타나는 툴바 |
| FloatingMenu | 항상 고정된 상단 툴바 구성 |
| editor.commands.setImage({ src }) | 이미지 삽입 |
| editor.commands.setLink({ href }) | 링크 추가 |
| editor.commands.toggleHighlight() | 형광펜 기능 (작성 강조용) |
BubbleMenu 구현
- 사용자가 텍스트를 마우스로 드래그 선택하면 Bold, Italic, Underline 등을 제어할 수 있는 BubbleMenu 표시
구현 예시
import { BubbleMenu } from '@tiptap/react';
<BubbleMenu
editor={editor}
tippyOptions={{ duration: 100 }}
className="bg-white dark:bg-neutral-800 shadow rounded p-2 flex gap-2"
>
<button onClick={() => editor.chain().focus().toggleBold().run()}>B</button>
<button onClick={() => editor.chain().focus().toggleItalic().run()}>I</button>
<button onClick={() => editor.chain().focus().toggleUnderline().run()}>U</button>
</BubbleMenu>
BubbleMenu는 textSelection이 있을 때만 자동으로 표시됩니다.
URL 자동 링크화
- 사용자가 에디터에 https://... 같은 텍스트를 입력하면 자동으로 링크 마크를 적용
구현 방법
- Link 익스텐션 설정 시 autolink: true 활성화
- linkify-it 패키지 자동 포함됨
import Link from '@tiptap/extension-link';
Link.configure({
autolink: true,
openOnClick: false,
linkOnPaste: true,
validate: href => /^https?:\/\//.test(href),
})
linkOnPaste: true를 설정하면 URL을 붙여넣을 때 자동으로 링크로 변환됩니다.
이미지 드래그/삽입
- 사용자가 이미지를 드래그하거나 붙여넣으면 자동으로 삽입
- 또는 수동으로 삽입 버튼 클릭
1단계: 이미지 extension 설치
npm install @tiptap/extension-image
2단계: 에디터 확장에 포함
Image.configure({
allowBase64: true, // 개발용 base64 이미지 허용
}),
3단계: 드래그 앤 드롭 구현
이미지 삽입은 기본 onPaste, onDrop 이벤트를 통해 지원됩니다. 하지만 아래처럼 직접 커스터마이징할 수도 있습니다:
editor.view.dom.addEventListener('drop', async (event) => {
const file = event.dataTransfer?.files?.[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = () => {
editor.chain().focus().setImage({ src: reader.result as string }).run();
};
reader.readAsDataURL(file);
}
});
이후에는 base64 말고 실제 업로드된 URL로 삽입하도록 개선하는 것이 좋습니다 (S3, Supabase 등).
4단계
- 커스텀 Extension, Node, Mark를 만들 수 있다.
- Tiptap 확장 구조의 구성요소 (addOptions, addCommands, addNodeView 등)를 이해한다.
- <mention>, <highlight> 등 커스텀 태그를 다룰 수 있다.
개념 이해
1. Extension의 핵심 구조
import { Extension } from '@tiptap/core';
export const CustomExtension = Extension.create({
name: 'custom',
addOptions() {
return {
someOption: true,
};
},
addCommands() {
return {
setCustom:
() =>
({ commands }) => {
return commands.insertContent('<custom>hello</custom>');
},
};
},
addKeyboardShortcuts() {
return {
'Mod-k': () => this.editor.commands.setCustom(),
};
},
});
2. Node vs Mark 차이
항목 | Node | Mark
| 정의 위치 | 트리의 독립적인 블록 | 텍스트에 인라인으로 적용 |
| 예시 | Paragraph, Image, ListItem | Bold, Italic, Link |
| HTML 변환 | <node>content</node> | <span style="...">text</span> |
Node는 구조(Block), Mark는 스타일(Inline)
예: <p><strong>Hello</strong></p> 에서 p는 Node, strong은 Mark
실습 예제
1. Highlight 마크 만들기
import { Mark } from '@tiptap/core';
export const Highlight = Mark.create({
name: 'highlight',
parseHTML() {
return [{ tag: 'mark' }];
},
renderHTML() {
return ['mark', 0];
},
addCommands() {
return {
toggleHighlight:
() =>
({ commands }) => {
return commands.toggleMark(this.name);
},
};
},
});
사용 방법
editor.commands.toggleHighlight();
2. Mention 노드 만들기
import { Node } from '@tiptap/core';
export const Mention = Node.create({
name: 'mention',
group: 'inline',
inline: true,
atom: true,
addAttributes() {
return {
id: {},
label: {},
};
},
parseHTML() {
return [{ tag: 'span[data-mention]' }];
},
renderHTML({ HTMLAttributes }) {
return ['span', { ...HTMLAttributes, 'data-mention': '' }, 0];
},
addCommands() {
return {
insertMention:
(attrs) =>
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs,
});
},
};
},
});
이 구조는 멘션 자동완성과도 연동 가능합니다 (e.g. SlashCommand, Suggestion 플러그인).
추가 기능 예시
- SlashCommand 명령어 확장
- CustomNodeView를 통한 React 컴포넌트와의 연결 (addNodeView)
- addProseMirrorPlugins()로 키보드, 입력 이벤트 커스터마이징
기능 | 사용 API | 예시
| 옵션 정의 | addOptions | placeholder 등 설정 |
| 명령 정의 | addCommands | toggleHighlight, insertMention |
| 마크 정의 | Mark.create({...}) | Bold, Italic, Underline |
| 노드 정의 | Node.create({...}) | Paragraph, Mention, Image |
| 컴포넌트 연결 | addNodeView() | React 기반 UI 렌더링 |
1. <callout> 커스텀 Node 확장
"주의", "정보", "성공" 등 다양한 스타일의 박스를 만들 수 있는 블록 노드입니다. Markdown의 :::info 같은 역할.
1-1. 목표
- 블록 단위의 Callout 노드 구현
- type 속성(info, warning, success)을 가짐
- <div data-type="callout" data-callout-type="info">...</div> 형태로 HTML 출력
1-2. Callout.ts
import { Node, mergeAttributes } from '@tiptap/core';
export interface CalloutOptions {
defaultType: 'info' | 'warning' | 'success';
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
callout: {
setCallout: (type: string) => ReturnType;
};
}
}
export const Callout = Node.create<CalloutOptions>({
name: 'callout',
group: 'block',
content: 'block+',
defining: true,
addOptions() {
return {
defaultType: 'info',
};
},
addAttributes() {
return {
type: {
default: this.options.defaultType,
},
};
},
parseHTML() {
return [{ tag: 'div[data-type="callout"]' }];
},
renderHTML({ HTMLAttributes }) {
return [
'div',
mergeAttributes(HTMLAttributes, {
'data-type': 'callout',
class: `callout callout-${HTMLAttributes.type}`,
}),
0,
];
},
addCommands() {
return {
setCallout:
(type: string) =>
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: { type },
content: [
{
type: 'paragraph',
},
],
});
},
};
},
});
1-3. 스타일 예시 (Tailwind)
.callout-info {
@apply bg-blue-50 border-l-4 border-blue-400 p-4 text-blue-700;
}
.callout-warning {
@apply bg-yellow-50 border-l-4 border-yellow-400 p-4 text-yellow-800;
}
.callout-success {
@apply bg-green-50 border-l-4 border-green-400 p-4 text-green-700;
}
2. <mention> 태그 노드 구현 + 팝업 UI
사용자 이름 또는 해시태그 자동완성 기능
2-1. Mention.ts
import { Node, mergeAttributes } from '@tiptap/core';
export const Mention = Node.create({
name: 'mention',
group: 'inline',
inline: true,
atom: true,
selectable: false,
addAttributes() {
return {
id: { default: null },
label: { default: null },
};
},
parseHTML() {
return [{ tag: 'span[data-mention]' }];
},
renderHTML({ HTMLAttributes }) {
return [
'span',
mergeAttributes(HTMLAttributes, {
'data-mention': '',
class: 'mention',
}),
`@${HTMLAttributes.label}`,
];
},
addCommands() {
return {
insertMention:
(attrs) =>
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs,
});
},
};
},
});
2-2. mention 스타일
.mention {
@apply px-1 bg-blue-100 text-blue-600 rounded;
}
3. 팝업 UI 연동 (Suggestion)
Tiptap은 @tiptap/suggestion 패키지를 사용하여 자동완성 UI를 띄울 수 있습니다.
npm install @tiptap/suggestion
5단계
Tiptap 상태의 저장 및 복원, SSR 환경 호환, 렌더링 최적화를 마스터하기
1. 상태 저장: editor.getJSON()
Tiptap은 내부 문서를 JSON 형태로 직렬화 가능:
const json = editor.getJSON();
localStorage.setItem('note', JSON.stringify(json));
저장된 JSON을 DB에 저장하거나, 로컬스토리지/IndexedDB에 사용할 수 있습니다.
2. 상태 복원: editor.commands.setContent(json)
복원 시에는 아래처럼 사용:
useEffect(() => {
const saved = localStorage.getItem('note');
if (saved) {
editor?.commands.setContent(JSON.parse(saved));
}
}, [editor]);
주의: editor.commands.setContent()는 반드시 editor가 초기화된 이후 실행해야 하므로 useEffect 또는 이벤트 핸들러에서 사용합니다.
3. 상태 초기화 및 디버깅 팁
- editor.commands.clearContent() → 전체 초기화
- console.log(editor.getJSON()) → 현재 상태 확인
- editor.isFocused, editor.isEditable, editor.isEmpty → 상태 체크
필요시 onBlur, onFocus, onSelectionUpdate 이벤트를 활용해 사용자 행동을 추적 가능
4. SSR 환경 대응 (Next.js 등)
React 서버 사이드 렌더링 환경에서는 window, document 사용이 불가능하므로, 다음과 같은 전략을 사용해야 함:
4-1. dynamic()을 활용한 클라이언트 전용 로딩
// pages/editor.tsx
import dynamic from 'next/dynamic';
const TiptapEditor = dynamic(() => import('@/components/TiptapEditor'), {
ssr: false, // 서버에서는 렌더링하지 않음
});
export default function EditorPage() {
return <TiptapEditor />;
}
4-2. 내부에서 useEditor는 useEffect 안에서 초기화 가능
const [editor, setEditor] = useState<Editor | null>(null);
useEffect(() => {
const editorInstance = new Editor({
extensions: [StarterKit],
content: '',
});
setEditor(editorInstance);
return () => editorInstance.destroy();
}, []);
5. 퍼포먼스 고려
- editable: false로 read-only 최적화 가능
- debounce로 onUpdate 감지 줄이기 (lodash.debounce 추천)
- FloatingMenu, BubbleMenu는 focus 이벤트 기준이므로 조건부 렌더링 필요
- 대규모 문서: extension 수 줄이기, 이미지 등 lazy load 필요
5단계 실전 통합: Spring 백엔드 + Tiptap 연동
1. 저장 전략 (HTML/JSON 전송)
- editor.getHTML() 또는 editor.getJSON() 사용
- Spring 백엔드에서 /note/save 같은 엔드포인트 생성
- 프론트엔드에서는 fetch 또는 React Query를 통해 저장 요청
const saveContent = async () => {
const json = editor?.getJSON();
await fetch('/api/note/save', {
method: 'POST',
body: JSON.stringify({ content: json }),
headers: { 'Content-Type': 'application/json' },
});
};
2. 복원 전략 (서버 or localStorage)
- 서버에서 불러온 JSON 데이터를 editor.commands.setContent(json)으로 적용
- SSR일 경우에는 dynamic import로 editor 생성
editor.commands.setContent(savedJson);
3. SSR 안전한 렌더링 (Next.js 기준)
- dynamic(() => import('./Editor'), { ssr: false }) 사용
- 내부에서 useEffect로 editor 생성 → 클라이언트 전용 처리
4. 뷰 모드와 수정 모드 분리
- 뷰 모드: readOnly 상태로 EditorContent 렌더링
- 수정 모드: 일반 editor 활성화
<EditorContent editor={editor} editable={!readOnly} />
5. 로딩 최적화
- 로딩 중 Skeleton 표시
- useQuery, useEffect 등과 조합해서 Skeleton 노출
return isLoading ? <Skeleton className="h-[300px]" /> : <EditorContent editor={editor} />;
6. onUpdate 최적화
- editor.on('update', debounce(() => ..., 300)) 사용
useEffect(() => {
if (!editor) return;
const handler = debounce(() => {
const json = editor.getJSON();
saveToLocalStorage(json);
}, 300);
editor.on('update', handler);
return () => editor.off('update', handler);
}, [editor]);
'JavaScript > React' 카테고리의 다른 글
| forwardRef (4) | 2025.08.18 |
|---|---|
| useDropzone, browser-image-compression [image upload 라이브러리] (6) | 2025.07.10 |
| 파일 네이밍 컨벤션(File Naming Convention) - React Feature Folder (1) | 2025.06.14 |
| npm React 라이브러리 배포 (0) | 2025.06.13 |
| template 자동화 (0) | 2025.06.12 |