Files
Gnuboard7/docs/frontend/layout-json-components-loading.md
T
2026-04-01 10:30:52 +09:00

20 KiB

레이아웃 JSON - 데이터 로딩 및 생명주기

메인 문서: layout-json-components.md 관련 문서: data-sources.md | state-management.md


목차

  1. 데이터 로딩 중 Blur 효과 (blur_until_loaded)
  2. 페이지 전환 오버레이 (transition_overlay)
  3. 컴포넌트 생명주기 (lifecycle)

데이터 로딩 중 Blur 효과 (blur_until_loaded)

목적: 데이터 로딩 중 또는 특정 상태에서 컴포넌트에 blur 효과를 적용하여 로딩 상태를 시각적으로 표시합니다.

blur 핵심 원칙

✅ 필수: 컴포넌트에 blur_until_loaded 설정
✅ 지원: boolean 또는 표현식 문자열 (engine-v1.5.0+)
✅ 자동: 페이지 전환 및 초기 로딩 시 blur 효과 적용 (boolean true인 경우)
✅ 자동: 표현식이 truthy일 때 blur 효과 적용 (표현식 문자열인 경우)

사용법

1. Boolean 값 (기본)

데이터 소스 로딩 중 또는 페이지 전환 중 blur 효과 적용:

{
  "id": "users_data_grid",
  "type": "composite",
  "name": "DataGrid",
  "blur_until_loaded": true,
  "props": {
    "data": "{{users.data.data}}"
  }
}

2. 표현식 문자열 (engine-v1.5.0+)

특정 상태 값에 따라 blur 효과 적용:

{
  "id": "menu_form",
  "type": "basic",
  "name": "Form",
  "blur_until_loaded": "{{_global.isSaving}}",
  "props": {
    "id": "menu_form"
  }
}

사용 시나리오:

  • 폼 저장 중 blur 처리: "{{_global.isSaving}}"
  • 데이터 처리 중 blur: "{{_local.isProcessing}}"
  • 조건부 blur: "{{_global.isLoading || _global.isSaving}}"

3. 객체 형태 (engine-v1.6.0+)

특정 데이터 소스에 따라 개별 컴포넌트의 blur 효과를 제어:

{
  "id": "stats_widget",
  "type": "composite",
  "name": "StatCard",
  "blur_until_loaded": {
    "enabled": true,
    "data_sources": "dashboard_stats"
  },
  "props": {
    "data": "{{dashboard_stats.data}}"
  }
}

객체 필드 설명:

필드 타입 필수 설명
enabled boolean ✅ blur 효과 활성화 여부
data_sources string | string[] ❌ blur 해제 조건으로 사용할 데이터 소스 ID

data_sources 사용 패턴:

// 단일 데이터 소스
"blur_until_loaded": {
  "enabled": true,
  "data_sources": "dashboard_stats"
}

// 다중 데이터 소스 (모든 소스가 로드되어야 blur 해제)
"blur_until_loaded": {
  "enabled": true,
  "data_sources": ["users", "roles"]
}

실제 사용 예시 (대시보드 위젯):

{
  "data_sources": [
    { "id": "dashboard_stats", "endpoint": "/api/admin/dashboard/stats", "loading_strategy": "progressive" },
    { "id": "dashboard_resources", "endpoint": "/api/admin/dashboard/resources", "loading_strategy": "progressive" },
    { "id": "dashboard_activities", "endpoint": "/api/admin/dashboard/activities", "loading_strategy": "progressive" }
  ],
  "components": [
    {
      "id": "stats_widget",
      "type": "composite",
      "name": "StatCard",
      "blur_until_loaded": { "enabled": true, "data_sources": "dashboard_stats" },
      "props": { "data": "{{dashboard_stats.data}}" }
    },
    {
      "id": "resources_widget",
      "type": "composite",
      "name": "ResourceCard",
      "blur_until_loaded": { "enabled": true, "data_sources": "dashboard_resources" },
      "props": { "data": "{{dashboard_resources.data}}" }
    },
    {
      "id": "activities_widget",
      "type": "composite",
      "name": "ActivityList",
      "blur_until_loaded": { "enabled": true, "data_sources": "dashboard_activities" },
      "props": { "data": "{{dashboard_activities.data}}" }
    }
  ]
}

