-
Notifications
You must be signed in to change notification settings - Fork 0
JSON Document
json-document는 schema를 가진 JSON state를 제품 안에서 안전하게 읽고, 바꾸고, 선택하고, 되돌릴 수 있게 만드는 headless document layer다.
Form, CMS, outliner, visual editor, review tool, automation UI처럼 JSON을 중심 상태로 쓰는 제품은 많다. 하지만 실제 제품이 커지면 단순한 setState나 ad-hoc object mutation만으로는 부족해진다. 사용자는 항목을 선택하고, 복사하고, 붙여넣고, 여러 값을 한 번에 바꾸고, undo/redo를 기대한다. 제품은 schema를 지켜야 하고, 실패 이유를 설명해야 하며, patch 이후 selection이나 comment anchor 같은 주변 상태도 따라가야 한다.
json-document는 그 반복되는 core를 하나의 안정적인 API로 제공한다.
JSON 기반 제품에서 어려운 부분은 값을 바꾸는 코드 자체보다, 그 변경을 제품 품질로 끌어올리는 일이다.
예를 들어 title 하나를 바꾸는 것은 쉽다.
state.title = "Ready";하지만 제품에서는 곧바로 더 많은 질문이 따라온다.
- 이 값이 schema를 통과하는가?
- 실패하면 사용자는 어떤 이유를 볼 수 있는가?
- 이 변경은 undo stack에 어떤 단위로 들어가는가?
- 같은 command를 실행하기 전에 버튼을 비활성화할 수 있는가?
- 선택된 항목을 지우거나 옮길 때 selection은 어떻게 해석되는가?
- 복사한 여러 array item을 붙여넣을 때 기본 동작은 무엇인가?
- patch 이후 기존 pointer, annotation, comment anchor는 어디를 가리키는가?
- React가 아닌 환경에서도 같은 document logic을 쓸 수 있는가?
json-document는 이 질문들을 제품마다 다시 만들지 않기 위해 존재한다.
핵심 의도는 JSON을 단순 data blob이 아니라 편집 가능한 document로 다루는 것이다. Document는 현재 값만 갖지 않는다. 읽기 API, command API, preflight, selection, clipboard, history, schema query, pointer helper가 함께 있어야 실제 제품 workflow를 안정적으로 만들 수 있다.
Document는 Zod schema와 초기값으로 만든다.
const doc = createJSONDocument(Card, {
id: "card-1",
title: "Draft",
done: false,
}, {
history: 100,
selection: true,
});제품이 다루는 값은 schema를 통과한 state다. 외부 입력은 parse 경계를 통과하고, 이미 검증된 snapshot은 trustedInitial: true로 비용을 줄일 수 있다.
doc.value로 현재 state를 읽고, JSON Pointer로 특정 path를 읽는다.
doc.value;
doc.at("/title");
doc.entries("/items");검색은 JSONPath로 하고, 결과는 JSON Pointer로 받는다.
const found = doc.find("$..cards[?(@.done==false)]");JSONPath는 찾기 위한 언어이고, JSON Pointer는 바꾸기 위한 주소다. 이 구분 덕분에 검색, 편집, selection, schema query, patch tracking이 같은 address model을 공유한다.
제품 UI는 보통 command를 실행하기 전에 가능 여부를 알고 싶다.
const can = doc.canReplace("/title", "Ready");
if (!can.ok) {
can.code;
can.reason;
can.violations;
}can* API는 boolean만 주지 않는다. 실패 code, pointer, schema violation을 포함해 button disabled, command palette, menu availability, validation message를 만들 수 있게 한다.
가장 자주 쓰는 편집 command는 제품 행동에 가깝다.
doc.insert("/items/-", item);
doc.replace("/title", "Ready");
doc.delete("/items/0");
doc.move("/items/0", "/items/2");
doc.duplicate("/items/0");Selection을 켠 document에서는 source나 target을 생략하고 현재 selection을 기본값으로 쓸 수 있다.
doc.replace("Ready");
doc.delete();
doc.move("/items/2");
doc.copy();성공한 편집은 schema 검증을 통과한 뒤 commit된다. 실패하면 state, selection, clipboard, history가 부분적으로 바뀌지 않는다.
Selection은 DOM focus가 아니다. JSON document 위의 headless address state다.
doc.selection?.selectRanges(["/items/0", "/items/1"]);
doc.copy(doc.selection?.selectedSource ?? []);이 모델 덕분에 같은 document를 list, table, outline, form, custom canvas처럼 다르게 렌더링해도 selection 의미는 pointer와 range로 유지된다.
Selection은 optional이다. 단순 transform이나 server-side pipeline은 selection 없이 쓸 수 있고, editor 제품은 selection을 켜서 copy, cut, delete, move, duplicate의 기본 source로 사용할 수 있다.
Clipboard는 browser clipboard가 아니라 document instance 안의 headless buffer다.
doc.copy(["/items/0", "/items/1"]);
doc.paste("/items/-");History는 사용자 의도 단위로 동작한다.
doc.history.transaction({ label: "bulk edit" }, () => {
doc.replace("/title", "Ready");
doc.replace("/done", true);
});
doc.undo();
doc.redo();이 둘은 UI adapter와 분리되어 있다. Browser clipboard, native clipboard, remote persistence, analytics는 제품 쪽에서 연결하고, core는 document 안의 일관된 command semantics를 제공한다.
제품이 커지면 document 밖에도 따라가야 하는 상태가 생긴다.
- comment anchor
- review marker
- external id mapping
- remote cursor
- annotation
- selected sibling item range
json-document는 이런 adapter가 의존할 수 있는 low-level helper도 제공한다.
trackPointer("/items/0", doc.lastPatch);
resolveSiblingRange(["/items/0", "/items/1"]);
buildPointer(["items", 0, "title"]);
applyPatch(Schema, state, operations);제품 UI는 JSONDocument<T> command를 먼저 쓰고, adapter나 extension package는 필요할 때 low-level helper로 내려간다.
json-document는 UI component library가 아니다. DOM rendering, design system, persistence, collaboration transport, server sync를 직접 소유하지 않는다.
대신 어떤 UI framework에서도 같은 document behavior를 재사용할 수 있도록 core를 headless로 유지한다. React adapter는 별도 entrypoint인 @interactive-os/json-document/react에서 제공된다.
사용자 입력, pointer, JSONPath, paste payload, selection 상태는 정상적인 제품 흐름에서 실패할 수 있다. 그래서 대부분의 실패는 throw가 아니라 result로 돌아온다.
if (!result.ok) {
result.code;
}앱은 reason 문자열이 아니라 code로 분기한다. reason은 logging, debug UI, 개발자 진단을 위한 설명이다.
편집 command는 중간 상태를 남기지 않는다. Patch 일부만 적용되고 schema 검증에서 실패하는 상태, selection만 바뀌고 value는 그대로인 상태, clipboard는 바뀌었지만 history가 누락된 상태를 만들지 않는 것이 core의 중요한 책임이다.
@interactive-os/json-document core 공개 API는 1.0부터 안정화된 공개 표면이다.
1.x에서는 기존 이름, result 구조, 에러 코드, command 의미론, 원자성, selection 의미론, clipboard 기본값, strict 동작의 하위 호환성을 유지한다.
새 API는 추가될 수 있다. 하지만 기존 제품이 import path, method 이름, result branch, error code, selection behavior, clipboard behavior를 바꾸지 않아도 계속 동작해야 한다.
이 보장은 export 이름만의 lock이 아니다. code 기반 실패 분기, strict: false
기본값, 실패 atomicity, selectionAfter와 undo/redo selection 복구, clipboard
spread 기본값은 1.0 Semantic Contract로 별도 고정한다.
json-document는 완성된 editor가 아니다.
json-document는 form renderer가 아니다.
json-document는 persistence layer가 아니다.
json-document는 realtime collaboration protocol이 아니다.
이 package는 그 아래에서 여러 제품과 adapter가 공유할 수 있는 document core다. 제품은 이 core 위에 자신만의 UI, 저장 방식, 권한 모델, collaboration layer, domain workflow를 얹는다.
문서는 위에서 아래로 제품 개발자에게 가까운 API에서 adapter와 low-level primitive에 가까운 API 순서로 배치한다.
- json-document Core API Reference
- 1.0 Semantic Contract
- JSONDocument Method Reference
- Selection Method Reference
- Labs and Extensions
- Low-level Function Reference
처음 읽는다면 Core API Reference에서 전체 그림을 잡고, 실제 제품 코드가 doc.* method에 의존하는 시점에 JSONDocument Method Reference로 내려가면 된다. Selection을 켜는 editor라면 그 다음 Selection Method Reference를 읽는다. 이미 만들어진 extension을 찾는다면 Labs and Extensions를 보고, adapter나 extension package를 직접 만들 때는 마지막으로 Low-level Function Reference를 보면 된다.