Skip to content

Step 2 — ProseMirror 통합 + 핵심 API

상태: ✅ 완료 (2026-04-14) 목표: 실제 Markdown 편집 동작 + 외부에서 사용할 최소 public API 확정

과거 로드맵의 Step 2 (API 보강) 는 본 Step에 병합되었다 (steps/step-01-scaffolding.md 설계 변경 이력 참조).


목차

  1. 설치된 의존성
  2. 결정 사항
  3. 파일별 변경 내역
  4. 공개 API
  5. 검증 결과
  6. 알려진 제한 / 다음 Step에서 해결할 것

설치된 의존성

prosemirror-state    ^1.4.4   — 에디터 state (transaction, selection)
prosemirror-view     ^1.41.8  — DOM 렌더링 및 사용자 입력 처리
prosemirror-model    ^1.25.4  — 문서 모델 (Node, Mark, Schema)
prosemirror-markdown ^1.13.4  — 기본 스키마 + 파서 + 직렬화기
prosemirror-commands ^1.7.1   — baseKeymap (Enter, Backspace 등)
prosemirror-keymap   ^1.2.3   — keymap 플러그인 래퍼

모두 dependencies (not peerDependencies) 로 설치 — 사용자가 별도 설치할 필요 없이 npm i @newtil/editor 한 번으로 완결되도록.


결정 사항

1. 스키마는 prosemirror-markdown 기본값 사용

  • 커스텀 스키마 작성하지 않음
  • 제공 노드: doc, paragraph, heading(1~6), blockquote, bullet_list, ordered_list, list_item, code_block, horizontal_rule, hard_break, image
  • 제공 마크: strong, em, code, link
  • 이유: MVP에서는 Markdown 표현 범위와 정확히 일치하는 기본 스키마가 적절. 커스텀은 추후 필요 시.

2. 플러그인 구성: 최소한의 편집 가능성만