이 패턴을 사용하면 각 위젯이 자신의 데이터 소스가 로드될 때 개별적으로 blur가 해제됩니다.

값 타입별 동작

값 타입 예시 동작
true (boolean) blur_until_loaded: true 데이터 소스 로딩 중 또는 페이지 전환 중 blur
false (boolean) blur_until_loaded: false blur 미적용
표현식 문자열 "{{_global.isSaving}}" 표현식 결과가 truthy일 때 blur
객체 (engine-v1.6.0+) { enabled: true, data_sources: "users" } 지정된 데이터 소스가 undefined일 때 blur

Blur 적용 조건

Boolean true인 경우

다음 조건 중 하나라도 충족되면 blur 효과가 적용됩니다:

조건 설명 시나리오
isTransitioning === true 페이지 전환 중 검색, 페이지네이션, 다른 페이지 이동
데이터 소스가 undefined 초기 로딩 시 데이터 미로드 페이지 첫 접속, 새로고침

표현식 문자열인 경우

표현식 결과가 truthy이면 blur 효과가 적용됩니다:

표현식 결과 Blur 적용
"{{_global.isSaving}}" (isSaving = true) true ✓
"{{_global.isSaving}}" (isSaving = false) false ✗
"{{_global.isLoading || _global.isSaving}}" 둘 중 하나라도 true면 true 조건부

객체 형태인 경우 (engine-v1.6.0+)

enabled가 true이고, 지정된 data_sources 중 하나라도 undefined이면 blur 효과가 적용됩니다:

설정 조건 Blur 적용
{ enabled: true, data_sources: "users" } users === undefined ✓
{ enabled: true, data_sources: "users" } users !== undefined ✗
{ enabled: true, data_sources: ["users", "roles"] } 둘 중 하나라도 undefined ✓
{ enabled: false, data_sources: "users" } - ✗ (비활성화)

장점: 여러 위젯이 각각 다른 데이터 소스를 참조할 때, 각 위젯이 독립적으로 blur 해제됩니다.

동작 시나리오

1. 초기 페이지 로딩
   - 데이터 소스(예: users)가 undefined
   - blur 효과 적용 ✓
   - API 응답 수신 후 blur 해제

2. 페이지네이션/검색 (navigate 호출)
   - isTransitioning = true
   - blur 효과 적용 ✓
   - 데이터 로드 완료 후 blur 해제

3. 다른 페이지에서 돌아옴
   - navigate 호출 → isTransitioning = true
   - blur 효과 적용 ✓
   - 데이터 로드 완료 후 blur 해제

4. 데이터 로드 완료 상태
   - isTransitioning = false
   - 모든 데이터 소스 로드됨
   - blur 효과 미적용

실제 사용 예시

{
  "version": "1.0.0",
  "layout_name": "admin_user_list",
  "data_sources": [
    {
      "id": "users",
      "endpoint": "/api/admin/users",
      "loading_strategy": "progressive"
    }
  ],
  "components": [
    {
      "id": "users_data_grid",
      "type": "composite",
      "name": "DataGrid",
      "blur_until_loaded": true,
      "props": {
        "data": "{{users.data.data}}",
        "columns": [...]
      }
    }
  ]
}

적용되는 CSS

/* blur 효과가 적용될 때 */
.opacity-50.blur-sm.transition-all.duration-300.pointer-events-none
  • opacity-50: 50% 투명도
  • blur-sm: 약간의 blur 효과
  • transition-all duration-300: 0.3초 전환 애니메이션
  • pointer-events-none: 클릭 이벤트 비활성화

주의사항

  • ✅ progressive 또는 background loading_strategy와 함께 사용 권장
  • ✅ 데이터 테이블, 그리드 등 데이터 의존 컴포넌트에 적합
  • blocking loading_strategy는 렌더링 전에 데이터를 로드하므로 blur가 보이지 않을 수 있음
  • 레이아웃 JSON 수정 후 DB 동기화 필요 (시더 재실행 또는 관리자 화면에서 업데이트)

페이지 전환 오버레이 (transition_overlay)

