10 KiB
10 KiB
레이아웃 JSON - 슬롯 시스템
메인 문서: layout-json-components.md 관련 문서: components.md
목차
동적 슬롯 시스템 (slot, SlotContainer)
목적: 상태에 따라 컴포넌트를 다른 위치(슬롯)로 동적 이동시킵니다.
슬롯 시스템 핵심 원칙
중요: 슬롯 시스템은 레이아웃 상속의 slot과 다릅니다
✅ 레이아웃 상속 slot: 부모 레이아웃의 슬롯을 자식이 채우는 구조
✅ 동적 슬롯 시스템: 상태 변화에 따라 컴포넌트가 다른 슬롯으로 이동
✅ 필수: SlotProvider가 상위에 있어야 동작 (DynamicRenderer가 자동 제공)
동작 원리
slot속성이 있는 컴포넌트는 원래 위치에서 렌더링되지 않음- 해당 컴포넌트는 SlotContext에 등록됨
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만 포함하면 커스텀 렌더링 시 데이터 누락