{"id":47720334,"url":"https://github.com/2davi/rest-domain-state-manager","last_synced_at":"2026-04-02T19:21:54.676Z","repository":{"id":345024262,"uuid":"1183935496","full_name":"2davi/rest-domain-state-manager","owner":"2davi","description":"Proxy-based REST state manager — auto-tracks field changes, smart-routes HTTP methods, and rolls back on failure. Zero dependencies. From JSP grids to React hooks.","archived":false,"fork":false,"pushed_at":"2026-04-02T08:03:12.000Z","size":10508,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-02T14:01:38.168Z","etag":null,"topics":["broadcastchannel","dirty-check","domain-driven","esm","idempotency","javascript","json-patch","jsp","optimistic-update","proxy","react-hooks","rest-api","rollback","si","spring-boot","state-management","vitepress","web-worker","zero-dependency"],"latest_commit_sha":null,"homepage":"https://lab.the2davi.dev/rest-domain-state-manager","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/2davi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-17T04:54:16.000Z","updated_at":"2026-04-02T08:23:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/2davi/rest-domain-state-manager","commit_stats":null,"previous_names":["2davi/rest-domain-state-manager"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/2davi/rest-domain-state-manager","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2davi%2Frest-domain-state-manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2davi%2Frest-domain-state-manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2davi%2Frest-domain-state-manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2davi%2Frest-domain-state-manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/2davi","download_url":"https://codeload.github.com/2davi/rest-domain-state-manager/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2davi%2Frest-domain-state-manager/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31314375,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["broadcastchannel","dirty-check","domain-driven","esm","idempotency","javascript","json-patch","jsp","optimistic-update","proxy","react-hooks","rest-api","rollback","si","spring-boot","state-management","vitepress","web-worker","zero-dependency"],"created_at":"2026-04-02T19:21:54.088Z","updated_at":"2026-04-02T19:21:54.665Z","avatar_url":"https://github.com/2davi.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @2davi/rest-domain-state-manager\n\n[![npm version](https://img.shields.io/npm/v/@2davi/rest-domain-state-manager)](https://www.npmjs.com/package/@2davi/rest-domain-state-manager)\n[![CI](https://github.com/2davi/rest-domain-state-manager/actions/workflows/ci.yml/badge.svg)](https://github.com/2davi/rest-domain-state-manager/actions/workflows/ci.yml)\n[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)\n\nREST API 리소스를 Proxy로 감싸, **필드 변경을 자동으로 추적**하고,\n**POST / PATCH / PUT을 스마트하게 분기**하는 zero-dependency 상태 관리 라이브러리.\n\n저장 실패 시 클라이언트 상태를 자동 복원하는 **보상 트랜잭션**까지 내장.\n\n---\n\n## 어떤 환경에서 쓰시나요?\n\n### JSP / 레거시 환경 → [SI 빠른 시작](#그리드-ui-바인딩--uicomposer--uilayout)\n\nSpring Boot + JSP + jQuery 환경에서 1:N 폼 그리드를 10줄로 만드세요.  \n`fnAddRow()`, `fnRemoveRow()`, `fnReindexRows()` — 전부 사라집니다.\n\n### React / Vue → [프레임워크 연동 빠른 시작](#react-연동--usedomainstate)\n\n`useDomainState()` 한 줄로 GET → 수정 → PATCH 사이클을 자동화하세요.  \n`fetch`, `useState`, `useEffect`, 롤백 로직 — 전부 사라집니다.\n\n---\n\n## 이 라이브러리가 해결하는 것\n\nREST API 프론트엔드 개발에서 반복되는 세 가지 문제를 해결합니다.\n\n### 1. \"어떤 필드가 바뀌었는지 모르겠다\"\n\n```javascript\n// ❌ Before — 모든 필드를 수동으로 모아야 한다\nconst payload = {\n    name:  document.getElementById('name').value,\n    email: document.getElementById('email').value,\n    phone: document.getElementById('phone').value,\n    // ...필드가 30개면 30줄\n};\nawait fetch('/api/users/1', { method: 'PUT', body: JSON.stringify(payload) });\n```\n\n```javascript\n// ✅ After — 변경한 필드만 자동으로 추적되고, 적절한 HTTP 메서드가 선택된다\nconst user = await api.get('/api/users/1');\nuser.data.name = 'Davi';                // ← 이 변경이 자동으로 기록된다\nawait user.save('/api/users/1');         // → PATCH [{ \"op\": \"replace\", \"path\": \"/name\", \"value\": \"Davi\" }]\n```\n\n### 2. \"저장 실패하면 화면이 꼬인다\"\n\n```javascript\n// ❌ Before — 실패 시 수동 복원 코드를 매번 작성\ntry {\n    await fetch('/api/users/1', { method: 'PATCH', body: ... });\n} catch {\n    // 이전 상태로 어떻게 되돌리지? UI에 반영된 값은?\n}\n```\n\n```javascript\n// ✅ After — 실패 시 save() 이전 상태로 자동 복원\ntry {\n    await user.save('/api/users/1');\n} catch (err) {\n    // user.data는 이미 save() 호출 이전 상태로 되돌아가 있다.\n    console.log(user.data.name); // → 변경 전 값\n}\n```\n\n### 3. \"1:N 그리드의 fnAddRow()가 끝없이 복사된다\"\n\n```javascript\n// ❌ Before — 화면마다 복사되는 보일러플레이트\nfunction fnAddRow() { /* N0 줄 */ }\nfunction fnRemoveRow() { /* 인덱스 밀림 버그 */ }\nfunction fnReindexRows() { /* N0 줄 */ }\nfunction fnSelectAll() { /* N0 줄 */ }\n```\n\n```javascript\n// ✅ After — HTML template 선언 + 컨트롤 함수 한 줄\nconst { addEmpty, removeChecked, validate } =\n    certs.bind('#certGrid', { layout: CertLayout });\n// addEmpty()      — 빈 행 추가\n// removeChecked() — 체크된 행 역순 제거 (인덱스 밀림 자동 방지)\n// validate()      — required 필드 검증\n```\n\n---\n\n## 설치\n\n```bash\nnpm install @2davi/rest-domain-state-manager\n```\n\nNode.js ≥ 20. 브라우저: Chrome 94+, Firefox 93+, Safari 15.4+.\n\n---\n\n## 빠른 시작 — 3분 안에 동작하는 코드\n\n### STEP 1. API 핸들러 생성\n\n```javascript\nimport { ApiHandler } from '@2davi/rest-domain-state-manager';\n\nconst api = new ApiHandler({ host: 'localhost:8080', debug: true });\n```\n\n### STEP 2. GET → 폼 바인딩 → 저장\n\n```javascript\nimport { DomainState, UIComposer, UILayout } from '@2davi/rest-domain-state-manager';\n\nDomainState.use(UIComposer); // 플러그인 설치 (앱 진입점에서 1회)\n\n// ── UI 계약 선언: 어떤 필드가 어떤 DOM 요소에 연결되는지 ──\nclass UserFormLayout extends UILayout {\n    static templateSelector = '#userFormTemplate';\n    static columns = {\n        name:  { selector: '[data-field=\"name\"]',  required: true },\n        email: { selector: '[data-field=\"email\"]' },\n        city:  { selector: '[data-field=\"city\"]' },\n    };\n}\n```\n\n```javascript\n// GET 응답이 자동으로 DomainState로 변환된다\nconst user = await api.get('/api/users/1');\n\n// 폼에 바인딩하면, 사용자가 입력하는 동안 Proxy를 통해 상태가 자동으로 변경된다.\n// 개발자가 user.data.name = '...' 같은 코드를 직접 작성할 필요가 없다.\nconst { unbind } = user.bindSingle('#userForm', { layout: UserFormLayout });\n\n// 사용자가 name 필드에 'Davi'를 입력하고, city 필드에 'Seoul'을 입력한 뒤 저장 버튼을 클릭하면:\nawait user.save('/api/users/1');\n// → PATCH [{ \"op\": \"replace\", \"path\": \"/name\", \"value\": \"Davi\" },\n//          { \"op\": \"replace\", \"path\": \"/city\", \"value\": \"Seoul\" }]\n// 사용자가 건드리지 않은 필드는 페이로드에 포함되지 않는다.\n```\n\n스크립트에서 직접 값을 넣는 것도 동일하게 동작합니다:\n\n```javascript\nuser.data.name = 'Davi';            // → changeLog에 replace 기록\nuser.data.address.city = 'Seoul';   // → 중첩 경로도 자동 추적\n```\n\n폼 바인딩이든 스크립트 대입이든, **Proxy의 `set` 트랩을 통과하는 모든 변경이 자동 기록**됩니다.\n\n### STEP 3. 신규 생성 (POST)\n\n```javascript\nimport { DomainState, DomainVO } from '@2davi/rest-domain-state-manager';\n\nclass UserVO extends DomainVO {\n    static fields = {\n        name:  { default: '', validate: v =\u003e v.trim().length \u003e 0 },\n        email: { default: '' },\n        age:   { default: 0, transform: Number },\n    };\n}\n\nconst newUser = DomainState.fromVO(new UserVO(), api);\nnewUser.data.name  = 'Davi';\nnewUser.data.email = 'davi@example.com';\nawait newUser.save('/api/users');  // → POST (isNew === true)\n```\n\n`DomainVO`는 선택적 레이어입니다. `DomainState.fromJSON()`은 VO 없이도 완전히 동작합니다.\n\n---\n\n## HTTP 메서드 자동 분기\n\n`save()`는 두 가지 내부 상태를 분석하여 HTTP 메서드를 자동 결정합니다.\n\n| 조건                                 | 메서드    | 근거                                   |\n| ------------------------------------ | --------- | -------------------------------------- |\n| `isNew === true`                     | **POST**  | 서버에 아직 존재하지 않는 신규 리소스  |\n| 변경 없음 (`dirtyFields.size === 0`) | **no-op** | save() 조기 종료                       |\n| 변경 비율 ≥ 70%                      | **PUT**   | 전체 교체가 JSON Patch 배열보다 효율적 |\n| 변경 비율 \u003c 70%                      | **PATCH** | RFC 6902 JSON Patch — 변경 부분만 전송 |\n\nPOST 성공 후 `isNew`가 `false`로 전환되어, 이후 저장은 PATCH 또는 PUT으로 분기합니다.\n\n---\n\n## 보상 트랜잭션 — 실패 시 자동 복원\n\n`save()` 진입 시 `structuredClone()`으로 현재 상태 4종(데이터, 변경이력, dirty 필드, isNew 플래그)을\n깊은 복사합니다. HTTP 요청이 실패하면 4종을 모두 save() 이전 시점으로 원자적 복원합니다.\n\n```javascript\nuser.data.name = 'Davi';           // 변경 기록됨\nawait user.save('/api/users/1');    // 서버 500 에러 발생!\n// → user.data.name은 자동으로 이전 값으로 복원됨\n// → changeLog, dirtyFields도 save() 진입 이전 상태로 복원됨\n// → 즉시 재시도 가능\n```\n\n`DomainPipeline`의 보상 트랜잭션과도 연계됩니다.\n`strict: false` 모드에서 후속 `save()` 실패를 감지한 뒤,\n이미 성공한 인스턴스에 `restore()`를 호출하여 전체 파이프라인의 일관성을 복구할 수 있습니다.\n\n---\n\n## 1:N 배열 관리 — DomainCollection\n\n```javascript\nimport { DomainCollection } from '@2davi/rest-domain-state-manager';\n\n// GET 응답 배열 → DomainCollection 변환\nconst certs = DomainCollection.fromJSONArray(\n    await fetch('/api/certificates').then(r =\u003e r.text()),\n    api\n);\n\ncerts.add({ certName: '정보처리기사', certType: 'IT' });  // 항목 추가\ncerts.remove(0);                                          // 인덱스로 제거\n\nawait certs.saveAll({\n    strategy: 'batch',           // 배열 전체를 단일 HTTP 요청으로 전송\n    path: '/api/certificates',\n});\n// → PUT (기존 배열 전체 교체) 또는 POST (신규 생성)\n```\n\n---\n\n## 그리드 UI 바인딩 — UIComposer + UILayout\n\nHTML `\u003ctemplate\u003e` 기반으로 DOM 구조를 선언하고, 라이브러리가 데이터를 채웁니다.\nJS에서 DOM 구조를 생성하지 않으므로, CSS 프레임워크(Bootstrap, Tailwind)와 충돌 없이 사용할 수 있습니다.\n\n### 1. HTML — `\u003ctemplate\u003e` 선언\n\n```html\n\u003ctemplate id=\"certRowTemplate\"\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cinput type=\"checkbox\" class=\"dsm-checkbox\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cspan class=\"dsm-row-number\"\u003e\u003c/span\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cinput type=\"text\" data-field=\"certName\" placeholder=\"자격증명\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\n      \u003cselect data-field=\"certType\"\u003e\n        \u003coption value=\"IT\"\u003eIT\u003c/option\u003e\n        \u003coption value=\"LANG\"\u003e어학\u003c/option\u003e\n      \u003c/select\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/template\u003e\n\n\u003ctable\u003e\n  \u003ctbody id=\"certGrid\"\u003e\u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003cbutton id=\"btnAdd\"\u003e행 추가\u003c/button\u003e\n\u003cbutton id=\"btnRemove\"\u003e선택 삭제\u003c/button\u003e\n\u003cbutton id=\"btnSave\"\u003e저장\u003c/button\u003e\n```\n\n### 2. JS — UILayout 선언 + 바인딩\n\n```javascript\nimport {\n    ApiHandler, DomainState, DomainCollection,\n    UIComposer, UILayout\n} from '@2davi/rest-domain-state-manager';\n\n// 플러그인 설치 (앱 진입점에서 1회)\nDomainState.use(UIComposer);\n\n// 화면별 UI 계약 선언\nclass CertLayout extends UILayout {\n    static templateSelector = '#certRowTemplate';\n    static itemKey          = 'certId';\n    static columns = {\n        certName: { selector: '[data-field=\"certName\"]', required: true },\n        certType: { selector: '[data-field=\"certType\"]' },\n    };\n}\n\nconst api   = new ApiHandler({ host: 'localhost:8080' });\nconst certs = DomainCollection.fromJSONArray(\n    // NOTE: 현재 ApiHandler.get() 메서드는 단일 객체 응답 중심으로 설계되어 있습니다 ^0^\n    // TODO: 빠른 업데이트를 통해 fetch 병행 없이 불러오도록 개선하겠습니다 ^~^;\n    await fetch('/api/certificates').then(r =\u003e r.text()),\n    api\n);\n\n// 바인딩 → 컨트롤 함수 반환\nconst { addEmpty, removeChecked, validate } =\n    certs.bind('#certGrid', { layout: CertLayout });\n\ndocument.getElementById('btnAdd').onclick    = addEmpty;\ndocument.getElementById('btnRemove').onclick = removeChecked;\ndocument.getElementById('btnSave').onclick   = async () =\u003e {\n    if (!validate()) return;\n    await certs.saveAll({ strategy: 'batch', path: '/api/certificates' });\n};\n```\n\n### 반환되는 컨트롤 함수\n\n| 함수                 | 역할                                                      |\n| -------------------- | --------------------------------------------------------- |\n| `addEmpty()`         | 빈 행 추가 (template 복제 + input 리스너 자동 등록)       |\n| `removeChecked()`    | 체크된 행 역순(LIFO) 제거 — 인덱스 밀림 자동 방지         |\n| `removeAll()`        | 전체 행 제거                                              |\n| `selectAll(checked)` | 전체 체크박스 일괄 설정                                   |\n| `invertSelection()`  | 체크 상태 반전                                            |\n| `validate()`         | `required: true` 필드 검증 + `is-invalid` CSS 클래스 토글 |\n| `getCheckedItems()`  | 체크된 DomainState 배열 반환                              |\n| `getItems()`         | 전체 DomainState 배열 반환                                |\n| `getCount()`         | 총 행 수 반환                                             |\n| `destroy()`          | 이벤트 리스너 정리                                        |\n\n---\n\n## React 연동 — useDomainState\n\n서브패스 import로 React 어댑터를 사용합니다. React 18+ `useSyncExternalStore` 기반입니다.\nReact가 `peerDependencies(optional)`로 선언되어 있어, React 없이 설치해도 경고가 뜨지 않습니다.\n\n```javascript\nimport { useDomainState } from '@2davi/rest-domain-state-manager/adapters/react';\n\nfunction UserProfile({ userState }) {\n    const data = useDomainState(userState);\n\n    return (\n        \u003cdiv\u003e\n            \u003cinput\n                value={data.name}\n                onChange={e =\u003e { userState.data.name = e.target.value; }}\n            /\u003e\n            \u003cbutton onClick={() =\u003e userState.save('/api/users/1')}\u003e\n                저장\n            \u003c/button\u003e\n        \u003c/div\u003e\n    );\n}\n```\n\n혹은,\n\n```javascript\nimport { ApiHandler, DomainState } from '@2davi/rest-domain-state-manager';\nimport { useDomainState } from '@2davi/rest-domain-state-manager/adapters/react';\n\nconst api = new ApiHandler({ host: 'localhost:8080' });\n\nfunction UserProfile({ userId }) {\n    const [state, setUserState] = useState(null);\n\n    useEffect(() =\u003e {\n        api.get(`/api/users/${userId}`).then(setUserState);\n    }, [userId]);\n\n    const data = useDomainState(state); // Shadow State — 변경 시 자동 리렌더링\n\n    if (!data) return \u003cdiv\u003e로딩 중...\u003c/div\u003e;\n\n    return (\n        \u003cform\u003e\n            \u003cinput\n                value={data.name}\n                onChange={e =\u003e { state.data.name = e.target.value; }}\n            /\u003e\n            \u003cbutton onClick={() =\u003e state.save(`/api/users/${userId}`)}\u003e\n                저장 (PATCH 자동 분기)\n            \u003c/button\u003e\n        \u003c/form\u003e\n    );\n}\n```\n\n`userState.data.name = '...'`로 Proxy를 변이하면:\n\n1. Microtask 배칭 완료 → Structural Sharing 기반 불변 스냅샷 재빌드\n2. 변경된 키만 새 참조, 나머지 키는 이전 참조 재사용\n3. React가 `getSnapshot()` 재호출 → `Object.is()` 비교 → 리렌더링\n\n변경이 없으면 동일 참조를 반환하여 **무한 리렌더링 루프가 발생하지 않습니다.**\n저장 실패 시 모든 상태가 `save()` 이전으로 자동 복원됩니다. `useState`로 에러 상태를 따로 관리할 필요가 없습니다.\n\n---\n\n## 병렬 fetch + 후처리 — DomainPipeline\n\n여러 API를 병렬로 요청하고, 응답 순서와 무관하게 후처리를 체이닝합니다.\n\n```javascript\nconst result = await DomainState.all({\n    roles: api.get('/api/roles'),\n    user:  api.get('/api/users/1'),\n}, { strict: false })\n.after('roles', async roles =\u003e {\n    // roles 응답으로 셀렉트박스 옵션 채우기\n})\n.after('user', async user =\u003e {\n    // user 응답으로 폼 데이터 채우기\n})\n.run();\n\n// 개별 실패는 result._errors에 기록 (strict: false)\nif (result._errors?.length) console.warn(result._errors);\n```\n\n---\n\n## Idempotency-Key — 네트워크 재시도 안전\n\n[IETF Idempotency-Key 표준 초안](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/)에 기반합니다.\n\n```javascript\nconst api = new ApiHandler({ host: 'api.example.com', idempotent: true });\n\ntry {\n    await user.save('/api/users/1');\n} catch {\n    // 네트워크 타임아웃 후 재시도 — 동일 UUID가 자동 재사용됨\n    await user.save('/api/users/1');\n    // 서버는 동일 Idempotency-Key를 감지하여 중복 처리 방지\n}\n```\n\n`save()` 성공 시 UUID 즉시 초기화. 실패 시 유지되어 재시도 안전.\n\n---\n\n## CSRF 보안\n\n[OWASP CSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) 준수.\n`POST`, `PUT`, `PATCH`, `DELETE` 요청에만 `X-CSRF-Token` 헤더를 삽입합니다.\n\n```javascript\nconst api = new ApiHandler({ host: 'localhost:8080' });\n\n// DOM이 준비된 시점에 1회 호출\napi.init();\n// → \u003cmeta name=\"csrf-token\" content=\"...\"\u003e 파싱\n// → 이후 모든 뮤테이션 요청에 X-CSRF-Token 헤더 자동 주입\n\n// 또는 쿠키에서 파싱\napi.init({ csrfCookieName: 'XSRF-TOKEN' });\n```\n\n`init()` 미호출 시 CSRF 기능은 비활성 상태이며, 뮤테이션 요청에 토큰이 삽입되지 않습니다.\n\n---\n\n## Lazy Tracking Mode — 최소 페이로드\n\n```javascript\nconst user = await api.get('/api/users/1', { trackingMode: 'lazy' });\n\nuser.data.name = 'A';\nuser.data.name = 'B';\nuser.data.name = 'C';  // 같은 필드를 3번 변경\n\nawait user.save('/api/users/1');\n// realtime 모드: PATCH에 3개 항목 (A, B, C 각각 기록)\n// lazy 모드:    PATCH에 1개 항목 (최종 결과 C만 전송)\n```\n\n`lazy` 모드에서는 Proxy `set` 트랩이 changeLog 기록을 건너뛰고,\n`save()` 호출 시 초기 스냅샷과 현재 상태를 LCS 알고리즘으로 deep diff하여\n**최종 변경 결과만** PATCH 페이로드에 포함합니다.\n\ndiff 연산은 브라우저 환경에서 Web Worker로 오프로딩되어 메인 스레드를 차단하지 않습니다.\n\n---\n\n## 디버거 — 멀티탭 실시간 상태 시각화\n\n```javascript\nconst api = new ApiHandler({ host: 'localhost:8080', debug: true });\nconst user = await api.get('/api/users/1');\n\nuser.openDebugger();  // 디버그 팝업 열기\n```\n\n`BroadcastChannel` 기반으로 동일 출처의 모든 탭에서 `DomainState`의 상태를 실시간으로 확인할 수 있습니다.\n탭이 닫히거나 응답이 없으면 Heartbeat GC가 자동으로 정리합니다.\n\n---\n\n## 주요 기능 요약\n\n| 기능                | 설명                                                                  |\n| ------------------- | --------------------------------------------------------------------- |\n| Proxy 자동 추적     | `set`, `delete`, 배열 변이(`push`, `splice`, `sort` 등) 전체 인터셉트 |\n| RFC 6902 JSON Patch | changeLog를 표준 JSON Patch 배열로 직렬화                             |\n| HTTP 메서드 분기    | `isNew` + `dirtyRatio` 기반 POST / PATCH / PUT 자동 결정              |\n| 보상 트랜잭션       | `structuredClone` 기반 4종 상태 원자적 롤백                           |\n| DomainCollection    | 1:N 배열 상태 + `saveAll({ strategy: 'batch' })`                      |\n| UIComposer          | HTML `\u003ctemplate\u003e` 기반 그리드/폼 바인딩 + 컨트롤 함수 반환            |\n| React 어댑터        | `useSyncExternalStore` 기반 `useDomainState()` 훅                     |\n| Idempotency-Key     | IETF Draft 기반 UUID 자동 발급/재사용                                 |\n| CSRF 인터셉터       | 3-상태 설계. `\u003cmeta\u003e` + 쿠키 파싱                                     |\n| Lazy Tracking       | LCS deep diff + Worker 오프로딩. 최종 변경만 전송                     |\n| Microtask 배칭      | `queueMicrotask` 스케줄러. 동기 블록 내 다중 변경 → 단일 flush        |\n| V8 최적화           | WeakMap Lazy Proxying + Reflect API + DomainVO Shape 고정             |\n| 플러그인 시스템     | `DomainState.use(plugin)` — 선택적 DOM 의존 기능 분리                 |\n| 멀티탭 디버거       | BroadcastChannel + Heartbeat GC + Worker 직렬화                       |\n| DomainPipeline      | 병렬 fetch + 순차 after() 체이닝 + 보상 트랜잭션 연계                 |\n| Zero Dependency     | 런타임 의존성 0. `sideEffects: false` Tree-shaking 허용               |\n\n---\n\n## API 구성\n\n```javascript\nimport {\n    ApiHandler,         // HTTP 전송 레이어 (인스턴스 생성은 소비자가 담당)\n    DomainState,        // 팩토리 3종 + save/remove + Shadow State + 플러그인\n    DomainVO,           // 선택적 — 신규 INSERT 스키마 선언 시\n    DomainCollection,   // 1:N 배열 상태 컨테이너 + saveAll\n    DomainPipeline,     // 병렬 fetch + 순차 after() 체이닝\n    UIComposer,         // HTML \u003ctemplate\u003e 기반 그리드/폼 바인딩 플러그인\n    UILayout,           // 화면별 UI 계약 선언 베이스 클래스\n    closeDebugChannel,  // 디버그 채널 명시적 종료 (SPA 전환 시)\n} from '@2davi/rest-domain-state-manager';\n\n// React 어댑터 (별도 서브패스)\nimport { useDomainState } from '@2davi/rest-domain-state-manager/adapters/react';\n```\n\n---\n\n## 문서\n\n전체 가이드, 아키텍처 심층 분석, 인터랙티브 플레이그라운드:\n**[lab.the2davi.dev/rest-domain-state-manager](https://lab.the2davi.dev/rest-domain-state-manager)**\n\n| 카테고리          | 페이지                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |\n| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Quick Start**   | [SI 빠른 시작](https://lab.the2davi.dev/rest-domain-state-manager/guide/si-quickstart) · [모던 빠른 시작](https://lab.the2davi.dev/rest-domain-state-manager/guide/modern-quickstart)                                                                                                                                                                                                                                                                                         |\n| **Guide**         | [DomainCollection](https://lab.the2davi.dev/rest-domain-state-manager/guide/domain-collection) · [UIComposer \u0026 UILayout](https://lab.the2davi.dev/rest-domain-state-manager/guide/ui-composer) · [Tracking Modes](https://lab.the2davi.dev/rest-domain-state-manager/guide/tracking-modes) · [Idempotency](https://lab.the2davi.dev/rest-domain-state-manager/guide/idempotency) · [save() 분기 전략](https://lab.the2davi.dev/rest-domain-state-manager/guide/save-strategy) |\n| **Architecture**  | [Proxy 엔진](https://lab.the2davi.dev/rest-domain-state-manager/architecture/proxy-engine) · [HTTP 라우팅](https://lab.the2davi.dev/rest-domain-state-manager/architecture/http-routing) · [V8 최적화](https://lab.the2davi.dev/rest-domain-state-manager/architecture/v8-optimization)                                                                                                                                                                                       |\n| **Playground**    | [인터랙티브 데모 11종](https://lab.the2davi.dev/rest-domain-state-manager/playground/)                                                                                                                                                                                                                                                                                                                                                                                        |\n| **API Reference** | [TypeDoc 자동 생성](https://lab.the2davi.dev/rest-domain-state-manager/api/rest-domain-state-manager)                                                                                                                                                                                                                                                                                                                                                                         |\n| **Decision Log**  | [ARD 4편 + IMPL 5편](https://lab.the2davi.dev/rest-domain-state-manager/decision-log/)                                                                                                                                                                                                                                                                                                                                                                                        |\n\n---\n\n## License\n\nISC © 2026 [2davi](https://github.com/2davi)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F2davi%2Frest-domain-state-manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F2davi%2Frest-domain-state-manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F2davi%2Frest-domain-state-manager/lists"}