23 KiB
상태 관리 - 고급 기능
메인 문서: state-management.md 관련 문서: state-management-forms.md | g7core-api.md
목차
- 예약된 전역 상태
- 내부 전역 컨텍스트 변수
- 실제 사용 사례
- 조건부 렌더링과의 조합
- 별도 모듈 상태 동기화 패턴
- G7Core.state JavaScript API
- G7Core 편의 API
예약된 전역 상태
템플릿 엔진이 특수한 용도로 사용하는 예약된 전역 상태 속성입니다. 이 속성들은 시스템에서 자동으로 관리되며, 특정 기능과 연동됩니다.
예약 속성 목록
| 속성 | 타입 | 용도 | 관련 기능 |
|---|---|---|---|
toasts |
ToastItem[] |
토스트 알림 스택 | 토스트 시스템 |
modalStack |
string[] |
열린 모달 ID 스택 (engine-v1.2.0+) | 멀티 모달 시스템 |
activeModal |
string | null |
현재 최상위 모달 ID | 모달 시스템 (하위 호환) |
sidebarOpen |
boolean |
사이드바 열림 상태 | 반응형 레이아웃 |
errorMessage |
string | null |
에러 메시지 (에러 모달용) | 에러 처리 |
toasts (토스트 시스템)
_global.toasts는 화면에 표시할 토스트 알림들을 배열로 관리합니다.
ToastItem 구조
interface ToastItem {
id: string; // 고유 ID (자동 생성)
type: 'success' | 'error' | 'warning' | 'info';
message: string; // 표시할 메시지
icon?: string; // 커스텀 아이콘 (선택)
duration?: number; // 자동 닫힘 시간 ms (선택)
}
동작 방식
toast핸들러 호출 시toasts배열에 새 토스트 객체 추가- Toast 컴포넌트가 배열을 구독하여 스택 형태로 렌더링
- 각 토스트는 개별 타이머로 자동 제거되거나 닫기 버튼으로 수동 제거
- 제거 시
G7Core.state.update()로 배열에서 해당 항목 필터링
레이아웃 JSON 설정 예시
_admin_base.json에서 Toast 컴포넌트 바인딩:
{
"id": "global_toast",
"type": "composite",
"name": "Toast",
"props": {
"toasts": "{{_global.toasts}}",
"position": "top-right",
"duration": 5000
}
}
JavaScript API로 토스트 표시
// 편의 API (권장)
G7Core.toast.success('저장되었습니다');
G7Core.toast.error('오류가 발생했습니다');
G7Core.toast.warning('주의가 필요합니다');
G7Core.toast.info('정보 메시지');
// duration 지정
G7Core.toast.success('저장 완료', 5000);
// 상세 옵션
G7Core.toast.show('메시지', { type: 'info', icon: 'bell', duration: 3000 });
참고: 토스트 핸들러에 대한 자세한 내용은 actions-handlers.md를 참조하세요.
modalStack / activeModal (모달 시스템)
_global.modalStack은 열린 모달들의 ID를 스택 형태로 관리합니다. _global.activeModal은 하위 호환성을 위해 최상위 모달 ID를 저장합니다.
동작 방식
openModal핸들러 호출 시modalStack에 모달 ID가 push됨- 동시에
activeModal도 해당 ID로 설정됨 (하위 호환성) - 템플릿 엔진이
modals배열의 각 모달에isOpenprops를 자동 주입 closeModal핸들러 호출 시 스택에서 최상위 모달만 제거됨- 스택에 이전 모달이 있으면 해당 모달이 다시 표시됨
멀티 모달 (중첩 모달) 지원
✅ 모달 A 열림 → 모달 B 열림 → 모달 B 닫힘 → 모달 A 다시 표시
✅ 에러 모달이 확인 모달 위에 중첩 표시 가능
✅ 스택 기반으로 무제한 중첩 지원
스택 동작 예시:
1. openModal("confirm_modal")
→ modalStack: ["confirm_modal"]
→ activeModal: "confirm_modal"
2. openModal("error_modal")
→ modalStack: ["confirm_modal", "error_modal"]
→ activeModal: "error_modal"
3. closeModal()
→ modalStack: ["confirm_modal"]
→ activeModal: "confirm_modal"
4. closeModal()
→ modalStack: []
→ activeModal: null
사용 예시
{
"id": "activate_button",
"type": "basic",
"name": "Button",
"props": {
"children": "활성화"
},
"actions": [
{
"type": "click",
"handler": "openModal",
"params": {
"modalId": "activate_confirm_modal"
}
}
]
}
모달 정의와의 연동
레이아웃 JSON의 modals 배열에 정의된 모달은 activeModal 값에 따라 자동으로 표시됩니다:
{
"modals": [
{
"id": "activate_confirm_modal",
"type": "composite",
"name": "Modal",
"props": {
"title": "확인",
"width": "400px"
},
"children": [...]
}
]
}
위 모달은 _global.activeModal === "activate_confirm_modal"일 때 자동으로 열립니다.
내부 전역 컨텍스트 변수
버전: engine-v1.17.4+ 주의: 이 변수들은 템플릿 엔진 내부 동작을 위한 것으로, 일반적인 개발에서는 직접 사용하지 않습니다.
템플릿 엔진이 비동기 콜백과 상태 동기화를 위해 사용하는 window 전역 변수입니다.
내부 변수 목록
| 변수 | 용도 | 설정 시점 | 초기화 시점 |
|---|---|---|---|
__g7ActionContext |
액션 디스패치 컨텍스트 (setState 함수 포함) | 액션 실행 시 | 페이지 전환 시 |
__g7ForcedLocalFields |
비동기 setLocal fallback에서 업데이트된 필드만 저장 | setLocal fallback 시 | 페이지 전환 시 |
__g7PendingLocalState |
비동기 콜백에서 즉시 읽기 지원용 pending 상태 | setLocal 호출 시 | 페이지 전환 시 |
__g7ActionContext
액션 디스패치 시 현재 컴포넌트의 컨텍스트를 저장합니다. 커스텀 핸들러에서 G7Core.state.setLocal() 호출 시 이 컨텍스트를 사용하여 올바른 컴포넌트 상태를 업데이트합니다.
interface ActionContext {
state: Record<string, any>; // 현재 컴포넌트 상태 스냅샷
setState: (updates) => void; // 컴포넌트 상태 업데이트 함수
data: Record<string, any>; // 데이터 컨텍스트 ($event 등)
}
동작 흐름:
- 액션 핸들러 실행 시
__g7ActionContext설정 - 핸들러 내에서
setLocal()호출 시setState사용 - 핸들러 완료 후 컨텍스트 복원
__g7ForcedLocalFields
비동기 콜백(예: 외부 API 팝업의 oncomplete)에서 setLocal()을 호출하면 __g7ActionContext가 없어 fallback 경로를 사용합니다. 이때 업데이트된 필드만 이 변수에 저장됩니다.
// 예시: 주소 검색 팝업에서 선택 시
G7Core.state.setLocal({
'form.basic_info.zipcode': '12345',
'form.basic_info.base_address': '서울시 강남구'
});
// __g7ForcedLocalFields에 저장됨:
{
form: {
basic_info: {
zipcode: '12345',
base_address: '서울시 강남구'
}
}
}
DynamicRenderer에서의 병합 순서:
1. dataContext._local (전역 상태)
2. dynamicState (Form 자동 바인딩 - 사용자 입력)
3. __g7ForcedLocalFields (비동기 setLocal - 최우선)
이 순서를 통해:
- 비동기 콜백에서 설정한 값이 stale dynamicState를 덮어씀
- 다른 필드의 사용자 입력은 dynamicState에서 보존됨
__g7PendingLocalState
setLocal() 호출 직후 같은 동기 흐름에서 getLocal()로 값을 읽을 때 최신 값을 반환하기 위한 pending 상태입니다.
// setLocal 후 즉시 getLocal 호출 시
G7Core.state.setLocal({ 'checkout.zipcode': '12345' });
const current = G7Core.state.getLocal(); // __g7PendingLocalState에서 최신 값 반환
__g7LastSetLocalSnapshot (engine-v1.21.2+)
setLocal() 호출 시 설정되는 스냅샷으로, __g7PendingLocalState가 클리어된 후에도 getLocal()이 최신 값을 반환할 수 있도록 보장합니다.
배경: await 경계에서 React 18 마이크로태스크 배칭이 렌더를 플러시하면, useLayoutEffect가 __g7PendingLocalState = null로 클리어합니다. 이후 getLocal() 호출 시 stale한 globalLocal만 반환되는 문제가 있었습니다.
동작 원리:
// getLocal() fallback 우선순위:
// 1. __g7PendingLocalState (동일 이벤트 루프)
// 2. __g7LastSetLocalSnapshot (await 이후에도 유지)
// 3. globalLocal (기본값)
클리어 시점: dataContext._local이 갱신되면(= React 커밋 완료) queueMicrotask로 클리어됩니다. handleLocalSetState에서는 참조하지 않으므로 기존 stale 오염 방지 로직(__g7SetLocalOverrideKeys)과 충돌하지 않습니다.
페이지 전환 시 초기화
페이지 전환(navigate) 시 모든 내부 컨텍스트 변수가 초기화됩니다:
// TemplateApp.handleRouteChange()에서
(window as any).__g7ForcedLocalFields = undefined;
(window as any).__g7ActionContext = undefined;
(window as any).__g7PendingLocalState = undefined;
이 초기화가 없으면 이전 페이지의 stale 컨텍스트가 새 페이지에 영향을 줄 수 있습니다.
디버깅 시 활용
브라우저 콘솔에서 현재 상태 확인:
// 현재 액션 컨텍스트 확인
console.log(window.__g7ActionContext);
// 강제 적용된 로컬 필드 확인
console.log(window.__g7ForcedLocalFields);
// pending 상태 확인
console.log(window.__g7PendingLocalState);
자동 주입되는 Props
템플릿 엔진이 모달 컴포넌트에 자동으로 주입하는 props:
| prop | 설명 |
|---|---|
isOpen |
_global.activeModal === modalId 평가 결과 |
onClose |
closeModal 핸들러 호출 함수 |
직접 상태 조작 (비권장)
setState로 직접 조작할 수도 있지만, openModal/closeModal 핸들러 사용을 권장합니다:
{
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "global",
"payload": {
"activeModal": "my_modal_id"
}
}
]
}
참고: 모달 시스템에 대한 자세한 내용은 layout-json-features.md를 참조하세요.
실제 사용 사례
1. 모바일 사이드바 토글
{
"id": "mobile_menu_button",
"type": "basic",
"name": "Button",
"props": {
"className": "p-2 hover:bg-gray-100 rounded-lg"
},
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "global",
"payload": {
"sidebarOpen": "{{!_global.sidebarOpen}}"
}
}
]
}
2. 오버레이 닫기
{
"id": "overlay",
"type": "basic",
"name": "Div",
"if": "{{_global.sidebarOpen}}",
"props": {
"className": "fixed inset-0 bg-black bg-opacity-50 z-40"
},
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "global",
"payload": {
"sidebarOpen": false
}
}
]
}
3. 다크모드 토글
{
"id": "theme_toggle",
"type": "basic",
"name": "Button",
"props": {
"className": "p-2 rounded-lg"
},
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "global",
"payload": {
"theme": "{{_global.theme == 'dark' ? 'light' : 'dark'}}"
}
}
]
}
주요 사용 사례 목록
- 모바일 사이드바 토글 상태
- 다크모드 on/off 상태
- 전역 로딩 상태
- 모달/드로어 열림/닫힘 상태
- 사용자 선택 언어/테마
- 탭/메뉴 선택 상태
조건부 렌더링과의 조합
전역 상태와 if 조건을 조합하여 동적 UI를 구현할 수 있습니다.
{
"id": "mobile_overlay",
"type": "basic",
"name": "Div",
"if": "{{_global.sidebarOpen}}",
"props": {
"className": "fixed inset-0 bg-black bg-opacity-50 z-40"
},
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "global",
"payload": {
"sidebarOpen": false
}
}
]
}
위 예시에서:
if:_global.sidebarOpen이true일 때만 렌더링actions: 클릭 시sidebarOpen을false로 설정하여 오버레이 닫기
별도 모듈 상태 동기화 패턴
중요: 별도 모듈에서 상태를 캐싱하는 경우 동기화 필요
✅ 필수: update 메서드를 제공하여 최신 값 반영
문제 상황
별도 클래스나 모듈에서 locale, globalState 등을 생성자에서 받아 저장하면, 나중에 상태가 변경되어도 초기화 시점의 값이 계속 사용됩니다.
// ❌ 문제: 초기화 시점의 locale이 고정됨
class ErrorPageHandler {
private locale: string;
constructor(options: { locale: string }) {
this.locale = options.locale; // 초기화 시점 값 고정
}
async renderError() {
// this.locale은 항상 초기값 사용 (언어 변경 미반영)
await this.renderFunction({
translationContext: { locale: this.locale }
});
}
}
해결 패턴
update 메서드를 제공하여 상태 변경 시점에 명시적으로 동기화합니다:
// ✅ 해결: update 메서드로 동기화
class ErrorPageHandler {
private locale: string;
private globalState: Record<string, unknown>;
constructor(options: { locale: string; globalState?: Record<string, unknown> }) {
this.locale = options.locale;
this.globalState = options.globalState || {};
}
// 상태 업데이트 메서드 제공
updateLocale(locale: string): void {
this.locale = locale;
}
updateGlobalState(state: Record<string, unknown>): void {
this.globalState = { ...this.globalState, ...state };
}
}
호출 시점
상태가 변경되거나 모듈을 사용하기 전에 update 메서드를 호출합니다:
// 1. 상태 변경 시점에서 동기화
async changeLocale(locale: string): Promise<void> {
this.config.locale = locale;
// 관련 모듈에 변경 사항 전파
if (this.errorPageHandler) {
this.errorPageHandler.updateLocale(locale);
}
}
// 2. 모듈 사용 직전에 최신 상태 전달
async handleRouteNotFound(path: string): Promise<void> {
if (this.errorPageHandler) {
// 사용 전 최신 상태로 동기화
this.errorPageHandler.updateGlobalState(this.globalState);
this.errorPageHandler.updateLocale(this.config.locale);
await this.errorPageHandler.renderError(404, 'app');
}
}
적용 대상
이 패턴이 필요한 경우:
| 대상 | 동기화 항목 | update 메서드 |
|---|---|---|
| ErrorPageHandler | locale, globalState | updateLocale(), updateGlobalState() |
| 커스텀 핸들러 | 설정값, 컨텍스트 | 필요에 따라 구현 |
대안: 참조 전달
매번 동기화하는 대신, 객체 참조를 전달하여 자동 동기화할 수도 있습니다:
// 대안: 설정 객체 참조 전달
class ErrorPageHandler {
private config: TemplateAppConfig; // 참조로 전달
constructor(options: { config: TemplateAppConfig }) {
this.config = options.config; // 원본 객체 참조
}
async renderError() {
// this.config.locale은 항상 최신 값 (원본 객체 참조)
await this.renderFunction({
translationContext: { locale: this.config.locale }
});
}
}
주의: 참조 전달 방식은 의도치 않은 부작용이 발생할 수 있으므로, 명시적 update 메서드 방식을 권장합니다.
G7Core.state JavaScript API
컴포넌트 코드(TypeScript/JavaScript)에서 전역 상태에 직접 접근해야 할 때 사용하는 API입니다.
API 목록
| 메서드 | 설명 | 반환값 |
|---|---|---|
G7Core.state.get() |
현재 전역 상태 반환 | Record<string, any> |
G7Core.state.set(updates) |
상태 객체로 업데이트 | void |
G7Core.state.update(updater) |
함수형 업데이트 (현재 상태 기반) | void |
G7Core.state.subscribe(listener) |
상태 변경 구독 | 구독 해제 함수 |
G7Core.state.getDataSource(id) |
데이터 소스 값 조회 | DataSourceValue | undefined |
G7Core.state.get()
현재 전역 상태를 반환합니다.
const state = G7Core.state.get();
console.log(state.toasts); // 토스트 배열
console.log(state.sidebarOpen); // 사이드바 상태
G7Core.state.set(updates)
상태 객체를 병합하여 업데이트합니다.
dot notation 지원 (engine-v1.2.0+): 중첩된 객체 경로를 dot notation으로 표현할 수 있습니다.
// 단순 값 설정
G7Core.state.set({ sidebarOpen: true });
// 여러 값 동시 설정
G7Core.state.set({
theme: 'dark',
locale: 'en'
});
// dot notation 지원 (중첩 객체)
G7Core.state.set({
"filter.orderStatus": ["paid", "shipped"]
});
// 결과: { filter: { orderStatus: ["paid", "shipped"] } }
// 기존 중첩 객체와 깊은 병합
// 현재 상태: { filter: { dateRange: "2024-01-01~2024-12-31" } }
G7Core.state.set({
"filter.orderStatus": ["paid"]
});
// 결과: { filter: { dateRange: "2024-01-01~2024-12-31", orderStatus: ["paid"] } }
주의사항:
- dot notation 경로는 자동으로 중첩 객체로 변환됩니다
- 기존 중첩 객체와 깊은 병합(deep merge)되므로 다른 속성이 유지됩니다
- 배열은 병합되지 않고 교체됩니다
G7Core.state.update(updater)
현재 상태를 기반으로 새 상태를 계산해야 할 때 사용합니다. React의 함수형 setState와 유사합니다.
// 배열에서 항목 제거
G7Core.state.update(prev => ({
toasts: prev.toasts.filter(t => t.id !== targetId)
}));
// 카운터 증가
G7Core.state.update(prev => ({
count: (prev.count || 0) + 1
}));
// 배열에 항목 추가
G7Core.state.update(prev => ({
items: [...(prev.items || []), newItem]
}));
G7Core.state.subscribe(listener)
상태 변경을 구독합니다. 반환된 함수를 호출하면 구독이 해제됩니다.
// 구독 시작
const unsubscribe = G7Core.state.subscribe((state) => {
console.log('상태 변경:', state);
});
// 구독 해제
unsubscribe();
G7Core.state.getDataSource(dataSourceId)
버전: engine-v1.3.0+
데이터 소스의 현재 값을 반환합니다. 커스텀 핸들러나 컴포넌트에서 데이터 소스에 접근할 때 사용합니다.
// 데이터 소스 조회
const menus = G7Core.state.getDataSource('menus');
console.log(menus?.data); // API 응답 데이터
// 로딩 상태 확인
const users = G7Core.state.getDataSource('users');
if (users?.loading) {
console.log('로딩 중...');
}
반환값 구조
interface DataSourceValue {
data: any; // API 응답 데이터
loading: boolean; // 로딩 중 여부
error?: Error; // 에러 객체 (실패 시)
lastFetched?: Date; // 마지막 fetch 시간
}
사용 사례
- 커스텀 핸들러에서 데이터 소스 값 참조
- URL 파라미터 초기화 시 데이터 로드 대기
- 컴포넌트 코드에서 데이터 소스 직접 접근
데이터 소스 대기 패턴
데이터 소스가 로드될 때까지 대기해야 하는 경우:
async function waitForDataSource(
dataSourceId: string,
maxAttempts: number = 30,
interval: number = 100
): Promise<any[] | null> {
for (let i = 0; i < maxAttempts; i++) {
const dataSource = G7Core.state.getDataSource(dataSourceId);
const data = dataSource?.data;
if (Array.isArray(data) && data.length > 0) {
return data;
}
await new Promise((resolve) => setTimeout(resolve, interval));
}
return null;
}
// 사용
const menusData = await waitForDataSource('menus');
get()과 getDataSource()의 차이
| 메서드 | 대상 | 반환값 | 사용 시점 |
|---|---|---|---|
get() |
전역 상태 (_global) |
Record<string, any> |
UI 상태 (사이드바, 모달 등) |
getDataSource(id) |
데이터 소스 | DataSourceValue |
API 응답 데이터 |
getDataSource 주의사항
| 항목 | 설명 |
|---|---|
| undefined 반환 가능 | 데이터 소스가 없거나 아직 로드되지 않은 경우 |
get()과 다름 |
get()은 전역 상태, getDataSource()는 데이터 소스 |
| ✅ 실시간 값 | 항상 최신 데이터 소스 값 반환 |
JavaScript API 사용 시점
| 사용 O | 사용 X |
|---|---|
| 컴포넌트 내부에서 전역 상태 접근 | 레이아웃 JSON에서 상태 접근 (→ {{_global.xxx}} 사용) |
| 복잡한 상태 계산이 필요한 경우 | 단순 토글 (→ setState 액션 사용) |
| 외부 라이브러리와 상태 연동 | 일반적인 UI 상태 관리 |
G7Core 편의 API
템플릿 엔진은 자주 사용하는 기능에 대한 편의 API를 제공합니다.
전체 API 레퍼런스: g7core-api.md
G7Core.toast - 토스트 알림
// 편의 메서드
G7Core.toast.success('저장되었습니다');
G7Core.toast.error('오류가 발생했습니다');
G7Core.toast.warning('주의가 필요합니다');
G7Core.toast.info('정보 메시지');
// duration 지정 (밀리초)
G7Core.toast.success('저장 완료', 3000);
// 상세 옵션
G7Core.toast.show('메시지', { type: 'info', duration: 5000 });
G7Core.modal - 모달 관리
// 모달 열기/닫기
G7Core.modal.open('confirm_modal');
G7Core.modal.close(); // 최상위 모달 닫기
G7Core.modal.close('confirm_modal'); // 특정 모달 닫기
G7Core.modal.closeAll(); // 모든 모달 닫기
// 상태 확인
G7Core.modal.isOpen('confirm_modal'); // boolean
G7Core.modal.getStack(); // ['modal1', 'modal2']
G7Core.navigation - 네비게이션 상태
// 전환 중 여부 확인
G7Core.navigation.isPending(); // boolean
// 전환 완료 시 콜백 실행
const unsubscribe = G7Core.navigation.onComplete(() => {
console.log('페이지 전환 완료');
});
G7Core.componentEvent - 컴포넌트 간 이벤트
// 이벤트 구독
const unsubscribe = G7Core.componentEvent.on('eventName', (data) => {
console.log('이벤트 수신:', data);
});
// 이벤트 발생
await G7Core.componentEvent.emit('eventName', { key: 'value' });
// 리스너 제거
G7Core.componentEvent.off('eventName');
G7Core.componentEvent.clear(); // 모든 리스너 제거
관련 문서
- 상태 관리 개요 - _global, _local 기본 개념
- 폼 자동 바인딩 - FormContext, setState, 깊은 병합
- G7Core API 전체 - 렌더링, 인증, 반응형 등
- 액션 핸들러 - toast, openModal, setState 핸들러