에디터 / 프리뷰 분기

에디터 / 프리뷰 분기

이 사례의 핵심은 “에디터와 프리뷰를 비슷하게 맞추자"가 아니라
둘이 같은 schema와 renderer contract를 공유하지 않으면 아예 성립할 수 없게 만드는 것이다.

개요

콘텐츠 renderer까지 통제해도
UI는 여전히 구조를 무너뜨릴 수 있다.

그 이유는 같은 문서를 다루는 두 표면이
서로 다른 해석 경로를 가질 수 있기 때문이다.

대표적으로 다음과 같은 상황이다.

  • 에디터는 자체 노드 구조를 사용
  • 프리뷰는 별도 markdown renderer를 사용
  • export는 또 다른 HTML 템플릿을 사용
  • 특정 블록은 에디터에서는 보이지만 프리뷰에서는 깨짐

즉 문제는 단순한 표시 차이가 아니라
동일한 문서가 표면마다 다른 구조로 해석되는 것이다.

Glif에서는 다음을 목표로 한다.

  • editor, preview, export는 동일한 schema contract를 공유한다
  • renderer surface는 표면별로 달라도 node 의미는 동일하다
  • 위반은 코드 리뷰가 아니라 빌드, 테스트, 런타임에서 실패한다

이 문서의 질문

이 문서는 하네스 개요에서 정리한 다섯 공통 패턴 중 parallel path divergence에 가장 가깝다. 핵심 질문은 다음과 같다.

같은 문서가 editor, preview, export에서 같은 schema와 의미를 유지하는가

문제

다음과 같은 구조가 반복적으로 생긴다.

Editor AST
→ editor 전용 serializer
→ preview 전용 parser
→ preview 전용 renderer

또는

Markdown source
→ editor plugin 해석
→ preview regex 해석
→ export template 해석

이 방식은 초기에 개발이 빠르다.
하지만 구조적으로는 다음 문제를 만든다.

  • 같은 문서가 화면마다 다르게 보임

  • 특정 블록이 저장 후 사라지거나 변형됨

  • editor에서 허용된 구조가 preview에서는 unknown node가 됨

  • export 결과가 preview와 다름

  • AI가 표면마다 따로 맞추는 ad-hoc 코드를 계속 재생산함

즉 문제는 renderer가 여러 개라는 점이 아니라
의미 해석의 기준점이 하나가 아니라는 것이다.

문제의 본질

사람과 AI는 editor와 preview를
하나의 문서 시스템이 아니라 별도 UI 문제로 나누어 다루는 경향이 있다.

기준은 대체로 단순하다.

  • editor는 editor가 보이면 된다

  • preview는 preview가 보이면 된다

  • export는 결과만 나오면 된다

  • 공통 schema를 맞추는 비용은 생략할 수 있다

그래서 다음과 같은 우회가 쉽게 발생한다.

  • editor plugin 내부 임시 node type 추가

  • preview에서만 쓰는 별도 HTML 변환기 작성

  • export에서만 쓰는 template field 추가

  • 한 표면에서 깨진 것을 다른 표면에서 ad-hoc fallback으로 봉합

즉 사람과 AI는 공통 의미 계층을 보존하기보다
표면별로 따로 맞추는 짧은 경로를 만든다.

목표 구조

Glif에서는 문서 해석 경로를 다음처럼 고정한다.

D2 diagram
핵심 규칙은 단순하다.
  • 의미는 schema가 정의한다

  • editor, preview, export는 schema를 소비한다

  • 특정 표면만 이해하는 node 의미는 허용되지 않는다

하네스 적용 위치

이 문제는 네 레벨에서 동시에 막는다.

구조 레벨

공유 schema 패키지를 기준점으로 둔다.

  • doc-schema

  • editor-adapter

  • preview-renderer

  • export-renderer

각 표면은 schema를 참조할 수 있지만
서로의 내부 구현을 직접 참조하지 않는다.

즉 editor와 preview는 서로를 맞추는 것이 아니라
같은 schema에 맞춰지는 구조가 된다.

검증 레벨

정적 분석으로 표면 간 우회 참조를 막는다.

  • preview가 editor 내부 node type 직접 참조 금지

  • export가 preview helper 직접 참조 금지

  • surface-specific transform import 금지

  • schema 밖 node literal 추가 금지

실행 레벨

런타임에서는 shared schema 위반을 즉시 실패시킨다.

  • unknown node type throw

  • editor adapter가 schema 불일치 노드 생성 시 실패

  • preview/export renderer가 지원하지 않는 node 발견 시 실패

테스트 레벨

같은 fixture를 editor, preview, export에 동시에 통과시킨다.

  • 하나의 doc fixture

  • 하나의 schema expectation

  • 세 표면의 round-trip 검증

즉 테스트도 각 표면이 따로 맞는지가 아니라
같은 문서를 같은 의미로 해석하는지를 검증해야 한다.

실제 구현

editor, preview, export는 같은 schema를 공통 기준으로 써야 한다

이 케이스의 핵심은 표면이 비슷해 보이느냐가 아니라, 같은 문서를 같은 의미 구조로 통과시키느냐이다.

export type DocNode =
  | { type: "paragraph"; children: InlineNode[] }
  | { type: "heading"; level: 1 | 2 | 3; children: InlineNode[] }
  | { type: "blockquote"; children: DocNode[] }
  | { type: "code_block"; language: string | null; value: string }
  | { type: "callout"; tone: "info" | "warn"; children: DocNode[] };

