33 KiB
상태 관리 - 폼 자동 바인딩 및 setState
메인 문서: state-management.md 관련 문서: state-management-advanced.md | data-binding.md
목차
- 레이아웃 레벨 상태 초기화
- 폼 자동 바인딩 (FormContext)
- 폼 변경 감지 (trackChanges/hasChanges)
- setState 액션
- 깊은 병합 (Deep Merge)
- payload 표현식 평가
레이아웃 레벨 상태 초기화
버전: engine-v1.11.0+
레이아웃 JSON 최상위에서 initLocal, initGlobal, initIsolated로 정적 초기값을 설정할 수 있습니다.
initLocal (로컬 상태 초기값)
레이아웃 JSON 최상위에 initLocal 블록을 정의하여 _local 초기값을 설정합니다:
{
"version": "1.0.0",
"layout_name": "admin_board_form",
"initLocal": {
"activeTab": "basic",
"hasChanges": false,
"isSaving": false,
"formData": {
"name": "",
"slug": "",
"type": "basic",
"categories": [],
"show_view_count": false,
"use_comment": false
}
},
"body": { ... }
}
| 특징 | 설명 |
|---|---|
| ✅ 즉시 사용 가능 | 레이아웃 로드 시 바로 초기값 적용 |
| ✅ 타입 힌트 | boolean 필드에 false 지정 시 Toggle 자동 바인딩 작동 |
| 정적 값만 | 동적 표현식({{}}) 미지원 |
하위 호환: 기존
state속성도initLocal과 동일하게 동작합니다 (deprecated).
initGlobal (전역 상태 초기값)
레이아웃 JSON 최상위에 initGlobal 블록을 정의하여 _global 초기값을 설정합니다:
{
"initGlobal": {
"sidebarOpen": true,
"theme": "light"
}
}
initIsolated (격리 상태 초기값)
레이아웃 JSON 최상위에 initIsolated 블록을 정의하여 _isolated 초기값을 설정합니다:
{
"initIsolated": {
"selectedItems": [],
"filterOptions": {
"status": "all"
}
}
}
실행 순서 및 병합 동작
1. 레이아웃 initLocal/initGlobal/initIsolated (정적 기본값)
2. 데이터소스 initLocal/initGlobal/initIsolated (API 응답으로 깊은 병합)
3. initActions (동적 초기화)
병합 방식: 데이터소스의 initLocal/initGlobal/initIsolated가 적용될 때 깊은 병합됩니다:
- 레이아웃 레벨 초기값은 기본값으로 유지
- 데이터소스 응답에서 추가되는 값만 병합/덮어쓰기
// 레이아웃 initLocal (기본값)
{
"initLocal": {
"formData": {
"name": "",
"type": "basic",
"category": "default" // 기본값
}
}
}
// 데이터소스 응답 (API)
{
"name": "Product A",
"type": "premium"
// category 없음
}
// 결과 (_local.formData)
{
"name": "Product A", // API 값
"type": "premium", // API 값
"category": "default" // 기본값 유지
}
데이터소스 initLocal (API 응답 바인딩)
data_sources의 initLocal 옵션으로 API 응답을 로컬 상태에 자동 바인딩합니다:
{
"state": {
"formData": {
"name": "",
"slug": ""
}
},
"data_sources": [
{
"id": "board",
"type": "api",
"endpoint": "/api/boards/{{route?.id}}",
"method": "GET",
"auto_fetch": "{{!!route?.id}}",
"condition": "{{!!route?.id}}",
"initLocal": "formData"
}
]
}
| 옵션 | 설명 |
|---|---|
initLocal |
API 응답 데이터를 저장할 _local 경로 (예: "formData" → _local.formData) |
auto_fetch |
자동 fetch 조건 (편집 모드에서만 fetch) |
condition |
API 호출 조건 |
동작 순서:
1. 레이아웃 로드 → state.formData 초기값 적용
2. route?.id 존재 → API 자동 호출
3. API 응답 수신 → _local.formData에 응답 데이터 덮어쓰기
4. 컴포넌트 리렌더링 → 폼 필드에 API 데이터 표시
두 방법 함께 사용 (권장)
{
"state": {
"formData": {
"name": "",
"type": "basic",
"use_comment": false
}
},
"data_sources": [
{
"id": "board",
"endpoint": "/api/boards/{{route?.id}}",
"auto_fetch": "{{!!route?.id}}",
"initLocal": "formData"
}
]
}
- 생성 모드 (
route?.id없음):state.formData초기값 사용 - 편집 모드 (
route?.id존재): API 응답이formData를 덮어씀
폼 자동 바인딩 (FormContext)
버전: engine-v1.2.0+
폼 필드의 value와 onChange를 자동으로 바인딩하는 시스템입니다. dataKey prop을 사용하여 verbose한 actions 블록 없이 간결하게 폼을 구성할 수 있습니다.
핵심 원칙
✅ dataKey로 폼 데이터 경로 지정: 자식 컴포넌트들이 해당 경로에 자동 바인딩
✅ name prop으로 필드 식별: Input, Textarea, Select의 name이 필드명으로 사용
✅ trackChanges로 변경 추적: hasChanges가 자동으로 true로 설정됨
✅ 명시적 value 우선: value가 지정되어 있으면 자동 바인딩 비활성화
기존 방식 (verbose)
{
"name": "Input",
"props": {
"name": "title",
"value": "{{_local.formData.title}}"
},
"actions": [
{
"type": "change",
"handler": "setState",
"params": {
"target": "local",
"formData": {
"...": "{{_local.formData}}",
"title": "{{event.target.value}}"
},
"hasChanges": true
}
}
]
}
새로운 방식 (자동 바인딩)
로컬 상태 바인딩 (기본)
{
"name": "Div",
"props": {
"dataKey": "formData",
"trackChanges": true
},
"children": [
{
"name": "Input",
"props": { "name": "title" }
},
{
"name": "Textarea",
"props": { "name": "content" }
},
{
"name": "Select",
"props": {
"name": "category",
"options": "{{categories}}"
}
}
]
}
전역 상태 바인딩 (_global. 접두사)
버전: engine-v1.3.0+
전역 상태에 폼 데이터를 바인딩해야 하는 경우 _global. 접두사를 사용합니다.
기본 사용법
{
"name": "Div",
"props": {
"dataKey": "_global.formData",
"trackChanges": true
},
"children": [
{
"name": "Input",
"props": { "name": "title" }
},
{
"name": "Toggle",
"props": { "name": "isActive" }
}
]
}
필수: init_actions로 초기값 설정
dataKey="_global.xxx" 패턴을 사용하려면 반드시 init_actions에서 전역 상태를 초기화해야 합니다:
{
"version": "1.0.0",
"layout_name": "login_page",
"init_actions": [
{
"handler": "setState",
"params": {
"target": "global",
"loginForm": {
"email": "",
"password": ""
}
}
}
],
"components": [
{
"type": "basic",
"name": "Form",
"dataKey": "_global.loginForm",
"children": [
{
"type": "basic",
"name": "Input",
"props": { "name": "email", "type": "email" }
},
{
"type": "basic",
"name": "Input",
"props": { "name": "password", "type": "password" }
}
],
"actions": [
{
"type": "submit",
"handler": "login",
"params": {
"body": {
"email": "{{_global.loginForm.email}}",
"password": "{{_global.loginForm.password}}"
}
}
}
]
}
]
}
왜 init_actions가 필요한가?
| 항목 | state 블록 | init_actions |
|---|---|---|
| 적용 대상 | _local 상태만 |
_local 또는 _global 상태 |
| 동적 값 | 불가 | 가능 ({{query.xxx}} 등) |
dataKey="_global.xxx" |
❌ 작동 안함 | ✅ 작동함 |
주의: state 블록은 _local에만 적용됨
- "state": { "formData": {...} } → _local.formData (O)
- "state": { "_global.formData": {...} } → 작동 안함 (X)
✅ 전역 상태 초기화는 반드시 init_actions 사용
- init_actions + setState target: "global" → _global.formData (O)
init_actions에서 _local 상태 초기화 (URL 쿼리 동기화)
버전: engine-v1.4.0+
init_actions에서 target: "global"과 _local 래퍼를 함께 사용하여 _local 상태를 동적 값으로 초기화할 수 있습니다. 이 패턴은 특히 URL 쿼리 파라미터를 _local 상태에 동기화할 때 사용됩니다.
{
"version": "1.0.0",
"layout_name": "admin_product_list",
"init_actions": [
{
"handler": "setState",
"params": {
"target": "global",
"_local": {
"filter": {
"searchField": "{{query.search_field || 'all'}}",
"displayStatus": "{{query.display_status || 'all'}}",
"taxStatus": "{{query.tax_status || 'all'}}",
"category": "{{query.category || ''}}",
"keyword": "{{query.keyword || ''}}"
}
}
}
}
],
"components": [...]
}
동작 원리:
| 항목 | 설명 |
|---|---|
target: "global" |
상태 저장소 타겟 지정 (전역 스토어 접근) |
_local: {...} |
_local 상태 네임스페이스에 데이터 저장 |
{{query.xxx}} |
URL 쿼리 파라미터에서 동적 값 바인딩 |
왜 이 패턴이 필요한가?
| 방법 | 동적 값 지원 | _local 초기화 | 사용 시점 |
|---|---|---|---|
state 블록 |
❌ | ✅ | 정적 초기값만 필요할 때 |
init_actions + target: "global" + 직접 키 |
✅ | ❌ (_global에 저장) | 전역 상태 초기화 |
init_actions + target: "global" + _local 래퍼 |
✅ | ✅ | URL 쿼리 → _local 동기화 |
사용 사례:
- 목록 화면에서 필터 값을 URL과 동기화 (뒤로가기/새로고침 시 유지)
- 페이지네이션 상태를 URL에서 복원
- 검색 키워드를 URL 파라미터에서 초기화
동작 방식
dataKey가_global.로 시작하면_global.formData에 바인딩- 입력값 변경 시
_global.formData.title,_global.formData.isActive등이 자동 업데이트 trackChanges가true이면_global.hasChanges가 자동으로true로 설정
사용 사례
- 여러 레이아웃/컴포넌트에서 같은 폼 데이터를 공유해야 할 때
- 모달에서 입력받은 데이터를 부모 페이지에서 사용해야 할 때
- 사이드 패널과 메인 영역 간 데이터 동기화가 필요할 때
- 로그인 폼처럼 실패 시에도 입력값을 유지해야 할 때
격리된 상태 바인딩 (_isolated. 접두사)
버전: engine-v1.14.0+
격리된 상태에 폼 데이터를 바인딩해야 하는 경우 _isolated. 접두사를 사용합니다. 격리된 상태는 해당 영역 내에서만 유효하며, 상태 변경 시 전체 레이아웃이 아닌 격리된 영역만 리렌더링됩니다.
기본 사용법
{
"type": "Div",
"isolatedState": {
"miniForm": {
"name": "",
"email": ""
}
},
"isolatedScopeId": "quick-form",
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"dataKey": "_isolated.miniForm",
"trackChanges": true
},
"children": [
{
"type": "basic",
"name": "Input",
"props": { "name": "name", "placeholder": "이름" }
},
{
"type": "basic",
"name": "Input",
"props": { "name": "email", "type": "email", "placeholder": "이메일" }
}
]
}
]
}
필수: isolatedState로 초기값 정의
주의: dataKey="_isolated.xxx" 패턴을 사용하려면
상위 컴포넌트에 isolatedState 속성이 정의되어 있어야 합니다.
✅ isolatedState가 있는 컴포넌트 내에서만 _isolated 바인딩 가능
❌ isolatedState 없이 _isolated 바인딩 시 → _local로 폴백 + 경고 로그
동작 방식
dataKey가_isolated.로 시작하면_isolated.miniForm에 바인딩- 입력값 변경 시
_isolated.miniForm.name,_isolated.miniForm.email자동 업데이트 - 상태 변경 시 격리된 영역만 리렌더링 (성능 최적화)
trackChanges가true이면_isolated.hasChanges가 자동으로true로 설정
사용 사례
| 사례 | 설명 |
|---|---|
| 빈번한 입력 폼 | 검색 필터, 실시간 미리보기 폼 등 잦은 입력이 발생하는 경우 |
| 독립적인 UI 영역 | 사이드 패널, 미니 폼 등 다른 영역과 상태를 공유하지 않는 경우 |
| 성능 최적화 필요 | 입력 시 전체 레이아웃 리렌더링을 방지해야 하는 경우 |
_local vs _global vs _isolated 비교
| 접두사 | 스코프 | 리렌더링 범위 | 라이프사이클 | 사용 시점 |
|---|---|---|---|---|
(없음) / _local. |
현재 레이아웃 | 전체 레이아웃 | 페이지 이동 시 초기화 | 일반 폼 데이터 |
_global. |
앱 전체 | 전체 앱 | 유지됨 | 다중 페이지 공유 데이터 |
_isolated. |
격리된 컴포넌트 | 격리된 영역만 | 컴포넌트 언마운트 시 소멸 | 빈번한 인터랙션 영역 |
Props 설명
| Prop | 타입 | 필수 | 설명 |
|---|---|---|---|
dataKey |
string | ✅ | 폼 데이터 경로. 기본적으로 _local 바인딩 (예: "formData" → _local.formData). _global. 접두사로 전역 상태 바인딩 가능 |
trackChanges |
boolean | ❌ | true이면 입력 시 hasChanges를 자동으로 true로 설정 |
debounce |
number | ❌ | 밀리초 단위 debounce 지연 시간. 빈번한 입력 시 성능 최적화에 사용 (예: 300) |
autoBinding |
boolean | ❌ | false로 설정 시 해당 필드의 자동 바인딩을 명시적으로 비활성화 (engine-v1.17.6+). 커스텀 핸들러나 G7Core API로 상태를 관리하는 경우 사용 |
자동 바인딩 조건
자동 바인딩이 적용되려면 다음 조건을 모두 충족해야 합니다:
- 부모(또는 조상) 컴포넌트에
dataKeyprop이 있어야 함 - 자식 컴포넌트에
nameprop이 있어야 함 - 자식 컴포넌트에
valueprop이 명시적으로 지정되어 있지 않아야 함
자동 바인딩 스킵 조건 (engine-v1.17.6+)
컴포넌트 props에 autoBinding: false가 명시되면 해당 필드의 자동 바인딩이 스킵됩니다:
{
"type": "basic",
"name": "Input",
"props": {
"type": "radio",
"name": "purchase_restriction",
"value": "none",
"checked": "{{(_local.form.purchase_restriction ?? 'none') === 'none'}}",
"autoBinding": false
}
}
- 스킵된 필드만 자동 바인딩이 해제되며, 같은 폼의 다른 필드는 종전대로 자동 바인딩 유지
- 라디오 버튼, 체크박스 등 자동 바인딩의
value덮어쓰기가 유해한 컴포넌트에서 사용 autoBinding: false사용 시value/checked/onChange를 직접 바인딩해야 함
지원 컴포넌트
| 컴포넌트 | 바인딩 방식 | 비고 |
|---|---|---|
| Input | value + onChange |
text, number, email 등 모든 타입 |
| Textarea | value + onChange |
|
| Select | value + onChange |
|
| Toggle | value + onChange |
boolean 값 자동 감지하여 checked 처리 |
| Checkbox | value + onChange |
boolean 값 자동 감지하여 checked 처리 |
동작 방식
1. DynamicRenderer가 dataKey prop 감지
2. FormProvider로 자식 컴포넌트들을 감싸기
3. 자식 컴포넌트 렌더링 시 name prop 감지
4. state[dataKey][name] 값의 타입에 따라 자동 바인딩:
- boolean: value + onChange(e.target.checked) - Toggle, Checkbox용
- 그 외: value + onChange(e.target.value) - Input, Textarea, Select용
5. trackChanges가 true이면 hasChanges도 자동 설정
중첩 구조 지원
dataKey가 있는 컴포넌트 내부에 여러 단계의 자식이 있어도 자동 바인딩이 작동합니다:
{
"name": "Form",
"props": { "dataKey": "formData" },
"children": [
{
"name": "Div",
"props": { "className": "form-group" },
"children": [
{
"name": "Label",
"text": "제목"
},
{
"name": "Input",
"props": { "name": "title" }
}
]
}
]
}
명시적 바인딩 혼용
일부 필드만 명시적으로 바인딩하고 나머지는 자동 바인딩 사용:
{
"name": "Div",
"props": { "dataKey": "formData" },
"children": [
{
"name": "Input",
"props": { "name": "title" }
},
{
"name": "Input",
"props": {
"name": "customField",
"value": "{{_local.customValue}}",
"onChange": "customHandler"
}
}
]
}
위 예시에서 title은 자동 바인딩되고, customField는 명시적 value가 있으므로 자동 바인딩되지 않습니다.
setState dot notation 지원
폼 자동 바인딩과 함께 setState 액션에서도 dot notation 경로를 지원합니다:
{
"actions": [
{
"type": "change",
"handler": "setState",
"params": {
"target": "_local.formData.title",
"value": "{{event.target.value}}"
}
}
]
}
이 방식은 기존의 spread 연산자 방식보다 간결하며, 다른 필드를 덮어쓰지 않고 부분 업데이트합니다.
주의사항
| 항목 | 설명 |
|---|---|
| Form 태그 필수 아님 | dataKey는 어떤 컴포넌트에든 적용 가능 (Div, Section 등) |
| 초기 데이터 필요 | state.formData 또는 data_sources의 initLocal로 초기화 필요 |
| boolean 타입 감지 | 초기값이 boolean이어야 Toggle/Checkbox 자동 바인딩 작동 |
| FileInput 미지원 | File 객체 처리를 위한 별도 로직 필요 |
| debounce 중복 주의 | 자동 바인딩 debounce와 액션의 debounce 속성을 함께 사용하면 이중 지연 발생 (예: 300ms + 300ms = 600ms). 아래 "debounce 레벨 구분" 참조 |
| autoBinding: false 시 명시적 바인딩 필수 | autoBinding: false 사용 시 value/onChange를 직접 바인딩해야 함. 미설정 시 필드가 빈 상태로 표시됨 (engine-v1.17.6+) |
debounce 레벨 구분
G7에서 debounce는 두 가지 레벨에서 설정할 수 있습니다. 혼동에 주의하세요.
| 레벨 | 설정 위치 | 적용 대상 | 설명 |
|---|---|---|---|
| 컴포넌트 레벨 | dataKey 부모의 debounce prop |
자동 바인딩 onChange | 폼 자동 바인딩의 상태 업데이트 지연 |
| 액션 레벨 | actions[].debounce 속성 |
개별 액션 실행 | 특정 액션(apiCall, refetchDataSource 등)의 실행 지연 |
{
"type": "basic",
"name": "Div",
"props": {
"dataKey": "formData",
"debounce": 300
},
"children": [
{
"type": "basic",
"name": "Input",
"props": { "name": "keyword" },
"actions": [
{
"type": "change",
"handler": "refetchDataSource",
"params": { "dataSourceId": "search_results" },
"debounce": 500
}
]
}
]
}
위 예시에서 두 debounce가 모두 적용되면:
- 컴포넌트 debounce (300ms) → _local.formData.keyword 업데이트 지연
- 액션 debounce (500ms) → refetchDataSource 실행 지연
- 총 지연: 최대 800ms (이중 지연 발생!)
✅ 권장: 하나만 선택
- 폼 자동 바인딩만 사용 → 컴포넌트 debounce만 설정
- 명시적 액션 사용 → 액션 debounce만 설정
폼 변경 감지 (trackChanges/hasChanges)
버전: engine-v1.3.0+
폼 데이터가 변경되었는지 추적하여 저장 버튼 활성화 등의 UI 상태를 자동으로 관리하는 시스템입니다.
변경 감지 핵심 원칙
✅ trackChanges로 변경 추적 활성화: 부모 컴포넌트에 trackChanges: true 설정
✅ hasChanges 자동 관리: 입력 시 _local.hasChanges가 자동으로 true로 설정
✅ 초기 로드 시 리셋: initLocal 적용 시 hasChanges가 자동으로 false로 초기화
✅ 저장 성공 시 리셋: onSuccess 핸들러에서 hasChanges: false로 수동 리셋 필요
기본 사용법
{
"id": "form_container",
"type": "basic",
"name": "Div",
"dataKey": "formData",
"trackChanges": true,
"children": [
{
"name": "Input",
"props": { "name": "title" }
},
{
"name": "Input",
"props": { "name": "description" }
}
]
}
저장 버튼과 연동
hasChanges 플래그를 사용하여 저장 버튼의 활성화 상태를 제어합니다:
{
"id": "save_button",
"type": "basic",
"name": "Button",
"props": {
"disabled": "{{!_local.hasChanges || _local.isSaving}}"
},
"text": "$t:common.save",
"actions": [
{
"type": "click",
"handler": "sequence",
"actions": [
{
"handler": "setState",
"params": {
"target": "local",
"isSaving": true
}
},
{
"handler": "apiCall",
"target": "/api/settings",
"params": { "method": "POST", "body": "{{_local.formData}}" },
"onSuccess": [
{
"handler": "setState",
"params": {
"target": "local",
"isSaving": false,
"hasChanges": false
}
},
{
"handler": "toast",
"params": { "type": "success", "message": "$t:common.save_success" }
}
],
"onError": [
{
"handler": "setState",
"params": {
"target": "local",
"isSaving": false
}
}
]
}
]
}
]
}
취소 버튼과 연동
데이터를 다시 불러와서 폼을 초기 상태로 되돌립니다:
{
"id": "cancel_button",
"type": "basic",
"name": "Button",
"text": "$t:common.cancel",
"actions": [
{
"type": "click",
"handler": "refetchDataSource",
"params": {
"dataSourceId": "settings"
}
}
]
}
refetchDataSource 실행 시 initLocal이 다시 적용되면서 hasChanges가 자동으로 false로 리셋됩니다.
동작 흐름
1. 레이아웃 로드
↓
2. data_sources의 initLocal 적용
→ _local.formData = API 응답 데이터
→ _local.hasChanges = false (자동 초기화)
↓
3. 사용자가 폼 필드 수정
→ _local.formData.필드명 = 새 값
→ _local.hasChanges = true (trackChanges가 true일 때 자동)
↓
4. 저장 버튼 활성화 (hasChanges가 true이므로)
↓
5. 저장 버튼 클릭 → API 호출
↓
6-a. 저장 성공
→ onSuccess에서 hasChanges: false 설정
→ 저장 버튼 비활성화
6-b. 저장 실패
→ hasChanges는 true 유지
→ 저장 버튼 활성화 유지
변경 감지 Props 설명
| Prop | 타입 | 위치 | 설명 |
|---|---|---|---|
dataKey |
string | 부모 컴포넌트 | 폼 데이터 경로 (예: "formData" → _local.formData) |
trackChanges |
boolean | 부모 컴포넌트 | true이면 자식 입력 필드 변경 시 hasChanges 자동 설정 |
hasChanges |
boolean | _local 상태 |
폼 데이터 변경 여부 (자동 관리됨) |
initLocal과 hasChanges 자동 리셋
data_sources에서 initLocal이 설정된 경우, API 응답이 로컬 상태에 적용될 때 hasChanges가 자동으로 false로 초기화됩니다:
{
"data_sources": [
{
"id": "settings",
"type": "api",
"endpoint": "/api/admin/settings",
"method": "GET",
"auto_fetch": true,
"initLocal": "formData",
"refetchOnMount": true
}
]
}
이 동작은 DynamicRenderer.tsx에서 다음과 같이 처리됩니다:
// initLocal 적용 시 hasChanges를 false로 초기화
setLocalDynamicState(prev => ({
loadingActions: prev.loadingActions || {},
...localInitData,
hasChanges: false,
}));
전역 상태에서의 사용
dataKey가 _global.로 시작하는 경우, hasChanges도 전역 상태에 저장됩니다:
{
"name": "Div",
"props": {
"dataKey": "_global.formData",
"trackChanges": true
},
"children": [...]
}
이 경우 _global.hasChanges가 자동으로 관리됩니다.
변경 감지 주의사항
| 항목 | 설명 |
|---|---|
| 저장 성공 시 수동 리셋 필요 | onSuccess에서 hasChanges: false 명시적 설정 필요 |
| 원본 값 비교 미지원 | 현재는 입력 발생 시 무조건 dirty로 처리 (원본 값과 동일해도 hasChanges가 true) |
| dataKey 필수 | trackChanges는 dataKey와 함께 사용해야 동작 |
| refetchDataSource 활용 | 취소 시 refetchDataSource로 원본 데이터 다시 로드하면 자동 리셋 |
setState 액션
컴포넌트 로컬 상태 또는 전역 상태를 업데이트하는 액션입니다.
기본 문법
{
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "component" | "global",
"payload": {
"key": "value"
}
}
]
}
필드 설명
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
event |
string | ✅ | 이벤트 타입 (onClick, onChange 등) |
type |
string | ✅ | "setState" 고정 |
target |
string | ✅ | "component" (로컬) 또는 "global" (전역) |
payload |
object | ✅ | 업데이트할 상태 객체 |
target 옵션 비교
| target | 접근 방식 | 영향 범위 | 사용 시점 |
|---|---|---|---|
"component" |
해당 컴포넌트 내부 | 해당 컴포넌트만 | 컴포넌트 내부 상태 관리 |
"global" |
_global.속성 |
모든 컴포넌트 | 여러 컴포넌트 간 상태 공유 |
컴포넌트 로컬 상태 업데이트
{
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "component",
"payload": {
"isOpen": true,
"selectedTab": "profile"
}
}
]
}
전역 상태 업데이트
{
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "global",
"payload": {
"sidebarOpen": false,
"currentTheme": "dark"
}
}
]
}
깊은 병합 (Deep Merge)
버전: engine-v1.2.0+
로컬 상태 업데이트 시 중첩 객체의 특정 필드만 업데이트하고 나머지 필드는 자동으로 유지됩니다.
병합 모드 선택
| 모드 | 옵션 | 동작 | 사용 시점 |
|---|---|---|---|
| 깊은 병합 (기본) | 생략 또는 "merge": "deep" |
재귀적 깊은 병합 (중첩 객체 보존) | 개별 필드 업데이트 |
| 얕은 병합 | "merge": "shallow" |
최상위 키만 덮어쓰기 (1단계) | 프리셋 적용, 필터 초기화 |
| 완전 교체 | "merge": "replace" |
기존 상태 완전 무시, 새 값으로 교체 | 폼 초기화, 서버 데이터로 전체 교체 |
3모드 비교 예시
현재 상태: { form: { name: "기존", price: 1000 }, ui: { tab: "basic" } }
업데이트: { form: { name: "신규" } }
replace: { form: { name: "신규" } } ← ui 키 자체가 사라짐
shallow: { form: { name: "신규" }, ui: { tab: "basic" } } ← ui 키 유지, form 전체 교체
deep: { form: { name: "신규", price: 1000 }, ui: { tab: "basic" } } ← price도 유지
완전 교체 (merge: "replace")
버전: engine-v1.18.0+
기존 상태를 완전히 무시하고 새 값으로 교체합니다. 서버에서 받은 데이터로 전체 상태를 덮어쓰거나, 폼을 완전 초기화할 때 사용합니다.
{
"handler": "setState",
"params": {
"target": "_local",
"merge": "replace",
"formData": {
"name": "",
"price": 0,
"description": ""
}
}
}
주의사항:
replace는 해당 target의 모든 기존 키를 제거합니다
- local target: 기존 _local 전체가 payload로 교체됨
- global target: _global 전체가 payload로 교체됨 (modalStack 등도 사라질 수 있음)
- 부분 업데이트에는 deep 또는 shallow를 사용하세요
얕은 병합 (merge: "shallow")
버전: engine-v1.4.0+
전체 객체를 교체해야 할 때 사용합니다. 프리셋 적용이나 폼 초기화 시 기존 값을 모두 덮어쓰고 새 값으로 교체합니다.
{
"handler": "setState",
"params": {
"target": "_local",
"merge": "shallow",
"filter": {
"searchField": "all",
"displayStatus": "all",
"taxStatus": "all",
"category": ""
}
}
}
얕은 병합 동작 예시:
// 현재 상태
{
filter: { searchField: "name", displayStatus: "visible", taxStatus: "taxable", keyword: "테스트" }
}
// 업데이트 payload (merge: "shallow")
{
filter: { searchField: "all", displayStatus: "all", taxStatus: "all" }
}
// 결과 (얕은 병합) - filter 전체가 교체됨
{
filter: { searchField: "all", displayStatus: "all", taxStatus: "all" }
}
// 비교: 깊은 병합이었다면 (기본 동작)
{
filter: { searchField: "all", displayStatus: "all", taxStatus: "all", keyword: "테스트" }
}
사용 사례:
| 사례 | 설명 |
|---|---|
| 프리셋 적용 | 저장된 필터 프리셋을 불러와 모든 필터 값 교체 |
| 폼 초기화 | 초기화 버튼 클릭 시 모든 폼 필드를 기본값으로 리셋 |
| 부분 객체 교체 | 특정 최상위 키의 객체를 통째로 교체하되 다른 키는 유지 |
참고: 전체 상태를 서버 데이터로 완전히 교체하려면
merge: "replace"를 사용하세요.
주의사항:
얕은 병합은 최상위 키만 덮어씁니다
- filter 키 전체가 교체됨
- 다른 최상위 키(visibleFilters, isEditMode 등)는 유지됨
기존 방식 (spread 연산자 필요)
{
"type": "change",
"handler": "setState",
"params": {
"target": "local",
"formData": {
"...": "{{_local.formData}}",
"name": "{{$event.target.value}}"
},
"hasChanges": true
}
}
새로운 방식 (깊은 병합)
{
"type": "change",
"handler": "setState",
"params": {
"target": "local",
"formData": {
"name": "{{$event.target.value}}"
},
"hasChanges": true
}
}
spread 연산자 없이도 formData.name만 업데이트되고 formData.email, formData.slug 등은 자동으로 유지됩니다.
병합 규칙
| 값 타입 | 동작 | 예시 |
|---|---|---|
| 일반 객체 (Object) | 기존 상태와 병합 | { formData: { name: "new" } } → formData.name만 변경 |
| 배열 (Array) | 덮어쓰기 (병합 안함) | { tags: [1, 2] } → tags 전체 교체 |
| null | 덮어쓰기 | { formData: null } → formData를 null로 설정 |
| 기본 타입 | 덮어쓰기 | { count: 5 } → count를 5로 설정 |
동작 예시
// 현재 상태
{
formData: { name: "old", email: "a@b.com", slug: "old-slug" },
hasChanges: false
}
// 업데이트 payload
{
formData: { name: "new" },
hasChanges: true
}
// 결과 (깊은 병합)
{
formData: { name: "new", email: "a@b.com", slug: "old-slug" },
hasChanges: true
}
하위 호환성
기존 spread 연산자 패턴("...": "{{_local.formData}}")은 계속 지원됩니다. spread 연산자가 있으면 evaluateExpressions()에서 먼저 처리되어 완성된 객체가 전달됩니다.
깊은 병합 제한사항
| 항목 | 설명 |
|---|---|
| 1단계 병합만 지원 | formData.nested.field 같은 2단계 이상 병합은 미지원 |
| 전역 상태는 미적용 | target: "global"인 경우 깊은 병합 미적용 |
| 배열은 병합 안함 | 배열 값은 전체가 교체됨 |
payload 표현식 평가
payload 값에 {{}} 표현식을 사용하여 동적으로 값을 계산할 수 있습니다.
지원하는 표현식
{
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "global",
"payload": {
// boolean 토글
"sidebarOpen": "{{!_global.sidebarOpen}}",
// 조건부 값 설정 (삼항 연산자)
"theme": "{{_global.theme == 'dark' ? 'light' : 'dark'}}",
// 비교 연산
"isLarge": "{{count > 100}}",
// 정적 값 (표현식 없음)
"selectedTab": "profile"
}
}
]
}
표현식 타입별 예시
| 표현식 타입 | 예시 | 결과 |
|---|---|---|
| boolean 토글 | "{{!_global.isOpen}}" |
true ↔ false |
| 삼항 연산자 | "{{_global.theme == 'dark' ? 'light' : 'dark'}}" |
'light' 또는 'dark' |
| 비교 연산 | "{{count > 100}}" |
true 또는 false |
| 정적 값 | "profile" |
"profile" |