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

10 KiB

레이아웃 JSON - 슬롯 시스템

메인 문서: layout-json-components.md 관련 문서: components.md


목차

  1. 동적 슬롯 시스템 (slot, SlotContainer)
  2. 컴포넌트 내부 커스텀 렌더링 (component_layout)

동적 슬롯 시스템 (slot, SlotContainer)

목적: 상태에 따라 컴포넌트를 다른 위치(슬롯)로 동적 이동시킵니다.

슬롯 시스템 핵심 원칙

중요: 슬롯 시스템은 레이아웃 상속의 slot과 다릅니다
✅ 레이아웃 상속 slot: 부모 레이아웃의 슬롯을 자식이 채우는 구조
✅ 동적 슬롯 시스템: 상태 변화에 따라 컴포넌트가 다른 슬롯으로 이동
✅ 필수: SlotProvider가 상위에 있어야 동작 (DynamicRenderer가 자동 제공)

동작 원리

  1. slot 속성이 있는 컴포넌트는 원래 위치에서 렌더링되지 않음
  2. 해당 컴포넌트는 SlotContext에 등록됨
  3. SlotContainer가 해당 슬롯 ID의 컴포넌트들을 렌더링
[원래 위치 - hidden]        [SlotContainer - 실제 렌더링]
┌─────────────────┐         ┌─────────────────┐
│ slot="basic"    │ ──────> │ slotId="basic"  │
│ (등록만 함)      │         │ (여기서 렌더링)   │
└─────────────────┘         └─────────────────┘

기본 구조

{
  "children": [
    {
      "id": "slot_registration_area",
      "type": "basic",
      "name": "Div",
      "props": { "className": "hidden" },
      "children": [
        {
          "id": "filter_row_1",
          "type": "basic",
          "name": "Div",
          "slot": "{{condition ? 'slot_a' : 'slot_b'}}",
          "slotOrder": 1,
          "children": [...]
        }
      ]
    },
    {
      "id": "slot_a_container",
      "type": "composite",
      "name": "SlotContainer",
      "props": { "slotId": "slot_a" }
    },
    {
      "id": "slot_b_container",
      "type": "composite",
      "name": "SlotContainer",
      "props": { "slotId": "slot_b" }
    }
  ]
}

필드 설명

필드 타입 필수 설명
slot string ✅ 슬롯 ID (표현식 지원, 예: "basic_filters" 또는 "{{condition ? 'a' : 'b'}}")
slotOrder number ❌ 슬롯 내 정렬 순서 (기본: 등록 순서)

SlotContainer Props

Prop 타입 필수 설명
slotId string ✅ 렌더링할 슬롯 ID
className string ❌ 컨테이너 CSS 클래스

실제 사용 예시: 필터 편집 모드

사용자가 체크박스를 클릭하면 필터가 "기본 필터"와 "상세 필터" 사이를 이동:

{
  "id": "category_filter_row",
  "type": "basic",
  "name": "Div",
  "slot": "{{(_local.visibleFilters || []).includes('category') ? 'basic_filters' : 'detail_filters'}}",
  "slotOrder": 1,
  "props": { "className": "filter-row" },
  "children": [
    {
      "type": "basic",
      "name": "Input",
      "props": {
        "type": "checkbox",
        "className": "{{_local.isEditMode ? 'checkbox mr-2' : 'hidden'}}",
        "checked": "{{(_local.visibleFilters || []).includes('category')}}"
      },
      "actions": [
        {
          "type": "change",
          "handler": "sequence",
          "actions": [
            {
              "handler": "setState",
              "params": {
                "target": "_local",
                "visibleFilters": "{{(_local.visibleFilters || []).includes('category') ? (_local.visibleFilters || []).filter(f => f !== 'category') : [...(_local.visibleFilters || []), 'category']}}"
              }
            },
            {
              "handler": "saveToLocalStorage",
              "params": {
                "key": "g7_product_filter_visible_filters",
                "value": "{{...}}"
              }
            }
          ]
        }
      ]
    },
    {
      "type": "basic",
      "name": "Span",
      "text": "$t:category_label"
    }
  ]
}

SlotContainer 배치:

{
  "id": "basic_filter_section",
  "type": "basic",
  "name": "Div",
  "children": [
    {
      "id": "basic_filters_slot",
      "type": "composite",
      "name": "SlotContainer",
      "props": {
        "slotId": "basic_filters",
        "className": "flex flex-col"
      }
    }
  ]
},
{
  "id": "detail_filter_wrapper",
  "type": "basic",
  "name": "Div",
  "if": "{{_local.showDetailFilter}}",
  "children": [
    {
      "id": "detail_filters_slot",
      "type": "composite",
      "name": "SlotContainer",
      "props": {
        "slotId": "detail_filters",
        "className": "flex flex-col"
      }
    }
  ]
}

동작 흐름

1. 초기 상태 (_local.visibleFilters = ['category', 'date'])
   - category_filter_row → slot="basic_filters"
   - date_filter_row → slot="basic_filters"
   - brand_filter_row → slot="detail_filters"

