
Kim JeongHyeonFrontend Focused Fullstack Developer
Markdown 에디터의 실시간 미리보기, Tiptap 기반 Rich Text 에디터의 커스텀 확장, 이미지 업로드와 정렬 기능을 구현합니다.
블로그 에디터를 설계할 때 가장 먼저 결정해야 할 것은 입력 방식입니다. Markdown은 개발자에게 익숙하고 빠르지만, 비개발자에게는 진입 장벽이 됩니다. Rich Text 에디터는 직관적이지만 HTML 출력의 일관성 관리가 어렵습니다.
결론적으로 둘 다 지원하기로 했습니다. 포스트마다 content_type을 저장하고, 에디터 UI에서 자유롭게 전환할 수 있게 합니다.
Markdown 에디터는 단순한 <textarea>와 실시간 미리보기 패널의 조합입니다. 핵심은 스크롤 동기화입니다.
// 에디터 영역과 미리보기 영역의 스크롤을 동기화
const handleEditorScroll = (e: React.UIEvent) => {
if (syncLock.current) return;
syncLock.current = true;
const editor = e.currentTarget;
const preview = previewRef.current;
if (!preview) return;
const ratio = editor.scrollTop / (editor.scrollHeight - editor.clientHeight);
preview.scrollTop = ratio * (preview.scrollHeight - preview.clientHeight);
requestAnimationFrame(() => { syncLock.current = false; });
};비율 기반 동기화는 에디터와 미리보기의 콘텐츠 높이가 다르더라도 자연스럽게 작동합니다.
Tiptap은 ProseMirror 기반의 모듈형 에디터 프레임워크입니다. 필요한 기능만 Extension으로 추가할 수 있어 번들 크기를 최소화할 수 있습니다.
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
const editor = useEditor({
extensions: [
StarterKit,
Image.configure({ inline: false }),
Link.configure({ openOnClick: false }),
CodeBlockLowlight.configure({ lowlight }),
],
content: initialContent,
});기본 Image extension은 정렬 기능이 없습니다. data-align 속성을 추가하는 커스텀 확장을 만들었습니다:
const CustomImage = Image.extend({
addAttributes() {
return {
...this.parent?.(),
dataAlign: {
default: "center",
parseHTML: (el) => el.getAttribute("data-align") || "center",
renderHTML: (attrs) => ({
"data-align": attrs.dataAlign || "center",
}),
},
width: {
default: null,
parseHTML: (el) => el.getAttribute("data-width") || null,
renderHTML: (attrs) => {
if (!attrs.width) return {};
return { "data-width": attrs.width, style: `width: ${attrs.width}` };
},
},
};
},
});CSS에서 data-align 값에 따라 margin으로 정렬합니다:
.tiptap img[data-align="left"] {
margin-left: 0;
margin-right: auto;
}
.tiptap img[data-align="center"] {
margin-left: auto;
margin-right: auto;
}
.tiptap img[data-align="right"] {
margin-left: auto;
margin-right: 0;
}사용자가 이미지를 드래그&드롭하거나 붙여넣기하면:
<img> 삽입Tiptap의 CodeBlockLowlight 확장과 highlight.js를 조합하면 에디터에서 실시간 구문 하이라이팅이 가능합니다.
import { lowlight } from "lowlight/lib/core";
import javascript from "highlight.js/lib/languages/javascript";
import typescript from "highlight.js/lib/languages/typescript";
import css from "highlight.js/lib/languages/css";
lowlight.registerLanguage("javascript", javascript);
lowlight.registerLanguage("typescript", typescript);
lowlight.registerLanguage("css", css);에디터가 완성되었으니, 다음 편에서는 블로그의 퍼블릭 페이지를 만듭니다. 포스트 목록의 검색/필터링, 시리즈 네비게이션, 댓글 시스템, 그리고 좋아요 기능까지 구현합니다.