구현 로드맵
@newtil/editor 를 단계별로 구현하는 계획입니다. 전체 설계는 design.md 를 참조하세요.
단계별 접근 원칙
전체를 한 번에 만들면 어디서 깨졌는지 추적하기 어렵기 때문에 각 Step마다 실제 브라우저 동작을 검증한 뒤 다음으로 넘어갑니다. 각 Step은 완결된 커밋 단위가 되며, 문제가 생기면 직전 Step으로 안전하게 되돌아갈 수 있습니다.
편집 모델 (중요 결정)
편집은 WYSIWYG만 지원합니다. Markdown은 읽기 전용으로만 노출되며, source/WYSIWYG 모드 토글은 제공하지 않습니다.
| 항목 | 지원 여부 |
|---|---|
| WYSIWYG 편집 (ProseMirror) | ✅ |
Markdown 읽기 (editor.markdown getter) | ✅ |
HTML 읽기 (editor.html getter) | ✅ |
| HTML 붙여넣기 (자동 정제) | ✅ |
Markdown source 직접 편집 (mode="source") | ❌ 미지원 |
| 분할 뷰 (source ↔ wysiwyg 라이브) | ❌ 미지원 |
결정 근거:
- 양방향 편집은 round-trip 정규화 문제가 피할 수 없음 (강조 기호, 리스트 기호, 여백 등이 통일됨)
- CodeMirror 통합 및 양방향 동기화 로직은 복잡도 대비 사용자 가치 낮음
- 읽기 전용 Markdown 출력으로 대부분의 실사용 시나리오(저장, 복사, 백엔드 전송)가 커버됨
Step 1 — 스캐폴딩 ✅
상태: 완료 (2026-04-14) 상세 기록: steps/step-01-scaffolding.md
달성한 것
- [x] 프로젝트 폴더 구조 수립
- [x]
package.json작성 (@newtil/editor@0.1.0, scoped + public) - [x] tsup 라이브러리 빌드 설정 (ESM + CJS + .d.ts, CSS 자동 주입)
- [x] Vite 데모 개발 서버 설정
- [x] TypeScript strict 설정
- [x] 최소
<newtil-editor>Web Component (contentEditable 기반 뼈대) - [x] BEM 스타일 훅 네이밍 (
.newtil-editor,.newtil-editor__content) 적용 - [x] Shadow DOM 미사용 확정 (Light DOM + 계층 1 격리)
- [x] 데모 페이지 (
index.html+demo.ts) - [x]
git init+ GitHub 원격 연결 + 첫 푸시
검증 결과
npm install— 54 패키지, 정상npm run typecheck— 오류 없음npm run build— ESM(2.3KB) + CJS(2.3KB) + DTS 생성npm run dev— Vite 서버 정상 기동
Step 2 — ProseMirror 통합 + 핵심 API ✅
상태: 완료 (2026-04-14) 상세 기록: steps/step-02-prosemirror.md목표: 실제 Markdown 편집 동작 + 외부에서 사용할 최소 public API 확정
과거 Step 2 (API 보강) 는 이 Step에 병합됨. API는 ProseMirror state를 데이터 원천으로 삼아야 의미가 있으므로 통합이 자연스러움.
달성한 것
2-A. ProseMirror 통합
- [x] 의존성 추가:
prosemirror-state,prosemirror-view,prosemirror-model,prosemirror-markdown,prosemirror-commands,prosemirror-keymap(기본 스키마로prosemirror-markdown제공 스키마 채택 —prosemirror-schema-basic미설치) - [x]
defaultMarkdownParser로 초기value를 ProseMirror Document 로 변환 - [x]
EditorView를.newtil-editor__content에 마운트 - [x] 기존
contentEditable뼈대를 ProseMirror 기반으로 교체 - [x]
baseKeymap플러그인으로 Enter/Backspace 등 기본 편집 키 활성화
2-B. 핵심 API
- [x] 속성(attribute):
value(초기 Markdown 문자열) - [x] 프로퍼티(property):
markdowngetter/setter,htmlgetter,value별칭 - [x] 이벤트:
change—CustomEvent<{markdown: string, html: string}>발행 (Transaction의docChanged === true일 때) - [x] 출력 직렬화: Markdown은
defaultMarkdownSerializer, HTML은DOMSerializer사용 - [x] 타입 export:
NewtilEditorChangeDetail
2-C. 데모 페이지 갱신
- [x]
index.html2개 분할 패널 (Markdown / HTML) 추가, 반응형 레이아웃 - [x]
demo.ts에서 실제 Markdown 샘플을 속성으로 주입 - [x]
change이벤트 구독으로 하단 패널 실시간 갱신 (읽기 전용)
검증 결과
npm run typecheck— 오류 없음npm run build— ESM 5.69KB + CJS 5.96KB + DTS (ProseMirror 는 external)npm run dev— Vite HMR 정상, 의존성 사전 최적화 자동 완료- 브라우저 — 초기 Markdown 렌더링, Enter/Backspace 동작, 실시간 Markdown/HTML 표시, HTML 붙여넣기 자동 정제 확인
Step 3 — 명령어 입력 시스템 ⬜
상태: 예정 목표: 사용자가 명령어를 외우지 않고도 모든 편집 기능에 접근 가능 + 파워유저 편의 제공
설계 재검토 (2026-04-14): 초기에는 "툴바/단축키/InputRules" 로 묶여 있었으나, 슬래시 명령 메뉴(Notion/Linear 스타일)가 주 UX 여야 한다는 결정에 따라 Step 3를 명령어 입력 시스템 전용 으로 재정의. 툴바는 Step 4로 분리.
3-A. 기반 플러그인 (파워유저 편의) ✅
상세 기록: steps/step-03a-commands-foundation.md
prosemirror-history + prosemirror-inputrules + 확장 keymap. 슬래시 메뉴 없이도 Markdown 문법이나 단축키로 바로 편집 가능한 상태를 만든다.
- [x] 의존성 추가:
prosemirror-history,prosemirror-inputrules,prosemirror-schema-list - [x]
history()플러그인 추가 → undo/redo 상태 관리 - [x] InputRules — Markdown 타이핑 시 자동 블록 변환:
- [x]
#~######→ heading 1~6 - [x]
-/*/+→ bullet list - [x]
1.→ ordered list - [x]
>→ blockquote - [x]
```→ code block - [x] 보너스: smartQuotes, emDash (
--), ellipsis (...)
- [x]
- [x] 확장 keymap:
- [x]
Mod-z→ undo - [x]
Mod-Shift-z/Mod-y→ redo - [x]
Mod-b→ toggle strong - [x]
Mod-i→ toggle em - [x]
Mod-`→ toggle inline code - [x]
Enter(리스트 안) →splitListItem - [x]
Tab/Shift-Tab(리스트 안) →sinkListItem/liftListItem
- [x]
- [x] 데모 페이지에 간단한 사용 힌트 추가 (Markdown 타이핑, 주요 단축키)
3-B. 슬래시 명령 메뉴 ⭐ (주 UX) ✅
상세 기록: steps/step-03b-slash-menu.md
"/" 입력 시 부유 메뉴가 뜨고, 타이핑으로 필터링 후 키보드/마우스/터치로 선택 → 현재 블록 변환 또는 새 요소 삽입.
- [x] 커스텀 ProseMirror 플러그인
slashMenu:- [x] 빈 블록 (또는 공백 뒤) 에서
/감지 (코드 블럭 내부는 제외) - [x] 플러그인 상태 유지: open, from, query, selectedIndex
- [x] 타이핑 시 query 업데이트 및 메뉴 필터 (apply 로직에서 doc 변경 추적)
- [x]
ArrowUp/ArrowDown→ 항목 이동 (순환) - [x]
Enter→ 선택 항목 실행 →/+ 쿼리 삭제 + 명령 적용 - [x]
Escape/Space/ 외부 클릭 / 커서 이동 → 메뉴 닫기
- [x] 빈 블록 (또는 공백 뒤) 에서
- [x] 부유 메뉴 UI (
.newtil-editor__slash-menu):- [x]
document.body에 append +position: fixed(overflow/stack 컨텍스트 격리) - [x]
EditorView.coordsAtPos로 위치, 뷰포트 경계 넘으면 좌/상 반전 - [x] 아이템 아이콘 + 라벨 + 설명 렌더링
- [x] 키보드 포커스 표시 (
aria-selected), 마우스 호버 선택 - [x] 결과 없을 때 "해당하는 명령이 없습니다" 표시
- [x]
- [x] 기본 명령 세트 (12개):
- [x] Heading 1~3, Text, Bullet list, Numbered list, Quote, Code block, Divider
- [x] Link (
window.prompt으로 URL + 텍스트 입력) - [x] Image (로컬 파일 선택 → Data URL, 취소 시 URL prompt fallback)
- [x] Video (
window.prompt으로 URL + poster 입력, 스키마 확장으로video노드 추가) - [x] 한글/영문 키워드로 필터링 (예:
/불,/quote,/인용,/동영상)
- [x] 확장 API 준비:
buildSlashMenu(schema, commands?)에 커스텀 명령 배열 주입 가능,SlashCommand타입 export- 사용자 대상
editor.registerCommand()래퍼는 Step 4+ 에서 추가 예정
- 사용자 대상
Step 4 — 툴바 + 속성 + 이벤트 ⬜
상태: 예정 목표: 선택 영역 기반 서식 조작 UI + 나머지 Web Component API
할 일
4-A. 선택 영역 floating toolbar
- [ ] 텍스트 선택 시 선택 영역 위에 뜨는 소형 툴바 (BEM
.newtil-editor__floating-toolbar) - [ ] 버튼:
B(strong),I(em),<>(code), 🔗 (link) - [ ] 버튼 활성화 상태 반영 (현재 mark 여부)
4-B. 상단 고정 툴바 (선택)
- [ ]
<newtil-editor toolbar="top">속성 시 상단 고정 툴바 표시 - [ ] Undo/Redo + Heading 드롭다운 + 리스트 버튼
4-C. 속성/이벤트 확장
- [ ]
placeholder속성 — 빈 에디터일 때 회색 힌트 - [ ]
readonly속성 — 편집 비활성화 - [ ]
focus,blur,input이벤트 passthrough
Step 5 — CSS 격리 보강 + @newtil/design-tokens 테마 파일 ⬜
상태: 예정 목표: Light DOM 환경에서도 호스트 페이지 CSS 영향을 최소화하고, @newtil/* 패밀리와 브랜드 일관성 확보
할 일
5-A. CSS 격리 다층 전략 적용 (design.md §9.5.3 참조)
- [ ] 계층 2 — 에디터 내부 모든 Markdown 블록 요소(
h1~h6,p,ul,ol,li,blockquote,code,pre,a,hr,img)에 기본 스타일 명시 재정의 - [ ] 계층 3 — 전체 에디터 CSS를
@layer newtil-editor.base/@layer newtil-editor.theme로 래핑 - [ ]
color: inherit,font-family: inherit을 적극 활용하여 부모 테마와 자연스러운 조화 유지
5-B. 테마 통합
- [ ]
src/themes/newtil.css작성 —@newtil/design-tokens의 CSS 변수(--newtil-color-*,--newtil-space-*등)를 참조하여 에디터 스타일 덮어쓰기 - [ ] tsup 빌드에 테마 파일 별도 출력 설정 (해시 없는 고정 파일명)
- [ ]
package.jsonexports필드에./themes/newtil추가 - [ ] README 및 demo 페이지에 테마 적용 예시 추가
패키지 구조 반영 (2026-04-14):
@newtil/*가 3-패키지로 분리되면서 토큰은@newtil/design-tokens가 단일 출처가 됨. 따라서 테마 파일은 특정 패키지(@newtil/css또는@newtil/ui)가 아니라@newtil/design-tokens의 변수만 참조한다. 결과적으로 utility(@newtil/css) 사용자, 컴포넌트(@newtil/ui) 사용자, 토큰만 쓰는 사용자 모두 동일한 테마를 받음.
선행 조건
@newtil/design-tokens가 npm에 publish 되어 있을 것 (변수명/계층 구조 확정 후)- 변수 명명 규칙(
--newtil-color-primary,--newtil-space-4등)이 안정화될 것
Step 6 — React / Vue 래퍼 ⬜
상태: 예정 목표: 프레임워크 사용자 편의성
할 일
- [ ]
src/react/index.tsx— React 래퍼 (<NewtilEditor>컴포넌트)- [ ]
ref전달, 이벤트는onChange,onInput형태로 매핑
- [ ]
- [ ]
src/vue/index.ts— Vue 래퍼 (v-model지원) - [ ] tsup 멀티 엔트리 설정
- [ ]
package.jsonexports에./react,./vue추가 - [ ] 각 래퍼의
peerDependencies정의 (react,vue를 optional 로)
Step 7 — 첫 npm 배포 ⬜
상태: 예정 목표: @newtil/editor@0.1.0 공개
할 일
- [ ]
npm pack --dry-run으로 배포 내용 검증 - [ ]
README.md최종화 (실사용 예시, 스크린샷) - [ ]
CHANGELOG.md작성 - [ ]
npm publish --access public - [ ] GitHub Release 태그
v0.1.0
선행 조건
- 최소한 Step 3 (명령어 입력 시스템) 까지는 완료되어야 함
- Step 4, 5, 6 은 이후 minor 버전(0.2.x) 에서 추가해도 무방
Step 이후 — 추가 기능 로드맵 (참고)
| 기능 | 우선순위 | 메모 |
|---|---|---|
| 읽기 전용 Markdown 미리보기 패널 | 중 | <newtil-editor preview="markdown"> 속성. 분할 뷰로 현재 Markdown 을 실시간 표시 (읽기 전용). Step 2의 API만으로도 사용자가 직접 구현 가능하므로 우선순위 중 |
| 모바일 floating toolbar | 중 | design.md §10.3 |
| 이미지 업로드 훅 | 중 | 사용자 콜백으로 S3/CDN 업로드 연동 |
| YouTube/Vimeo URL 자동 임베드 | 중 | URL 패턴 감지 → iframe 임베드 (동영상 지원의 첫 단계) |
@newtil/ui 툴바 통합 옵션 | 중 | 기본은 자체 toolbar 유지 (의존성 0). @newtil/ui 사용자가 옵트인하면 툴바를 @newtil/ui 의 버튼/아이콘으로 구성. 별도 빌드 옵션 또는 별도 패키지(@newtil/editor-ui-toolbar?) 검토 |
| 협업 편집 (Yjs) | 저 | ProseMirror 표준 연동 존재 |
| 코드블럭 신택스 하이라이팅 | 저 | prosemirror-highlight or highlight.js |
| 표 편집 | 저 | prosemirror-tables |
| Slash 커맨드 메뉴 | 저 | Notion 스타일 |
<video> 태그 임베드 노드 | 저 | 스키마 확장 + HTML 임베드 허용. XSS 주의 |