엔진 버전: engine-v1.23.0+ 레벨: 레이아웃 (컴포넌트 레벨의 blur_until_loaded와 독립)

목적: 페이지 전환(라우트 변경) 시 순수 DOM 조작으로 오버레이를 표시하여, React 18 비동기 렌더링 중 이전 페이지의 DOM이 잠깐 보이는 stale flash를 방지합니다.

핵심 원칙

✅ 레이아웃 레벨 설정 (베이스 레이아웃에서 한 번 설정하면 모든 하위 레이아웃에 적용)
✅ 순수 DOM 조작 — React 렌더 사이클과 독립적 (동기, 즉시 표시)
✅ 모든 경로(progressive/non-progressive/취소/에러)에서 오버레이 자동 정리
✅ 다크 모드 자동 감지

transition_overlay 사용법

1. 축약 (기본 opaque 스타일)

{
    "transition_overlay": true,
    "components": [...]
}

2. 상세 설정

{
    "transition_overlay": {
        "enabled": true,
        "style": "blur"
    },
    "components": [...]
}

3. 타겟 컨테이너 지정

특정 영역만 오버레이로 가리려면 target에 컴포넌트 ID를 지정합니다.

{
    "transition_overlay": {
        "enabled": true,
        "target": "main_content_area"
    },
    "components": [...]
}
속성 타입 기본값 설명
enabled boolean false 전환 오버레이 활성화
style "opaque" | "blur" | "fade" | "skeleton" | "spinner" "opaque" 오버레이 스타일
target string - 오버레이 대상 요소 ID. 미지정 시 전체 화면

target 지정 시 CSS <style> 태그를 <head>에 주입하여 타겟 요소에 ::after 의사 요소로 오버레이를 생성합니다. 타겟 요소에 position:relative; z-index:0을 설정하여 명시적 stacking context를 생성하므로, ::after가 타겟 내부에 갇히고 헤더/네비게이션 등 형제 요소를 절대 가리지 않습니다. target 미지정 시 document.body에 position:fixed div를 삽입하는 폴백 방식을 사용합니다.

핵심: <style> 태그는 React 렌더 트리 외부(<head>)에 삽입되므로, renderTemplate() 시 DOM 교체에도 소멸하지 않습니다. CSS 규칙은 셀렉터 기반으로 새로 렌더된 DOM에도 자동 적용됩니다.

스타일 옵션

style 효과 설명
"opaque" (기본) 완전 불투명 배경 이전 페이지가 전혀 보이지 않음 (권장)
"blur" backdrop-blur + 반투명 배경 이전 페이지가 흐리게 보임
"fade" 80% 반투명 배경 이전 페이지가 약간 보임
"skeleton" 동적 스켈레톤 UI 레이아웃 구조 미리보기 (engine-v1.24.0+)
"spinner" 커스텀 로딩 컴포넌트 스피너 등 간결한 로딩 표시 (engine-v1.29.0+)

blur_until_loaded와의 차이

구분 transition_overlay blur_until_loaded
레벨 레이아웃 (전체 페이지) 컴포넌트 (개별 위젯)
메커니즘 순수 DOM 조작 React Context + CSS
트리거 레이아웃 전환 시 데이터 소스 로딩 / 표현식 평가
타이밍 React 렌더 이전 (동기) React 렌더 이후 (비동기)
관계 상호 보완 (순차 동작) 상호 보완 (순차 동작)

동작 흐름

사용자 클릭 → handleRouteChange()
  ① layout API fetch → layoutData.transition_overlay 확인
  ② blocking 데이터 로드
  ③ showTransitionOverlay() → DOM에 #g7-transition-overlay 삽입 (동기)
  ④ renderTemplate() → React 비동기 렌더 (오버레이가 stale DOM을 가림)
  ⑤-A progressive 데이터 있음 → 로드 완료 → hideTransitionOverlay()
  ⑤-B progressive 데이터 없음 → renderTemplate 리턴 후 즉시 hideTransitionOverlay()