editor 전용 node union, preview 전용 node union을 따로 두기 시작하면 divergence는 빨라진다.

표면 내부 상태와 문서 의미를 분리한다

export interface EditorAdapter {
  fromDoc(nodes: DocNode[]): unknown;
  toDoc(editorState: unknown): DocNode[];
}

export interface PreviewRenderer {
  render(nodes: DocNode[]): React.ReactElement;
}

export interface ExportRenderer {
  render(nodes: DocNode[]): string;
}

각 표면은 내부 상태를 가질 수 있지만, 경계 밖으로 나올 때는 반드시 DocNode[]로 합의해야 한다.

표면 간 직접 참조를 끊는다

preview가 editor internals를 직접 import하거나, export가 preview 전용 타입을 아는 순간 shared schema는 이름만 남는다. 그래서 표면 간 직접 참조를 lint로 막고, 공통 의미는 shared package로만 통과시키는 편이 맞다.

unknown node는 조용히 넘기지 않는다

export function assertKnownNode(node: DocNode): DocNode {
  switch (node.type) {
    case "paragraph":
    case "heading":
    case "blockquote":
    case "code_block":
    case "callout":
      return node;
    default: {
      const neverNode: never = node;
      throw new Error(`Unknown node type: ${(neverNode as any).type}`);
    }
  }
}

preview용 기본 div나 export용 빈 문자열 같은 fallback은 divergence를 조용히 누적시킨다.

테스트도 표면별이 아니라 문서별 fixture를 공유한다

const docFixture: DocNode[] = [
  { type: "heading", level: 2, children: [{ type: "text", value: "Hello" }] },
  { type: "callout", tone: "info", children: [{ type: "paragraph", children: [{ type: "text", value: "World" }] }] },
];

round-trip, preview, export가 모두 같은 fixture를 처리해야 새 node 추가나 의미 변경이 표면 전체에 동시에 반영된다.

위반 예시와 실패 결과

다음 코드는 반드시 실패해야 한다.

type EditorOnlyNode = {
  type: "selection_marker";
  from: number;
  to: number;
};

type MixedDocNode = DocNode | EditorOnlyNode;

실패 결과:

error: Surface-specific nodes must not be added to shared document schema.

또는 preview가 editor 내부를 직접 참조:

import { editorNodeToHtml } from "@/editor/internal/render";

실패 결과:

error: Preview must not depend on editor internals. Use shared schema.

또는 shared fixture를 export renderer가 처리하지 못할 때:

Error: Unknown node type: callout

이 순간부터 editor와 preview의 일치성은 문서가 아니라
실행 가능한 제약이 된다.

보조 강제 수단

이 케이스의 보조 강제 수단은 shared meaning source를 세 표면보다 더 강한 기준으로 두는 데 있다. shared schema만 두면 표면별 direct import가 남고, interface만 두면 unknown node drift가 남고, round-trip test가 없으면 divergence가 천천히 누적된다.

  • shared schema package → 의미 기준점 고정
  • editor adapter / preview renderer / export renderer interface → 표면 역할 분리
  • import boundary lint → 표면 간 직접 참조 차단
  • unknown node runtime guard → 불일치 즉시 실패
  • shared fixture round-trip tests → 세 표면 동기화 강제

핵심은 editor, preview, export를 각각 잘 만드는 것이 아니라 같은 문서를 같은 의미로 통과시키는 것이다.

결과

하네스 적용 이후 변화는 명확하다.

  • editor, preview, export가 같은 schema를 기준으로 움직인다.
  • 표면 전용 상태는 adapter 계층에 머물고 문서 의미 계층으로 새지 않는다.
  • 해석 불일치가 생겨도 어느 경로에서 갈라졌는지 추적하기 쉬워진다.

실무 적용 팁

editor와 preview는 같은 모양이 아니라 같은 의미를 공유해야 한다

표면 구현이 달라도
문서 의미 계층이 다르면 결국 분기한다.

표면 전용 상태를 schema에 올리지 않는다

cursor, selection, hover, decoration 같은 값은
문서 의미가 아니라 표면 상태다.

fallback으로 불일치를 봉합하지 않는다

한 표면에서 모르는 node를 조용히 넘기기 시작하면
divergence는 눈에 안 띄게 누적된다.

fixture는 표면별이 아니라 문서별로 관리한다

그래야 새 node가 추가될 때
세 표면이 동시에 검사된다.

요약

이 케이스에서 하네스는 다음과 같이 구성된다.

  • editor, preview, export는 shared schema를 공통 기준으로 사용한다

  • 표면 내부 상태와 문서 의미를 분리한다

  • 표면 간 직접 내부 참조는 정적 분석으로 차단된다

  • unknown node와 schema 불일치는 런타임에서 즉시 실패한다

  • shared fixture와 round-trip 테스트로 세 표면의 동기화를 강제한다

결과적으로

  • 동일 문서는 동일한 의미 구조로 해석되고

  • 표면별 ad-hoc 해석이라는 우회 경로는 제거되며

  • editor, preview, export는 같은 문서 시스템 위에서만 동작하게 된다

“에디터와 프리뷰를 최대한 비슷하게 맞추자"가 아니라
“같은 schema를 공유하지 않으면 둘은 아예 같은 문서를 다룰 수 없다"가 하네스다.