2. 체크박스 클릭 (category 해제)
   - visibleFilters에서 'category' 제거
   - slot 표현식 재평가
   - category_filter_row → slot="detail_filters"로 변경
   - SlotContainer 자동 업데이트

3. 결과
   - basic_filters: [date_filter_row]
   - detail_filters: [category_filter_row, brand_filter_row]

슬롯 등록 영역 패턴 (권장)

슬롯에 등록할 컴포넌트는 hidden 영역에 배치하는 것을 권장:

{
  "id": "slot_registration_area",
  "type": "basic",
  "name": "Div",
  "props": {
    "className": "hidden",
    "aria-hidden": "true"
  },
  "children": [
    { "id": "comp_1", "slot": "slot_a", "slotOrder": 1, ... },
    { "id": "comp_2", "slot": "slot_b", "slotOrder": 2, ... }
  ]
}

이유:

  • slot 속성이 있는 컴포넌트는 원래 위치에서 null을 반환
  • 하지만 부모 Div 구조는 렌더링되므로 hidden으로 숨김
  • 실제 렌더링은 SlotContainer에서 수행

slotOrder 사용

여러 컴포넌트가 같은 슬롯에 등록될 때 순서 지정:

{
  "id": "category_filter", "slot": "basic_filters", "slotOrder": 1
},
{
  "id": "date_filter", "slot": "basic_filters", "slotOrder": 2
},
{
  "id": "status_filter", "slot": "basic_filters", "slotOrder": 3
}

결과: SlotContainer에서 slotOrder 순으로 렌더링

내부 구현 참고 (개발자용)

슬롯 시스템은 다음 컴포넌트로 구현됩니다:

파일 역할
SlotContext.tsx SlotProvider, useSlotContext 제공
SlotContainer.tsx 슬롯 ID에 등록된 컴포넌트 렌더링
DynamicRenderer.tsx slot 속성 평가, 슬롯 등록 처리

전역 접근:

  • window.__slotContextValue로 슬롯 컨텍스트에 직접 접근 가능
  • React Context 타이밍 이슈 방지를 위해 전역 변수 사용

주의사항

  • ✅ slot 표현식은 {{}} 구문 사용 필수 (동적 슬롯인 경우)
  • ✅ 정적 슬롯은 문자열로 직접 지정 가능: "slot": "basic_filters"
  • ✅ SlotContainer는 해당 slotId가 없으면 빈 상태로 렌더링
  • ✅ slotOrder가 같으면 등록 순서대로 렌더링
  • slot 속성이 있는 컴포넌트는 원래 위치에서 렌더링되지 않음
  • 슬롯 시스템 사용 시 hidden 영역에 등록 컴포넌트 배치 권장
  • ❌ SlotProvider 없이는 슬롯 시스템 동작 안 함 (DynamicRenderer가 자동 제공)

트러블슈팅


컴포넌트 내부 커스텀 렌더링 (component_layout)

목적: 컴포넌트 내부 항목의 렌더링을 커스터마이징합니다 (RichSelect, CardGrid 등).

핵심 원칙

중요: component_layout의 item 컨텍스트는 options/data 배열의 각 항목을 그대로 전달
✅ 필수: {{item.xxx}}로 참조하는 모든 필드가 options/data 배열에 포함되어야 함
❌ 금지: label, value만 포함하고 커스텀 필드 누락

component_layout 기본 구조

{
  "type": "composite",
  "name": "RichSelect",
  "props": {
    "value": "{{_global.selectedFile}}",
    "options": "{{files?.data?.map(f => ({ value: f.name, label: f.name, name: f.name, size: f.size }))}}",
    "placeholder": "$t:common.select_file"
  },
  "component_layout": {
    "item": [
      {
        "type": "basic",
        "name": "Div",
        "props": { "className": "flex flex-col" },
        "children": [
          { "type": "basic", "name": "Span", "text": "{{item.name}}" },
          { "type": "basic", "name": "Span", "text": "{{item.size}}" }
        ]
      }
    ],
    "selected": [
      { "type": "basic", "name": "Span", "text": "{{item.name}}" }
    ]
  }
}

component_layout 지원 컴포넌트

컴포넌트 component_layout 키 설명
RichSelect item, selected 드롭다운 항목 및 선택된 항목 렌더링
CardGrid item 카드 항목 렌더링

component_layout 컨텍스트 변수

변수 타입 설명
item object 현재 항목 객체 (options 배열의 요소)
index number 현재 항목의 인덱스
isSelected boolean 선택 여부 (RichSelect에서 제공)

component_layout 주의사항

  • ✅ options 배열 생성 시 component_layout에서 참조하는 모든 필드 포함 필수
  • ✅ {{item.xxx}} 바인딩은 options 배열의 각 항목에서 xxx 필드를 참조
  • ❌ options에 없는 필드를 {{item.xxx}}로 참조하면 빈 값 표시
  • 기본 label, value만 포함하면 커스텀 렌더링 시 데이터 누락

component_layout 트러블슈팅


관련 문서