ts
plugins: [keymap(baseKeymap)]
  • baseKeymap = Enter (문단 분리), Backspace (문단 합치기/노드 삭제), Delete, Mod-Enter (hard break), Shift-Enter (hard break) 등 기본 편집 키
  • undo/redo, inputRules (# → heading 자동 변환), 커스텀 단축키(Mod-B 등) 는 Step 3 에서 추가

3. 출력 포맷 2가지 제공

Getter구현
editor.markdowndefaultMarkdownSerializer.serialize(doc)
editor.htmlDOMSerializer.fromSchema(schema).serializeFragment(doc.content)<div> 에 담아 innerHTML 반환

value getter/setter 는 markdown 의 별칭.

4. change 이벤트 발행 조건

ts
if (tr.docChanged) {
  dispatchEvent(new CustomEvent("change", { detail: { markdown, html } }));
}
  • Transaction의 docChanged 가 true일 때만 발행
  • 즉, 커서 이동이나 selection 변화만으로는 이벤트 안 뜸
  • detailmarkdown, html 두 포맷 모두 포함 — 사용자가 필요한 쪽만 사용

5. CSS 자동 주입 전략 유지

  • import "prosemirror-view/style/prosemirror.css" — PM 기본 스타일
  • import "./styles.css" — 우리 커스텀 스타일
  • tsup의 injectStyle: true 로 최종 JS 번들에 <style> 태그 주입됨
  • 사용자는 import "@newtil/editor" 한 번으로 스타일까지 적용

6. 속성 ↔ 프로퍼티 동기화 정책

  • value attribute 변경 → attributeChangedCallback 에서 setMarkdown 호출 (단, 현재 값과 같으면 skip)
  • editor.markdown = "..." → 내부적으로 새 PM state 생성
  • attribute 와 property 간 무한 루프 방지: 같은 값이면 아무것도 안 함

파일별 변경 내역

src/index.ts — 완전 재작성

Step 1의 contentEditable 기반 stub 제거. ProseMirror 기반으로 교체.

핵심 구조:

ts
export class NewtilEditor extends HTMLElement {
  private view: EditorView | null = null;

  connectedCallback(): void { this.mount(); }
  disconnectedCallback(): void { this.view?.destroy(); /* cleanup */ }

  get markdown(): string { /* serialize */ }
  set markdown(md: string) { /* parse & replace doc */ }
  get html(): string { /* DOMSerializer */ }

  private mount(): void {
    // parse initial value → EditorState → EditorView
    // dispatchTransaction 훅에서 change 이벤트 발행
  }
}

타입 export:

ts
export interface NewtilEditorChangeDetail {
  markdown: string;
  html: string;
}

src/styles.css — 확장

.ProseMirror 셀렉터 하위의 Markdown 블록 요소(p, h1~h6, ul, ol, li, blockquote, code, pre, a, hr, img)에 기본 스타일 추가.

주: Step 4 (CSS 격리 보강) 에서 이 기본 스타일은 더 정교하게 다듬어지며, @layer 로 래핑될 예정.

src/globals.d.ts — 신규

ts
declare module "*.css";

TypeScript 가 .css side-effect import 를 인식하도록 하는 ambient 선언.

demo.ts — 재작성

  • 실제 Markdown 샘플 (# Welcome...) 을 속성으로 주입
  • change 이벤트 구독 → 하단 #md-display, #html-display 에 실시간 표시

index.html — 확장

  • 2개 분할 패널 (Markdown / HTML) 레이아웃 추가
  • 반응형 (720px 이하에서 1열로)

공개 API

속성 (Attribute)

이름타입설명
valuestring초기 Markdown 문자열 (attribute로 주입)

프로퍼티 (Property)

이름타입읽기/쓰기설명
markdownstringR/W현재 내용을 Markdown 으로
htmlstringR현재 내용을 HTML 로 (읽기 전용)
valuestringR/Wmarkdown 의 별칭

이벤트

이벤트명detail발행 시점
change{ markdown: string, html: string }Transaction의 docChanged === true 일 때

사용 예시

js
import "@newtil/editor";

const editor = document.querySelector("newtil-editor");

// 1) attribute로 초기값 주입
editor.setAttribute("value", "# Hello");

// 2) property로 동적 변경
editor.markdown = "## Updated content";

// 3) 편집 결과 구독
editor.addEventListener("change", (e) => {
  console.log(e.detail.markdown);
  console.log(e.detail.html);
});

검증 결과

명령결과
npm install prosemirror-*✅ 6개 패키지 + 전이 의존성 설치
npm run typecheck✅ 오류 0건
npm run build✅ ESM 5.69KB + CJS 5.96KB + DTS (ProseMirror는 external)
npm run dev (Vite)✅ HMR 정상, 의존성 사전 최적화 자동 수행

브라우저 동작 확인 항목 (http://localhost:5174):

  • 초기 Markdown (# Welcome...) 이 제목/리스트/인용구 등으로 렌더링됨
  • Enter → 새 문단 생성
  • Backspace → 빈 문단 제거
  • 하단 Markdown 패널에 현재 상태가 \n\n 포함된 올바른 Markdown 으로 표시
  • 하단 HTML 패널에 <h1>, <p>, <ul> 등 올바른 HTML 로 표시
  • 외부 HTML(예: 웹페이지 텍스트) 붙여넣기 시 자동 정제 — 스키마 허용 태그만 유지

알려진 제한 / 다음 Step에서 해결할 것

제한해결 예정 Step
# , ** 등 Markdown 단축 입력 시 자동 변환 안 됨Step 3 (inputRules)
Ctrl+B, Ctrl+I 등 단축키 없음Step 3 (keymap 확장)
undo/redo 없음Step 3 (prosemirror-history)
툴바 UI 없음Step 3
placeholder 미지원Step 3
readonly 미지원Step 3
focus/blur 이벤트 미발행Step 3
호스트 페이지 CSS 영향 가능성Step 4 (격리 다층 전략)
@newtil/* 패밀리 테마 없음Step 4
React/Vue 래퍼 없음Step 5

설계 변경 이력

(없음 — Step 2는 기존 설계를 그대로 구현)