주의사항

  • transition_overlay 미설정 레이아웃에는 영향 없음 (if 가드)
  • target 지정 시: ::after 의사 요소의 z-index: 2147483647로 타겟 내부 콘텐츠만 덮음
  • target 미지정 시: DOM 오버레이의 z-index: 9999로 전체 화면 덮음
  • pointer-events: none으로 사용자 입력을 차단하지 않음
  • DOM 오버레이 폴백에만 aria-hidden="true" 적용 (CSS 주입 방식은 DOM 요소 없음)

skeleton 스타일 — 동적 스켈레톤 UI (engine-v1.24.0+)

style: "skeleton"을 지정하면, CSS 오버레이 대신 레이아웃 JSON의 컴포넌트 트리를 파싱하여 동적 스켈레톤 UI를 렌더링합니다.

설정 예시

{
    "transition_overlay": {
        "enabled": true,
        "style": "skeleton",
        "target": "main_content_area",
        "skeleton": {
            "component": "PageSkeleton",
            "animation": "pulse",
            "iteration_count": 5
        }
    }
}

skeleton 옵션

속성 타입 필수 기본값 설명
component string ✅ - 스켈레톤 렌더러 컴포넌트 이름 (레지스트리에 등록 필수)
animation string - "pulse" 애니메이션 타입: "pulse" / "wave" / "none"
iteration_count number - 5 iteration 블록의 기본 반복 횟수

동작 원리

1. Navigation → Layout JSON fetch (캐시 or API)
2. style === "skeleton" 감지
3. ComponentRegistry에서 skeleton.component 조회
4. 레이아웃의 components 트리 + options를 props로 전달
5. 스켈레톤 컴포넌트가 target 영역 위에 React로 렌더링
6. Blocking data fetch 완료 → renderTemplate() 호출
7. hideTransitionOverlay() → 스켈레톤 컨테이너 제거

스켈레톤 컴포넌트 Props 인터페이스

엔진이 스켈레톤 컴포넌트에 전달하는 props:

interface SkeletonComponentProps {
    /** 레이아웃 JSON의 components 배열 (전체 컴포넌트 트리) */
    components: LayoutComponent[];
    /** 스켈레톤 옵션 */
    options: {
        animation: 'pulse' | 'wave' | 'none';
        iteration_count: number;
    };
}

스켈레톤 미표시 조건

조건 처리
skeleton.component가 레지스트리에 미등록 opaque 스타일 자동 폴백
target 요소 미존재 opaque 스타일 자동 폴백
모든 blocking 데이터 캐시 히트 스켈레톤 표시 시간이 매우 짧음 (즉시 교체)

템플릿 개발자 가이드

스켈레톤 컴포넌트 구현 시 고려사항:

  • props로 받은 컴포넌트 트리를 재귀 순회하여 스켈레톤 생성
  • 레이아웃 컨테이너(Div, Flex, Grid 등)는 className 유지하며 자식 순회
  • 콘텐츠 컴포넌트(H1, Input, Button 등)는 회색 플레이스홀더로 대체
  • iteration 블록은 iteration_count만큼 반복 생성
  • Modal, Toast 등 팝업 컴포넌트는 빈 Fragment로 처리
  • 접근성: role="status", aria-busy="true" 적용 권장
  • 다크 모드: dark: variant 클래스 적용 필수

spinner 스타일 — 커스텀 로딩 컴포넌트 (engine-v1.29.0+)

style: "spinner"를 지정하면, skeleton 대신 커스텀 로딩 컴포넌트(스피너 등)를 렌더링합니다. skeleton이 레이아웃 트리를 파싱하여 복잡한 스켈레톤 UI를 생성하는 반면, spinner는 단순한 로딩 인디케이터를 표시합니다.

설정 예시

{
    "transition_overlay": {
        "enabled": true,
        "style": "spinner",
        "target": "main_content_area",
        "spinner": {
            "component": "PageLoading"
        }
    }
}

3단계 fallback chain (skeleton과 동일)

{
    "transition_overlay": {
        "enabled": true,
        "style": "spinner",
        "target": "mypage_tab_content",
        "fallback_target": "main_content_area",
        "spinner": {
            "component": "PageLoading"
        }
    }
}

spinner 옵션

속성 타입 필수 기본값 설명
component string - - 로딩 컴포넌트 이름 (레지스트리에 등록 필수). 미지정 시 기본 스피너
text string - - 로딩 텍스트 (컴포넌트의 options.text로 전달)

