마이그레이션 가이드: MVVM에서 Context-Layered로
이 문서는 기존 MVVM 또는 단순 React Context 구조에서 Context-Layered Architecture로 이동할 때의 기준점을 설명합니다. 핵심은 “한 번에 전부 바꾸는 것”이 아니라, 책임 분리 기준에 맞춰 점진적으로 재배치하는 것입니다.
가장 큰 차이
| 관점 | 기존 MVVM/단순 구조 | Context-Layered |
|---|---|---|
| 초점 | 개념적 계층 | 구현 가능한 책임 계층 |
| 비즈니스 로직 | ViewModel이나 컴포넌트에 섞임 | business와 handlers로 분리 |
| 의존성 주입 | context 또는 import에 의존 | props 기반 DI 가능 |
| 실행 흐름 | 컴포넌트 중심 | handler 중심 |
| 테스트 | 컴포넌트 통합 테스트 위주 | 레이어별 테스트 가능 |
이전 구조와 이후 구조
이전
src/components/checkout/
├── CheckoutModel.tsx
├── CheckoutViewModel.tsx
├── CheckoutView.tsx
└── CheckoutPerformance.tsx이후
src/pages/checkout/
├── contexts/
├── business/
├── handlers/
├── actions/
├── hooks/
├── views/
└── CheckoutPage.tsx현재 패키지 경계 migration
tool protocol은 framework-neutral 별도 package가 되었습니다. action schema나 MCP/provider adapter가 필요한 경우에만 설치합니다.
npm install @context-action/react @context-action/core @context-action/tool-protocol zoddefineAction, createActionSchema, listAllTools와 protocol type은 @context-action/tool-protocol에서 import합니다. 저장소에는 개발 트랙으로 packages/react/src/tools가 남아 있지만 React 4는 @context-action/react/tools를 의도적으로 export하지 않습니다. 별도 ToolContext 릴리스가 승인되기 전에는 설치된 패키지에서 이 경로를 import하지 마십시오. @context-action/react는 기본 action/store entry를, @context-action/core는 action runtime을 소유합니다. 기존 Core/React re-export는 제거되었습니다.
Durable mutation recovery는 별도의 선택적 package로 분리되었습니다. durable operation record, 프로세스 간 claim, HTTP/queue side-effect adapter가 필요한 애플리케이션만 @context-action/tool-durable-operations를 추가합니다. provider-neutral schema와 discovery만 사용하는 경우에는 필요하지 않습니다.
action context factory는 명시적인 context name을 요구합니다.
createActionContext<AppActions>('Checkout', { registry: { debug: true } });void action의 dispatch option은 payload와 혼동되지 않도록 두 번째 인자에 둡니다.
await register.actions.reset(undefined, { debounce: 100 });@context-action/react는 React 19.2 이상을 요구합니다. Store·Action API는 @context-action/react에서 import하며, 이전의 react18 호환 entry point는 더 이상 export하지 않습니다. @context-action/mutative-core / @context-action/mutative fork는 계속 immutable runtime 계약으로 유지합니다. 현재 stable cohort는 Core 1.2.6, Store Core 0.1.3, Mutative Core 0.8.14, scoped adapter 0.8.15, React 4.0.7입니다. mutative@1.3.0은 인수한 upstream 호환성 기준이며 runtime 교체 버전이 아닙니다. upstream 변경은 검토된 UPSTREAM.md와 upstream-lock.json에 기록하고 pnpm verify:mutative-upstream을 실행합니다.
State 계약 마이그레이션
새 state 코드는 Store and time-travel contracts의 role interface를 사용합니다. Declarative TimeTravelStore manager는 기본 readMode: 'safe'이고 direct createTimeTravelStore() factory는 호환성을 위해 readMode: 'reference'를 유지합니다. 외부 경계에는 getSafeValue()를 사용합니다. 하나의 action이 여러 Store를 바꾸면 StoreTransactionCoordinator에 participant를 명시적으로 등록하고 Core dispatch trace를 bindActionTransactions()로 연결합니다. action 이름이나 handler 등록에서 ownership을 추론하지 않습니다.
권장 마이그레이션 순서
1. Context 정의부터 분리
먼저 액션과 스토어 타입, provider 생성 코드를 contexts/로 옮깁니다. 이 단계에서는 동작 변경보다 “경계 분리”가 목적입니다.
2. 순수 로직을 business/로 이동
다음 항목을 순수 함수로 떼어냅니다.
- validation
- 계산 규칙
- 상태 전이 규칙
- 도메인별 파생 값 계산
이 단계가 끝나면 UI와 handler에서 계산 코드가 눈에 띄게 줄어듭니다.
3. side effect를 handlers/로 이동
API 호출, store 업데이트, ref focus, 로그 기록 같은 실행 흐름은 handler에 둡니다.
useCheckoutHandler('submit', async (payload) => {
const current = checkoutStore.getValue();
const result = createOrderDraft(payload, current);
if (result.success) {
checkoutStore.setValue(result.nextState);
await apiClient.save(result.order);
}
});4. view에서 직접 dispatch 코드를 actions/로 감싸기
export function useCheckoutActions() {
const dispatch = useCheckoutDispatch();
return {
submit: (order: OrderData) => dispatch('submit', order),
reset: () => dispatch('reset'),
};
}이렇게 하면 view가 action 이름과 payload 모양에 덜 직접적으로 결합됩니다.
5. store 직접 접근을 hooks/로 정리
view가 여러 store를 직접 읽기 시작하면 다시 복잡도가 올라갑니다. view용 데이터 접근은 hooks/에 모으는 것이 좋습니다.
6. 최종적으로 Page에서 조립
provider 구성, Handler Registry 마운트, 외부 의존성 주입은 페이지에서 담당하게 정리합니다. 실제 handler hook 호출은 Registry 내부에 둡니다. Ref 경계를 사용하는 경우의 표준 순서는 Action Provider → Store Provider → Ref Provider → Handler Registry → View이며, Ref를 사용하지 않으면 Registry가 Store Provider 바로 아래에 옵니다.
마이그레이션 체크리스트
- context 생성 코드가
contexts/에 모였는가 - 검증과 계산 로직이
business/로 분리되었는가 - API 호출과 store 업데이트가
handlers/에 모였는가 - view는 hook과 action만 사용하도록 단순화되었는가
- 테스트가 컴포넌트 하나에 몰리지 않고 레이어별로 분산되었는가
흔한 실수
views/에서 store를 직접 update하기handlers/안에 검증 규칙을 길게 쓰기actions/없이 view에서 dispatch 이름을 직접 남발하기contexts/에 runtime logic을 넣기