정의
글을 "보기만" 하면 되는 화면(미리보기 카드, 블로그 임베드 등)에서, 편집기용 무거운 라이브러리를 아예 안 내려받게 만들고 싶을 때 쓰는 패턴이다. 편집 기능은 필요 없는데도 편집기 코드가 딸려 오면 그 화면이 느려지기 때문이다.
구체적으로, 문서 패키지에서 **편집용 진입점(editor entry)**과 **읽기용 진입점(viewer entry)**을 나눠 내보낸다(진입점 = 소비하는 앱이 import하는 시작 파일). 편집용에는 리치텍스트 편집 엔진(예: ProseMirror), 드래그앤드롭 라이브러리(dnd-kit), 툴바 같은 편집 UI(chrome)가 들어가고, 읽기용에는 그게 빠진다. 이 분리를 export 목록·CSS·import graph(어떤 파일이 어떤 파일을 import하는지의 의존 그래프) 세 수준 모두에서 해야 한다.
왜 필요한가
패키지 하나가 모든 걸 한 입구로 내보내면(barrel export — 여러 모듈을 index에서 한꺼번에 re-export하는 방식), 미리보기 카드·블로그 임베드·SSR 프리뷰까지 편집기 전체를 끌고 온다. 화면에 보이는 DOM은 읽기 전용 편집기와 똑같아도, import graph가 실제로 달라야 tree-shaking(안 쓰는 코드를 번들에서 자동으로 걷어내는 최적화)이 먹혀서 편집기 코드가 빠진다.
동작 원리
@pkg/viewersubpath +viewer.css(chrome·toolbar layer 제외) — 소비자는 viewer-only 앱에서 editor entry를 import하지 않는다.- viewer module graph에서 PM/dnd import 0건을 lint 또는 dependency-cruiser로 CI 검증한다 — dev HMR tree-shake 착시를 막기 위해 production analyze가 필수다.
- readOnly prop으로 BlockRenderer를 공유한다 — DOM은 editor와 같고 import graph만 달라야 peek·gallery·embed LCP/TTI가 내려간다.
- publish manifest
exports/files에 CSS entry를 포함한다 — 누락 시 npm install 후 unstyled viewer가 배포된다.
실무 적용
- 번들 size budget CI (viewer gzip 상한).
- Storybook: ViewerOnly vs Editable side-by-side.
- peek·gallery·embed는 viewer entry만 import (lint rule).
트레이드오프
- dual entry 유지 비용 vs peek LCP/TTI 이득.
- token은 공유, chrome만 제외 — visual parity 테스트 필요.
사용하면 안 되는 case
- 패키지가 항상 full editor만 노출하면 split 불필요.
흔한 실수
- dev HMR에서 tree-shake 착시 → production analyze 필수.
- npm publish에 CSS 누락 → 소비자 unstyled viewer.
관련 개념
- [[webpack-splitchunks-vendor-shared-chunk]]
- [[css-cascade-layers-theming]]