KISA 제보 3건(KVE-2026-2010/2011/2018)과 그 동일 계열 형제 결함을 전수 조치하고, 그 과정에서 드러난 두 결함군을 함께 닫는다. - 검증 시점과 연결 시점이 host 를 다르게 읽던 SSRF 통로를 정규화 SSoT 한 곳으로 모았다 - 세션을 여는 지점(2FA 완료·토큰 재발급)이 잠금 검사를 거치지 않아 계정 잠금이 우회됐다 - 인증도 서명도 없는 브라우저 리턴 콜백이 주문 상태를 바꾸던 통로를 4 PG 전부에서 닫고, 소유권을 대조하는 close-report 를 토스에도 신설했다. 그 결과 정리 주체를 잃는 결제창 미완료 주문은 만료 자동취소가 거둔다 - 저장소 A(_local)에만 쓰는 경로가 B 의 값을 조용히 덮던 회귀를 정본 writer 로 닫았다 (engine-v1.63.5). 한 방향만 보던 정적 검사에 반대 방향 축과 양방향 계약 테스트를 더했다 - 레이아웃 JSON 의 같은 객체 중복 키가 앞선 선언을 오류 없이 삼키던 결함군을 닫았다
31 KiB
레이아웃 JSON 스키마
TL;DR (5초 요약)
1. HTML 태그 직접 사용 금지 → 기본 컴포넌트 사용 (Div, Button, Span)
2. 텍스트: text 속성 또는 children 내 Span 사용 (텍스트 직접 배치 금지)
3. 다국어: "$t:key" 형식, 파라미터: "$t:key|param={{value}}"
4. 데이터 바인딩: "{{path.to.data}}" 형식
5. 다크 모드: light/dark 클래스 쌍 필수 (bg-white dark:bg-gray-800)
6. 새 속성 추가 시: UpdateLayoutContentRequest rules에도 추가 필수
7. 권한 제어: permissions 필드로 접근 권한 설정 (AND 로직, 401/403 응답)
8. globalHeaders: 패턴 기반 공통 헤더 자동 주입 (engine-v1.16.0+)
분리된 문서
이 문서는 가독성을 위해 다음과 같이 분리되었습니다:dff
| 문서 | 내용 |
|---|---|
| layout-json.md (현재) | 개요, 필수 필드, 컴포넌트 정의, 텍스트 렌더링 |
| layout-json-features.md | 에러 핸들링, 초기화 액션, 모달 시스템, 액션 시스템 |
| layout-json-components.md | 반복 렌더링(iteration), Blur 효과, 컴포넌트 생명주기 |
| layout-json-inheritance.md | 레이아웃 상속, Partial, 병합 구조, 상속 체인 검증 |
목차
개요
레이아웃 JSON은 화면 구성을 정의한 JSON 파일로, 데이터베이스 template_layouts 테이블에 저장됩니다.
그누보드7 템플릿 시스템에서 레이아웃 JSON은 다음 역할을 합니다:
- 화면에 표시될 컴포넌트 구조 정의
- API 데이터 소스 연결
- 사용자 인터랙션(액션) 처리
- 다국어 및 데이터 바인딩 지원
필수 필드
{
"version": "1.0.0",
"layout_name": "dashboard",
"meta": {
"title": "$t:dashboard.title",
"description": "$t:dashboard.description"
},
"data_sources": [],
"components": []
}
필드 설명:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
version |
string | ✅ | 스키마 버전 (예: "1.0.0") |
layout_name |
string | ✅ | 레이아웃 식별자 (아래 네이밍 규칙 참조) |
meta |
object | ❌ | 메타 정보 (title, description) |
data_sources |
array | ✅ | API 데이터 소스 정의 (각 항목에 label_key $t: 키 — 편집기 데이터 연결 피커 친화 명칭) |
init_actions |
array | ❌ | 초기화 액션 (레이아웃 로드 시 실행) |
modals |
array | ❌ | 모달 컴포넌트 정의 |
scripts |
array | ❌ | 외부 스크립트 동적 로드 (engine-v1.8.0+) |
errorHandling |
object | ❌ | 레이아웃 레벨 에러 핸들링 설정 (engine-v1.6.0+) |
permissions |
string[] | ❌ | 레이아웃 접근 권한 식별자 배열 (engine-v1.15.0+) |
globalHeaders |
array | ❌ | 전역 HTTP 헤더 규칙 배열 (engine-v1.16.0+) |
meta.seo |
object | ❌ | SEO 페이지 생성기 설정 (아래 참조) |
components |
array | ✅ | 컴포넌트 배열 |
한 객체에 같은 키를 두 번 쓰지 않는다
JSON 은 중복 키를 문법 오류로 보지 않는다. 브라우저(JSON.parse)도 서버 등록(json_decode)도
뒤에 온 값이 앞의 값을 덮는다. 그래서 앞에 쓴 선언은 예외도 경고도 없이 사라지는데,
파일에는 그대로 남아 있으므로 코드를 읽는 사람에게는 반영된 것처럼 보인다.
가장 자주 걸리는 자리는 props 다 — 노드가 children 뒤에 "props": {} 를 이미 갖고 있는데
작성자가 노드 앞머리에 "props": { ... } 를 새로 적는 경우다. 노드가 길면 앞머리와 꼬리가
한 화면에 들어오지 않아 눈으로는 발견되지 않고, 그 속성만 화면에 영영 나타나지 않는다.
| ❌ 금지 | ✅ 올바른 사용 |
|---|---|
한 노드에 props(또는 comment·actions 등)를 두 번 선언 |
하나로 합친다 — 값을 추가할 때는 그 노드에 이미 있는 키를 찾아 거기에 넣는다 |
| 노드 앞머리에 키를 추가하기 전에 꼬리를 확인하지 않음 | 노드 전체에서 그 키의 존재를 먼저 확인한다 |
의도적으로 재선언해야 하는 예외는 그 객체의 comment 에
audit:allow layout-json-duplicate-object-key <사유> 를 남긴다 (JSON 은 주석을 담을 수 없으므로
표식 자리가 comment 값이다). 정적 검사가 차단한다.
layout_name 네이밍 규칙
✅ 허용 문자: 영문(a-z, A-Z), 숫자(0-9), 언더스코어(_), 슬래시(/), 하이픈(-), 점(.)
✅ 슬래시 사용: 계층 구조 표현 가능 (예: "board/popular", "mypage/orders/show")
✅ 언더스코어 사용: 플랫 구조 표현 가능 (예: "admin_dashboard", "admin_user_list")
파일 경로와 일치: layout_name은 파일 경로(확장자 제외)와 동일해야 함
예시:
| 파일 경로 | layout_name |
|---|---|
layouts/home.json |
home |
layouts/board/popular.json |
board/popular |
layouts/mypage/orders/show.json |
mypage/orders/show |
layouts/admin_dashboard.json |
admin_dashboard |
상세 문서:
errorHandling: layout-json-features.mdinit_actions: layout-json-features.mdmodals: layout-json-features.mdscripts: layout-json-features.mdpermissions: 아래 섹션globalHeaders: 아래 섹션meta.seo: 아래 섹션
meta.seo (SEO 페이지 생성기)
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| seo.enabled | boolean | X | SEO 렌더링 활성화 (기본: false) |
| seo.data_sources | string[] | X | SEO 시 사전 로드할 data_sources ID |
| seo.extensions | array | X | SEO 변수 제공 확장 목록 (아래 참조) |
| seo.page_type | string | X | 확장 설정 SEO 템플릿 키 결정 (예: "product") |
| seo.toggle_setting | string | X | SEO 활성화 토글 설정 경로 |
| seo.vars | object | X | SEO 변수 선언 — data 소스 변수의 표현식 매핑 |
| seo.priority | number | X | sitemap priority (0.0~1.0) |
| seo.changefreq | string | X | sitemap changefreq (daily/weekly 등) |
| seo.og | object | X | Open Graph 메타태그 |
| seo.structured_data | object | X | JSON-LD 구조화 데이터 |
seo.extensions
SEO 변수를 제공하는 확장(모듈/플러그인)을 선언합니다. 여기에 선언된 확장의 seoVariables() 메서드가 호출되어, page_type에 해당하는 변수가 자동 해석됩니다.
"extensions": [
{ "type": "module", "id": "sirsoft-ecommerce" },
{ "type": "plugin", "id": "sirsoft-payment" }
]
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| type | "module" | "plugin" |
✅ | 확장 타입 |
| id | string | ✅ | 확장 식별자 (예: "sirsoft-ecommerce") |
이 방식은 레이아웃 이름에 dot-notation moduleIdentifier를 포함할 필요 없이, 레이아웃이 어떤 확장의 SEO 변수를 사용하는지 명시적으로 선언합니다.
Partial에 meta.seo 정의 금지 (무시됨) meta.seo 추가/변경 시 UpdateLayoutContentRequest 검증 규칙 동기화 필수 data_sources는 부모-자식 간 합집합 병합 (permissions와 동일 전략)
상세: seo-system.md
레이아웃 권한 (permissions)
버전: engine-v1.15.0+
레이아웃 접근에 필요한 권한을 정의합니다. 권한이 없는 사용자는 레이아웃 서빙 API에서 401/403 응답을 받습니다.
기본 문법
{
"version": "1.0.0",
"layout_name": "admin_dashboard",
"permissions": ["core.dashboard.read"],
"meta": {
"title": "$t:admin.dashboard.title"
},
"components": [...]
}
동작 규칙
| 조건 | 동작 |
|---|---|
permissions 필드 없음 또는 빈 배열 |
공개 레이아웃 (권한 체크 없음) |
permissions에 값이 있음 |
모든 권한 필요 (AND 로직) |
| 비회원 + 권한 없음 | 401 Unauthorized |
| 회원 + 권한 없음 | 403 Forbidden |
| Admin 역할 | 모든 권한 자동 통과 |
권한 식별자 형식
✅ 권한 식별자 규칙: [모듈명].[엔티티].[액션]
✅ 허용 문자: 영문(a-z), 숫자(0-9), 하이픈(-), 언더스코어(_), 점(.)
✅ 예시: core.dashboard.read, sirsoft-ecommerce.products.read
상속 시 권한 병합
부모-자식 레이아웃 상속 시 permissions는 합집합으로 병합됩니다:
부모: { "permissions": ["core.admin.access"] }
자식: { "permissions": ["core.users.read"] }
결과: { "permissions": ["core.admin.access", "core.users.read"] }
- 중복 제거 적용 (
array_unique) - 병합 결과의 모든 권한을 만족해야 접근 가능 (AND 로직)
예시
관리자 레이아웃
{
"version": "1.0.0",
"layout_name": "admin_user_list",
"permissions": ["core.users.read"],
"meta": { "title": "$t:admin.users.title" },
"components": [...]
}
모듈 관리 레이아웃
{
"version": "1.0.0",
"layout_name": "admin_ecommerce_product_list",
"permissions": ["sirsoft-ecommerce.products.read"],
"meta": { "title": "$t:admin.ecommerce.products.title" },
"components": [...]
}
공개 레이아웃 (권한 없음)
{
"version": "1.0.0",
"layout_name": "home",
"meta": { "title": "$t:common.home" },
"components": [...]
}
일괄 업데이트 커맨드
기존 레이아웃에 권한을 일괄 추가하려면 Artisan 커맨드를 사용합니다:
# 시뮬레이션 (실제 수정 없음)
php artisan layout:add-permissions --dry-run
# 실제 실행
php artisan layout:add-permissions
# 특정 레이아웃만
php artisan layout:add-permissions --layout=admin_dashboard
주의사항
권한 식별자는 DB의 permissions 테이블에 존재해야 함
빈 배열 []은 권한 체크 없음과 동일 (공개 레이아웃)
프론트엔드는 401/403 응답 시 적절한 UI 처리 필요 (로그인 페이지 리다이렉트 등)
컴포넌트별 권한 (Component Permissions)
버전: engine-v1.17.0+
개별 컴포넌트에 permissions 속성을 추가하면, 서버 사이드에서 해당 컴포넌트를 JSON에서 제거하여 서빙합니다. 프론트엔드 if 조건부 렌더링과 달리, 권한 없는 컴포넌트의 구조 자체가 클라이언트에 노출되지 않습니다.
기본 문법
{
"type": "basic",
"name": "Div",
"id": "admin_widget",
"permissions": ["core.dashboard.admin-widget"],
"children": [
{ "type": "basic", "name": "H2", "text": "관리자 전용 위젯" }
]
}
동작 규칙
| 조건 | 동작 |
|---|---|
permissions 없음 또는 빈 배열 |
항상 포함 (권한 체크 없음) |
permissions에 값이 있음 |
모든 권한 필요 (AND 로직) |
| 권한 없음 | 컴포넌트 + 하위 children 전체 제거 |
| Admin 역할 | 모든 컴포넌트 자동 통과 |
필터링 대상 영역
| 영역 | 설명 |
|---|---|
components |
메인 컴포넌트 트리 (재귀 children 포함) |
modals[] |
모달 자체에 permissions → 모달 전체 제거 |
modals[].components |
모달 내부 컴포넌트 트리 |
defines |
재사용 컴포넌트 정의 |
상위-하위 중복 선언
각 노드는 자기 permissions만 독립 평가합니다. 별도 병합 정책 없음:
{
"type": "basic", "name": "Div",
"permissions": ["core.users.read"],
"children": [
{
"type": "basic", "name": "Button",
"permissions": ["core.users.delete"],
"text": "삭제"
}
]
}
| 시나리오 | 상위 | 하위 | 결과 |
|---|---|---|---|
| read 없음 | 전체 제거 | 평가 안 됨 | 둘 다 없음 |
| read 있음 + delete 없음 | 통과 | 제거 | Div만 남음 |
| read 있음 + delete 있음 | 통과 | 통과 | 둘 다 남음 |
레이아웃 최상위 permissions와의 차이
| 구분 | 레이아웃 최상위 | 컴포넌트별 |
|---|---|---|
| 범위 | 레이아웃 전체 접근 제어 | 개별 컴포넌트 표시/숨김 |
| 실패 시 | 401/403 에러 응답 | 해당 컴포넌트만 JSON에서 제거 |
| 캐싱 | 캐시 전 체크 | post-cache 필터링 (캐시 영향 없음) |
확장(모듈/플러그인) 컴포넌트
Extension Point 또는 Overlay로 주입된 컴포넌트에도 permissions를 선언할 수 있습니다. 필터링은 applyExtensions() 이후에 수행되므로 자동 처리됩니다.
주의사항
권한 식별자는 DB의 permissions 테이블에 존재해야 함
보안 목적: 프론트엔드 if 조건과 달리 JSON에 컴포넌트 구조가 노출되지 않음
필터링 후 permissions 속성 자체도 제거됨 (클라이언트 노출 방지)
post-cache filtering — 기존 캐시 구조에 영향 없음
전역 헤더 (globalHeaders)
버전: engine-v1.16.0+
레이아웃에서 정의한 모든 API 호출(data_sources, apiCall 핸들러)에 공통 헤더를 자동으로 주입합니다.
기본 문법
{
"version": "1.0.0",
"layout_name": "shop_cart",
"globalHeaders": [
{
"pattern": "*",
"headers": { "X-Request-Source": "user-template" }
},
{
"pattern": "/api/modules/sirsoft-ecommerce/*",
"headers": { "X-Cart-Key": "{{_global.cartKey}}" }
}
],
"components": [...]
}
globalHeaders 배열 구조
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
pattern |
string | ✅ | 엔드포인트 매칭 패턴 (*, /api/shop/* 등) |
headers |
object | ✅ | 헤더 키-값 쌍 (표현식 지원) |
패턴 매칭 규칙
| 패턴 | 설명 | 매칭 예시 |
|---|---|---|
* |
모든 API | 전체 적용 |
/api/modules/sirsoft-ecommerce/* |
ecommerce 모듈 API | /api/modules/sirsoft-ecommerce/cart, /api/modules/sirsoft-ecommerce/products/123 |
/api/shop/* |
shop prefix API | /api/shop/products, /api/shop/categories |
/api/cart |
정확히 일치 | /api/cart만 (하위 경로 제외) |
표현식 지원
헤더 값에서 데이터 바인딩 표현식을 사용할 수 있습니다:
{
"globalHeaders": [
{
"pattern": "/api/modules/sirsoft-ecommerce/*",
"headers": {
"X-Cart-Key": "{{_global.cartKey}}",
"X-User-Locale": "{{_global.locale ?? 'ko'}}"
}
}
]
}
지원되는 컨텍스트:
| 컨텍스트 | 설명 |
|---|---|
{{_global.xxx}} |
전역 상태 값 |
{{_local.xxx}} |
로컬 상태 값 |
{{route.xxx}} |
라우트 파라미터 |
{{query.xxx}} |
쿼리 파라미터 |
헤더 우선순위
낮음 ← globalHeaders ← source.headers / params.headers → 높음
개별 API의 headers 속성이 globalHeaders의 동일한 키를 덮어씁니다:
{
"globalHeaders": [
{ "pattern": "*", "headers": { "X-Custom": "global-value" } }
],
"data_sources": [
{
"id": "products",
"endpoint": "/api/products",
"headers": { "X-Custom": "source-value" } // globalHeaders보다 우선
}
]
}
적용 범위
| 적용 대상 | 설명 |
|---|---|
data_sources |
레이아웃의 data_sources 배열에서 정의한 API |
apiCall 핸들러 |
actions에서 handler: "apiCall"로 호출하는 API |
refetchDataSource 핸들러 |
DataSourceManager를 통해 호출 (자동 적용) |
상속 시 병합
부모-자식 레이아웃 상속 시 globalHeaders는 pattern 기준으로 병합됩니다:
부모: [{ pattern: "*", headers: { "X-Template": "basic" } }]
자식: [{ pattern: "*", headers: { "X-Page": "cart" } }]
결과: [{ pattern: "*", headers: { "X-Template": "basic", "X-Page": "cart" } }]
- 동일 pattern: headers가 병합됨 (자식이 동일 키 덮어씀)
- 다른 pattern: 별도로 유지
상세 문서: layout-json-inheritance.md
주의사항
표현식 값이 빈 문자열/undefined/null이면 해당 헤더는 전송되지 않음
_isolated 컨텍스트는 지원하지 않음 (data_sources fetch 시점에 격리 스코프 미확정)
시스템 내부 API(레이아웃 로드, 번역 파일 등)에는 적용되지 않음
새 속성 추가 규정
주의: 레이아웃 JSON에 새로운 최상위 속성 추가 시 백엔드 작업 필수
필수: UpdateLayoutContentRequest rules()에도 새 속성 추가
프론트엔드만 수정하고 백엔드 누락 시 저장 후 데이터 사라짐
왜 필요한가?
Laravel의 FormRequest::validated() 메서드는 rules()에 정의된 필드만 반환합니다. 따라서:
- 레이아웃 JSON에 새 속성을 추가해도
UpdateLayoutContentRequest::rules()에 없으면- 저장 시 해당 속성이 삭제됩니다
새 속성 추가 체크리스트
□ 프론트엔드: 레이아웃 JSON에 새 속성 추가
□ 백엔드: UpdateLayoutContentRequest::rules()에 규칙 추가
□ 백엔드: UpdateLayoutContentRequest::messages()에 에러 메시지 추가
□ 다국어: lang/ko/validation.php, lang/en/validation.php 메시지 추가
□ 테스트: 해당 속성이 저장되는지 테스트 추가
파일 위치
- FormRequest:
app/Http/Requests/Layout/UpdateLayoutContentRequest.php - 다국어:
lang/ko/validation.php,lang/en/validation.php - 테스트:
tests/Feature/Api/Admin/LayoutControllerTest.php
예시: 새 속성 custom_field 추가
// UpdateLayoutContentRequest::rules()
'content.custom_field' => ['nullable', 'array'],
// UpdateLayoutContentRequest::messages()
'content.custom_field.array' => __('validation.layout.custom_field.array'),
컴포넌트 정의
{
"id": "unique_id",
"type": "basic|composite|layout",
"name": "ComponentName",
"props": {
"title": "$t:dashboard.title",
"value": "{{data.field}}"
},
"children": [],
"actions": [
{
"type": "click",
"handler": "navigate",
"target": "/path"
}
],
"data_binding": {
"value": "data.field"
}
}
필드 설명:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
id |
string | ✅ | 컴포넌트 고유 ID |
type |
string | ✅ | 컴포넌트 타입 (basic/composite/layout) |
name |
string | ✅ | 컴포넌트 이름 (components.json에 등록된 이름) |
comment |
string | ✅ | 시안 추적 코드 및 역할 설명 (예: "F-002: 상품명 검색 필드"). 공개 서빙 응답에서는 제거됨 (아래 참조) |
props |
object | ❌ | 컴포넌트에 전달할 props |
text |
string | ❌ | 텍스트 콘텐츠 (최우선 렌더링) |
children |
array | ❌ | 자식 컴포넌트 배열 (레이아웃/집합 컴포넌트에서 사용) |
iteration |
object | ❌ | 반복 렌더링 설정 (source, item_var, index_var) |
actions |
array | ❌ | 액션 핸들러 정의 (클릭, 키보드, 폼 제출 등) |
lifecycle |
object | ❌ | 생명주기 핸들러 (onMount, onUnmount) |
data_binding |
object | ❌ | 데이터 바인딩 정의 |
blur_until_loaded |
boolean | string | ❌ | 데이터 로딩 중 blur 효과 적용 (boolean 또는 표현식 문자열) |
component_layout |
object | ❌ | 컴포넌트 내부 커스텀 렌더링 레이아웃 정의 (RichSelect 등에서 사용) |
isolatedState |
object | ❌ | 격리된 상태 초기값 정의 (engine-v1.14.0+) |
isolatedScopeId |
string | ❌ | 격리된 스코프 식별자 (engine-v1.14.0+) |
autoBinding |
boolean | ❌ | false로 설정 시 해당 필드의 폼 자동 바인딩을 명시적으로 비활성화. 커스텀 핸들러나 G7Core API로 상태 관리 시 사용 (engine-v1.17.6+) |
상세 문서:
iteration: layout-json-components.mdblur_until_loaded: layout-json-components.mdlifecycle: layout-json-components.mdactions: layout-json-features.mdisolatedState: 아래 섹션
comment / _comment — 공개 서빙 응답에서 제거
comment / _comment 는 편집기·개발자용 설명 주석이며 런타임 렌더러는 참조하지 않는다. 공개 레이아웃 서빙(GET /api/layouts/{template}/{layout}.json) 응답에서는 전송 크기 절감을 위해 이 두 키가 노드 트리 전체에서 재귀적으로 제거된다(LayoutService::stripDeveloperComments).
- 편집 모드 서빙(
?with_source_meta=1, 편집 권한 필요)에서는 편집기가comment를 편집 가능한 속성으로 노출하므로 보존된다. 공개/편집 응답은 별도 캐시 키로 분리되어 있어 조건부 제거가 안전하다. - 따라서
comment는 편집기·저장본에서는 유지되고, 일반 사용자에게 서빙되는 응답에서만 사라진다. 레이아웃 로직이comment값에 의존해서는 안 된다.
격리된 상태 (isolatedState)
버전: engine-v1.14.0+
특정 컴포넌트 영역 내에서만 유효한 격리된 상태를 정의합니다. 해당 영역의 상태 변경 시 전체 레이아웃이 아닌 격리된 영역만 리렌더링되어 성능이 향상됩니다.
기본 문법
{
"type": "Div",
"isolatedState": {
"selectedCategories": [],
"currentStep": 1,
"isExpanded": false
},
"isolatedScopeId": "category-selector",
"children": [...]
}
속성 설명
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
isolatedState |
object | ✅ | 격리된 상태의 초기값을 정의하는 객체 |
isolatedScopeId |
string | ❌ | 외부에서 접근할 때 사용하는 스코프 식별자 |
상태 접근 방법
격리된 상태는 _isolated 접두사로 접근합니다:
{
"type": "basic",
"name": "Button",
"props": {
"disabled": "{{_isolated.currentStep === 1}}"
},
"text": "{{_isolated.selectedCategories.length}}개 선택됨"
}
상태 업데이트
setState 핸들러에서 target: "isolated" 옵션을 사용합니다:
{
"type": "click",
"handler": "setState",
"params": {
"target": "isolated",
"currentStep": "{{_isolated.currentStep + 1}}"
}
}
사용 시점
| 상황 | 권장 상태 | 이유 |
|---|---|---|
| 빈번한 사용자 인터랙션 | _isolated |
리렌더링 최소화 |
| 독립적인 UI 영역 | _isolated |
다른 영역과 무관한 상태 |
| 레이아웃 전체에서 공유 필요 | _local |
여러 컴포넌트에서 접근 |
| 페이지 이동 후에도 유지 필요 | _global |
앱 전역 상태 |
라이프사이클
1. 생성: isolatedState 속성이 있는 컴포넌트 마운트 시
2. 업데이트: setState target:"isolated" 또는 G7Core.state.setIsolated()
3. 소멸: 해당 컴포넌트 언마운트 시
4. 페이지 이동: 초기화됨 (언마운트 → 재마운트)
주의사항
isolatedState 외부에서 target: "isolated" 사용 시 → _local로 폴백 + 경고 로그
isolatedScopeId 중복 시 → 마지막 등록된 스코프 우선 + 경고 로그
중첩 isolatedState 시 → 가장 가까운 스코프 사용
관련 문서
텍스트 렌더링
중요: 컴포넌트에 텍스트를 표시할 때는 text 속성을 사용해야 합니다.
필수: text 속성 사용 (DynamicRenderer가 최우선 처리)
필수: children 배열 사용 (하위 컴포넌트 포함)
주의: props.children 사용 불가 (무시됨)
핵심 원칙
그누보드7 템플릿 엔진의 DynamicRenderer는 다음 우선순위로 렌더링합니다:
text속성 (최우선) - 텍스트 콘텐츠 표시children배열 - 하위 컴포넌트 렌더링props- 일반 속성 전달
올바른 패턴
{
"id": "page_title",
"type": "basic",
"name": "H1",
"props": {
"className": "text-2xl font-bold"
},
"text": "대시보드"
}
{
"id": "logo_text",
"type": "basic",
"name": "Span",
"props": {
"className": "text-white font-bold"
},
"text": "G7"
}
잘못된 패턴
{
"id": "page_title",
"type": "basic",
"name": "H1",
"props": {
"className": "text-2xl font-bold",
"children": "대시보드" // ❌ props.children은 무시됨
}
}
다국어 지원
text 속성에서도 다국어 키를 사용할 수 있습니다:
{
"id": "page_title",
"type": "basic",
"name": "H1",
"props": {
"className": "text-2xl font-bold"
},
"text": "$t:dashboard.title"
}
데이터 바인딩 지원
text 속성에서 데이터 바인딩도 가능합니다:
{
"id": "user_name",
"type": "basic",
"name": "Span",
"text": "{{user.name}}"
}
children 배열과의 차이
// ✅ text 속성: 단순 텍스트 표시
{
"id": "title",
"type": "basic",
"name": "H1",
"text": "제목"
}
// ✅ children 배열: 하위 컴포넌트 포함
{
"id": "card",
"type": "composite",
"name": "Card",
"children": [
{
"id": "card_title",
"type": "basic",
"name": "H2",
"text": "카드 제목"
},
{
"id": "card_content",
"type": "basic",
"name": "P",
"text": "카드 내용"
}
]
}
DynamicRenderer 동작 방식
// DynamicRenderer.tsx (코어 엔진)
const renderChildren = useMemo(() => {
// 1. text 속성이 있으면 최우선으로 사용
if (componentDef.text !== undefined) {
return componentDef.text;
}
// 2. children이 없으면 null
if (!componentDef.children || componentDef.children.length === 0) {
return null;
}
// 3. children 배열 렌더링
return componentDef.children.map((childDef, index) => {
// ...
});
}, [componentDef.text, componentDef.children]);
주의사항
- ❌
props.children은 DynamicRenderer에서 완전히 무시됩니다 - ✅ React 컴포넌트 개발 시
childrenprop은 정상 작동 (TSX 내부) - ✅ 레이아웃 JSON에서는
text속성과children배열만 사용
실제 사용 예시
{
"id": "sidebar_logo",
"type": "basic",
"name": "Div",
"props": {
"className": "flex items-center gap-3"
},
"children": [
{
"id": "logo_circle",
"type": "basic",
"name": "Div",
"props": {
"className": "flex items-center justify-center w-10 h-10 bg-black rounded-full"
},
"children": [
{
"id": "logo_text",
"type": "basic",
"name": "Span",
"props": {
"className": "text-white font-bold text-sm"
},
"text": "G7"
}
]
},
{
"id": "app_name",
"type": "basic",
"name": "Span",
"props": {
"className": "font-semibold text-lg"
},
"text": "G7"
}
]
}
Extension Point 데이터 전달 (engine-v1.15.0+, callbacks: engine-v1.28.0+)
extension_point에서 주입되는 컴포넌트에 데이터와 콜백을 전달합니다.
props: 데이터 전달용 — 표현식이 평가되어 전달됨 (resolveObject재귀 평가)callbacks: 액션 객체 전달용 — 평가 없이 그대로 전달 (ActionDispatcher가 실행 시점에 평가)
호스트 레이아웃 (extension_point 정의)
{
"type": "extension_point",
"name": "address_search_slot",
"props": {
"productId": "{{route.id}}",
"readOnlyFields": ["zipcode", "address"]
},
"callbacks": {
"onAddressSelect": {
"handler": "setState",
"params": { "target": "local", "form.zipcode": "{{$event.zipcode}}" }
}
}
}
플러그인 extension JSON (주입되는 컴포넌트)
{
"handler": "myPlugin.doSomething",
"params": {
"id": "{{extensionPointProps.productId}}",
"fields": "{{extensionPointProps.readOnlyFields}}",
"callbackAction": "{{extensionPointCallbacks.onAddressSelect}}"
}
}
{{extensionPointProps.xxx}}: 호스트의props에서 전달된 데이터 (표현식 평가됨){{extensionPointCallbacks.xxx}}: 호스트의callbacks에서 전달된 액션 객체 (그대로 전달)
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
props |
object | ❌ | 주입 컴포넌트에 전달할 데이터 (표현식 평가) |
callbacks |
object | ❌ | 주입 컴포넌트에 전달할 액션 객체 (평가 없이 전달) |
전달된 값은 주입 컴포넌트와 그 자손 전체에서 참조할 수 있습니다.
검색 봇용 화면에서는 extensionPointProps 만 해석되고 extensionPointCallbacks 는 해석되지 않습니다. 색인되어야 할 내용은 props 로 전달하세요. 사용자 작성 콘텐츠를 넘길 때는 평문/HTML 판정 값(isHtml)을 함께 전달합니다 — 두 화면 모두 이 값에 따라 이스케이프하거나 위험 요소를 제거한 뒤 출력합니다. 봇 화면의 노드 문법 지원 범위는 seo-system.md "SEO 렌더러 지원 노드 키", 정화 규칙은 같은 문서의 "봇 화면의 HTML 정화" 를 참조하세요.
Deprecated 속성
하위 호환성을 위해 별칭이 유지되는 속성입니다. 새 코드에서는 권장 속성을 사용하세요.
| Deprecated | 권장 속성 | 호환성 | 비고 |
|---|---|---|---|
init_actions |
initActions |
별칭 유지 (engine-v1.0.0+) | 카멜케이스 통일 |
state |
initLocal |
별칭 유지 (engine-v1.11.0+) | initLocal이 더 명시적 |
auth_required |
auth_mode |
별칭 유지 (engine-v1.0.0+) | auth_mode가 더 세밀한 제어 지원 |
기존 레이아웃에서 deprecated 속성 사용 시 정상 동작 (별칭 유지)
✅ 신규 레이아웃에서는 권장 속성만 사용
✅ 리팩토링 시 권장 속성으로 변경 권장
관련 문서
- 컴포넌트 개발 규칙 - basic, composite, layout 컴포넌트
- 데이터 바인딩 -
{{}}표현식,$t:다국어 - 데이터 소스 - API 데이터 자동 fetch
- 상태 관리 -
_global전역 상태