로딩 컴포넌트 Props 인터페이스

엔진이 로딩 컴포넌트에 전달하는 props:

interface LoadingComponentProps {
    options?: {
        text?: string;
    };
}

동작 원리

1. Navigation → Layout JSON fetch (캐시 or API)
2. style === "spinner" 감지
3. CSS ::after 주입으로 이전 콘텐츠 즉시 가림 (Step 1)
4-A. spinner.component 지정 시: ComponentRegistry에서 조회 → React flushSync 렌더링
4-B. spinner.component 미지정 시: 순수 DOM 기반 기본 CSS 스피너 생성
5. Blocking data fetch 완료 → renderTemplate() 호출
6. hideTransitionOverlay() → 스피너 컨테이너 + CSS 스타일 제거

spinner vs skeleton 비교

구분 spinner skeleton
용도 간결한 로딩 인디케이터 레이아웃 구조 미리보기
컴포넌트 트리 필요 ❌ (옵션만 전달) ✅ (components 배열 전달)
컴포넌트 미지정 시 기본 CSS 스피너 폴백 opaque 스타일 폴백
다국어 처리 컴포넌트 내부에서 G7Core.t() 사용 해당 없음

컴포넌트 생명주기 (lifecycle)

목적: 컴포넌트 마운트/언마운트 시점에 실행할 핸들러를 정의합니다.

actions vs lifecycle

속성 역할 트리거 시점 예시
actions 이벤트 기반 핸들러 click, change, submit 등 버튼 클릭, 폼 제출
lifecycle 생명주기 핸들러 마운트/언마운트 초기 데이터 로드, 정리 작업

구조

{
  "id": "user_form",
  "type": "composite",
  "name": "Form",
  "lifecycle": {
    "onMount": [
      {
        "type": "click",
        "handler": "loadSavedData",
        "target": "{{_local.formId}}"
      }
    ],
    "onUnmount": [
      {
        "type": "click",
        "handler": "cleanup"
      }
    ]
  },
  "actions": [
    {
      "type": "submit",
      "handler": "submitForm"
    }
  ]
}

필드 설명

필드 타입 필수 설명
onMount array ❌ 컴포넌트 마운트 시 실행할 액션 목록
onUnmount array ❌ 컴포넌트 언마운트 시 실행할 액션 목록

각 액션은 actions 배열과 동일한 구조를 따릅니다.

사용 사례

  • onMount: 초기 데이터 로드, 이벤트 리스너 등록, 분석 이벤트 전송
  • onUnmount: 리소스 정리, 이벤트 리스너 해제, 타이머 정리

접근 가능한 데이터

lifecycle 핸들러에서는 다음 데이터에 접근할 수 있습니다:

  • {{route.id}}, {{route.slug}} - URL 라우트 파라미터
  • {{query.page}}, {{query.filter}} - URL 쿼리 파라미터
  • {{_global.xxx}} - 전역 상태
  • {{_local.xxx}} - 컴포넌트 로컬 상태
  • 데이터 소스 결과

실제 사용 예시

{
  "id": "analytics_wrapper",
  "type": "basic",
  "name": "Div",
  "lifecycle": {
    "onMount": [
      {
        "type": "click",
        "handler": "trackPageView",
        "params": {
          "page": "{{route.layout}}",
          "userId": "{{_global.user.id}}"
        }
      }
    ]
  },
  "children": [...]
}

init_actions와의 차이점

구분 init_actions lifecycle.onMount
범위 레이아웃 레벨 컴포넌트 레벨
실행 시점 레이아웃 렌더링 직후 해당 컴포넌트 마운트 시
데이터 접근 route, query, _global, blocking route, query, _global, _local, 모든 데이터
사용 목적 테마 초기화, 전역 설정 컴포넌트별 초기화

주의사항

  • ✅ lifecycle 핸들러는 템플릿의 handlerMap에 등록되어 있어야 함
  • ✅ onMount 액션은 순차적으로 실행됨
  • ✅ onUnmount는 컴포넌트가 DOM에서 제거될 때 실행됨
  • ❌ React Strict Mode에서 onMount가 두 번 호출될 수 있음 (개발 환경)

관련 문서