21 KiB
데이터 바인딩 및 표현식
관련 문서: index.md | layout-json.md | data-sources.md
TL;DR (5초 요약)
1. API 데이터: {{user.name}}, URL 파라미터: {{route.id}}
2. 상태 접근: {{_global.xxx}}, {{_local.xxx}}, {{_isolated.xxx}}
3. Optional Chaining: {{user?.profile?.name ?? '기본값'}}
4. 파이프 함수: {{created_at | date}}, {{name | truncate(50)}}
5. $get 헬퍼: {{$get(obj, ['key1', 'key2'], fallback)}}
분리된 문서
이 문서는 가독성을 위해 다음과 같이 분리되었습니다:
| 문서 | 내용 |
|---|---|
| data-binding.md (현재) | 기본 문법, 데이터 바인딩 표현식, Optional Chaining, 반복 컨텍스트 |
| data-binding-i18n.md | $localized() 헬퍼, 다국어 처리, 지연 번역, 모듈/플러그인 다국어 병합, 컴포넌트 다국어 처리 |
| rawMarkers.ts | raw: 마커 — 번역 면제 바인딩 ({{raw:expression}}) |
목차
- 기본 문법
- 데이터 바인딩 표현식 (engine-v1.1.0+)
- Optional Chaining & Nullish Coalescing
- 파이프 함수 (engine-v1.9.0+)
- $get 헬퍼 함수 (engine-v1.9.0+)
- $switch 표현식 (engine-v1.9.0+)
- 반복 컨텍스트에서의 표현식
- text 속성 복합 바인딩
기본 문법
레이아웃 JSON에서 동적 데이터를 참조할 때 사용하는 문법입니다.
데이터 바인딩 유형
| 문법 | 설명 | 예시 |
|---|---|---|
{{data.field}} |
API 데이터 바인딩 | {{user.name}} |
{{route.param}} |
URL 파라미터 | /users/:id의 {{route.id}} |
{{query.key}} |
쿼리 스트링 | ?category=electronics의 {{query.category}} |
{{state.value}} |
컴포넌트 상태 | {{state.isOpen}} |
{{_global.property}} |
전역 상태 (engine-v1.1.0+) | {{_global.sidebarOpen}} |
{{_local.property}} |
로컬 상태 | {{_local.formData}} |
{{_isolated.property}} |
격리된 상태 (engine-v1.14.0+) | {{_isolated.selectedItems}} |
{{raw:expression}} |
번역 면제 바인딩 (engine-v1.27.0+) | {{raw:item.jsonData}} |
기본 예시
{
"props": {
"title": "{{user.name}}",
"email": "{{user.email}}",
"userId": "{{route.id}}",
"category": "{{query.category_id}}",
"sidebarOpen": "{{_global.sidebarOpen}}",
"formData": "{{_local.form}}",
"selectedCount": "{{_isolated.selectedItems?.length ?? 0}}"
}
}
격리된 상태 바인딩 (engine-v1.14.0+)
_isolated 상태는 isolatedState 속성이 정의된 컴포넌트 내에서만 접근 가능합니다.
{
"type": "Div",
"isolatedState": { "selectedId": null, "items": [] },
"children": [
{
"type": "basic",
"name": "Span",
"text": "선택됨: {{_isolated.selectedId ?? '없음'}}"
},
{
"type": "basic",
"name": "Div",
"iteration": { "source": "{{_isolated.items}}", "item": "item" },
"props": {
"className": "{{_isolated.selectedId === item.id ? 'bg-blue-100' : 'bg-white'}}"
}
}
]
}
데이터 바인딩 표현식 (engine-v1.1.0+)
DataBindingEngine은 {{}} 내부에서 다양한 표현식을 지원합니다.
1. 변수 참조
{
"text": "{{user.name}}",
"count": "{{products.total}}"
}
2. 삼항 연산자
{
"className": "{{isActive ? 'bg-blue-500' : 'bg-gray-500'}}",
"text": "{{count > 0 ? count + ' items' : 'No items'}}",
"props": {
"variant": "{{user.isPremium ? 'premium' : 'standard'}}"
}
}
3. 논리 연산자
{
// AND 연산자
"if": "{{isAdmin && hasPermission}}",
// OR 연산자
"className": "{{isLoggedIn || isGuest ? 'visible' : 'hidden'}}",
// NOT 연산자
"text": "{{!isEmpty ? 'Has data' : 'Empty'}}"
}
4. 비교 연산자
{
"if": "{{count >= 10}}",
"className": "{{price > 100 ? 'expensive' : 'affordable'}}",
"text": "{{status == 'active' ? 'Active' : 'Inactive'}}",
"disabled": "{{stock <= 0}}"
}
지원되는 비교 연산자:
| 연산자 | 설명 |
|---|---|
== |
같음 |
!= |
다름 |
> |
크다 |
< |
작다 |
>= |
크거나 같다 |
<= |
작거나 같다 |
5. 리터럴 값
{
// 문자열 리터럴
"className": "{{isOpen ? 'translate-x-0' : '-translate-x-full'}}",
// 숫자 리터럴
"count": "{{count || 0}}",
// boolean 리터럴
"enabled": "{{status == 'ready' ? true : false}}",
// null 리터럴
"value": "{{data || null}}"
}
6. 복합 표현식
{
// 동적 CSS 클래스 생성
"className": "{{_global.sidebarOpen ? 'translate-x-0' : '-translate-x-full'}} fixed inset-y-0 left-0 z-50 w-64 transition-transform lg:relative lg:translate-x-0",
// 조건부 텍스트
"text": "{{user.isOnline && user.status == 'active' ? 'Online' : 'Offline'}}",
// 중첩 삼항 연산자
"variant": "{{score >= 90 ? 'excellent' : score >= 70 ? 'good' : 'poor'}}"
}
사용 가능 위치
props.*: 모든 props 속성className: 동적 CSS 클래스text: 텍스트 내용if: 조건부 렌더링actions[].payload.*: 액션 payload
if 표현식의 {{}} 위치 (CRITICAL, engine-v1.47.1 명문화)
if 값은 ConditionEvaluator.evaluateStringCondition 이 평가한다. 평가 분기는 다음과 같다:
| 형태 | 평가 방식 | 결과 |
|---|---|---|
"{{x === 'y'}}" (전체를 {{}} 한 쌍) |
extractSingleBinding 추출 → evaluateExpression (식 평가) |
✅ 의도대로 동작 |
"{{x}} === 'y'" (보간 + 비교) |
resolveBindings 로 {{x}} 만 치환 → 결과 문자열을 Boolean() |
❌ x 값과 무관하게 비-빈 문자열 → 항상 truthy |
"x === 'y'" ({{}} 없음) |
resolveBindings 가 보간 대상 없음을 감지 → 원본 문자열 그대로 → Boolean() |
❌ 항상 truthy |
규칙: 비교(===, !==, <, >), 논리(&&, ||), 옵셔널체이닝(?.), 삼항(? :) 등 연산자가 포함된 식을 if 에 쓸 때는 반드시 식 전체를 {{...}} 한 쌍 안에 넣는다.
// ✅ 올바른 사용
{ "if": "{{query?.reason === 'session_expired'}}" }
{ "if": "{{user?.role === 'admin' && user?.active}}" }
{ "if": "{{count > 0 ? true : false}}" }
// ❌ 잘못된 사용 — 항상 truthy
{ "if": "{{query?.reason}} === 'session_expired'" }
{ "if": "query?.reason === 'session_expired'" }
// ❌ 잘못된 경로 — 항상 false (G7 컨텍스트는 query 가 root 직접 노출, route.query 아님)
{ "if": "{{route?.query?.reason === 'session_expired'}}" }
G7 컨텍스트의 query 위치: URL query string 은
query.xxx또는query?.xxx로 root 컨텍스트에 직접 노출된다 (route.xxx는 path params 만 — 예:/users/:id의route.id). 다른 모든 레이아웃 사례 (query.tab,query.error,query.q,query.clone_from등) 와 일관됨.
단순 변수 참조는 형태에 상관없이 동작 ({{user.isAdmin}} ↔ 식 평가 분기로 정확히 boolean 캐스팅됨). 위 규칙은 연산자 포함 식에만 해당.
Optional Chaining & Nullish Coalescing (engine-v1.1.0+)
데이터 로드 전/후 안전한 접근을 위해 Optional Chaining(?.)과 Nullish Coalescing(??)을 지원합니다.
{
// Optional Chaining - null/undefined 안전 접근
// 데이터가 없어도 에러 발생 안 함
"text": "{{users?.data?.pagination?.total}}",
// Nullish Coalescing - 기본값 설정
// null 또는 undefined인 경우 기본값 반환
"count": "{{users?.data?.total ?? 0}}",
// 복합 사용 - 안전한 접근 + 기본값
"info": "{{products?.items?.length ?? 0}} items"
}
사용 사례 - API 데이터 로드 전후 처리
{
// 데이터 로드 전: users가 undefined → 0 표시
// 데이터 로드 후: users.data.pagination.total 값 표시
"text": "$t:admin.users.pagination_info|total={{users?.data?.pagination?.total ?? 0}}|from={{users?.data?.pagination?.from ?? 0}}|to={{users?.data?.pagination?.to ?? 0}}"
}
동작 원리
- 데이터 소스가 로드되기 전: 변수가
undefined로 처리되어??이후의 기본값 반환 - 데이터 소스가 로드된 후: 실제 값으로 자동 업데이트 (리렌더링)
주의사항
✅ API 데이터 접근 시 항상 `?.`와 `??` 함께 사용 권장
✅ 초기 렌더링 시 에러 방지를 위해 기본값 필수
✅ 간결한 표현식 사용 권장 (한 줄)
✅ 복잡한 로직은 API에서 처리 후 결과만 바인딩
깊은 중첩 경로에서도 안전하게 작동
에러 발생 시 빈 문자열 또는 기본값 반환
과도한 중첩은 가독성 저하
실제 사용 사례 - 반응형 레이아웃
{
"id": "mobile_sidebar",
"type": "basic",
"name": "Div",
"props": {
"className": "{{_global.sidebarOpen ? 'translate-x-0' : '-translate-x-full'}} fixed inset-y-0 left-0 z-50 w-64 transition-transform duration-300 lg:relative lg:translate-x-0 bg-white"
}
}
파이프 함수 (engine-v1.9.0+)
파이프 함수를 사용하면 데이터를 체이닝 방식으로 변환할 수 있습니다.
기본 문법
{
"text": "{{created_at | date}}",
"text": "{{product.name | truncate(50)}}",
"text": "{{description | stripHtml | truncate(100)}}"
}
내장 파이프 함수
| 파이프 | 설명 | 예시 |
|---|---|---|
default(value) |
null/undefined/빈값 시 대체 | {{name | default('Unknown')}} |
fallback(value) |
null/undefined 시 대체 | {{data | fallback([])}} |
date(format) |
날짜 포맷 | {{created_at | date('YYYY-MM-DD')}} |
datetime(format) |
날짜시간 포맷 | {{created_at | datetime}} |
relativeTime |
상대 시간 | {{created_at | relativeTime}} → "3일 전" |
number(decimals) |
숫자 포맷 | {{price | number(2)}} |
truncate(len, suffix) |
문자열 자르기 | {{text | truncate(100, '...')}} |
stripHtml |
HTML 태그 제거 | {{content | stripHtml}} |
json |
JSON 문자열 변환 | {{object | json}} |
keys |
객체 키 배열 | {{object | keys}} |
values |
객체 값 배열 | {{object | values}} |
first |
배열 첫 요소 | {{items | first}} |
last |
배열 마지막 요소 | {{items | last}} |
join(separator) |
배열 문자열 결합 | {{tags | join(', ')}} |
localized |
다국어 객체 → 현재 로케일 값 | {{title | localized}} |
파이프 체이닝
여러 파이프를 연결하여 순차적으로 변환할 수 있습니다:
{
"text": "{{content | stripHtml | truncate(100) | default('내용 없음')}}"
}
주의사항
✅ 파이프는 순수 함수로 동작 (side-effect 없음)
✅ 단일 | = 파이프, || = OR 연산자 (혼동 주의)
✅ 파이프 인자는 괄호 안에 작성
배열 파이프(first, last, join)는 배열 입력 필수
잘못된 입력 시 fallback 값 반환
$get 헬퍼 함수 (engine-v1.9.0+)
동적 키를 포함한 깊은 객체 경로 접근과 폴백 값을 지원합니다.
기본 문법
{
"text": "{{$get(object, path, fallback)}}"
}
object: 접근할 객체path: 문자열 또는 문자열 배열 형태의 경로fallback: 값이 없을 때 반환할 기본값 (선택)
사용 예시
다중 통화 가격 접근:
{
"text": "{{$get(product.multi_currency_price, [currency, 'formatted'], product.price_formatted)}}"
}
다국어 객체 접근:
{
"text": "{{$get(product.name, _global.locale, product.name.ko)}}"
}
중첩 설정 접근:
{
"value": "{{$get(_global.settings, ['drivers', 'mail', 'host'], 'localhost')}}"
}
동적 키 접근:
{
"price": "{{$get(prices, [selectedPlan, 'monthly'], 0)}}"
}
비교: 기존 방식 vs $get
기존 (복잡):
{
"text": "{{item.multi_currency_unit_price[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.unit_price_formatted}}"
}
개선 (단순):
{
"text": "{{$get(item.multi_currency_unit_price, [_global.preferredCurrency ?? 'KRW', 'formatted'], item.unit_price_formatted)}}"
}
주의사항
✅ 순수 함수로 동작 (입력 객체 변경 없음)
✅ null/undefined 경로에서 안전하게 폴백 반환
✅ 배열 형태 경로로 동적 키 접근 가능
경로가 빈 배열이면 원본 객체 반환
$switch 표현식 (engine-v1.9.0+)
다단계 중첩 삼항 연산자를 선언적 switch-case 패턴으로 대체합니다.
기본 문법
{
"icon": {
"$switch": "{{tab}}",
"$cases": {
"products": "shopping-bag",
"contents": "book-open",
"policies": "file-check"
},
"$default": "file-text"
}
}
필드 설명
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
$switch |
string | ✅ | 평가할 표현식 |
$cases |
object | ✅ | 키-값 매핑 (키가 일치하면 해당 값 반환) |
$default |
any | ❌ | 일치하는 케이스가 없을 때 기본값 |
비교: 기존 방식 vs $switch
기존 (복잡):
{
"icon": "{{tab === 'products' ? 'shopping-bag' : tab === 'contents' ? 'book-open' : tab === 'policies' ? 'file-check' : 'file-text'}}"
}
개선 (단순):
{
"icon": {
"$switch": "{{tab}}",
"$cases": {
"products": "shopping-bag",
"contents": "book-open",
"policies": "file-check"
},
"$default": "file-text"
}
}
사용 위치
- 모든 props 속성
text속성className속성- computed 속성 (레이아웃 수준)
주의사항
✅ $cases 키와 $switch 결과가 정확히 일치해야 함 (===)
✅ $default가 없고 일치하는 케이스가 없으면 undefined 반환
✅ iteration/cellChildren 컨텍스트에서 각 아이템별 독립 평가
$cases 값에 추가 표현식 바인딩 불가 (정적 값만)
반복 컨텍스트에서의 표현식 (engine-v1.2.0+)
DataGrid의 cellChildren, 리스트 아이템 등 반복 렌더링 컨텍스트에서도 복잡한 표현식을 사용할 수 있습니다.
{
"columns": [
{
"key": "status",
"label": "$t:user.status",
"cellChildren": [
{
"type": "basic",
"name": "Span",
"props": {
"className": "{{row.status_variant === 'success' ? 'bg-green-100 text-green-800' : row.status_variant === 'danger' ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-800'}} px-2.5 py-0.5 rounded-full text-xs font-medium"
},
"text": "{{row.status_label}}"
}
]
}
]
}
지원 기능
| 기능 | 설명 | 예시 |
|---|---|---|
| 삼항 연산자 | 조건부 값 선택 | {{row.active ? 'active' : 'inactive'}} |
| 중첩 삼항 | 다중 조건 처리 | {{a ? 'x' : b ? 'y' : 'z'}} |
| 논리 연산자 | AND/OR 조건 | {{row.class || 'default'}} |
| 문자열 리터럴 | 따옴표 내 특수문자 | {{active ? 'px-2.5' : 'px-1'}} |
반복 컨텍스트 변수
row: 현재 행 데이터 (DataGrid)value/$value: 현재 셀 값item: 현재 리스트 아이템 (ul/li 패턴)index: 현재 인덱스
내부 동작
템플릿 엔진은 반복 렌더링 시 G7Core.renderItemChildren 헬퍼를 사용하여 각 아이템마다 독립적으로 표현식을 평가합니다. 캐싱으로 인한 데이터 중복 문제를 방지하기 위해 skipCache 옵션이 자동으로 적용됩니다.
text 속성 복합 바인딩 (engine-v1.2.0+)
text 속성에서 데이터 바인딩과 다국어($t:)를 조합한 복합 표현식을 사용할 수 있습니다.
{
"id": "error_message",
"type": "basic",
"name": "P",
"text": "{{_global.errorMessage || $t:common.error_occurred}}"
}
동작 방식
_global.errorMessage가 설정되어 있으면 해당 값 표시- 설정되지 않으면
$t:common.error_occurred번역 키의 값 표시
삼항 연산자와 $t: 조합
{
"text": "{{isError ? $t:common.error : $t:common.success}}"
}
주요 특징
✅ $t: 토큰은 따옴표로 감싸지 않아도 자동으로 문자열로 처리됨
✅ 표현식 평가 후 $t: 결과가 자동으로 번역됨
✅ ||, &&, ? : 등 모든 JavaScript 표현식 사용 가능
컨텍스트 변수 레퍼런스 (engine-v1.11.0+)
레이아웃 JSON 표현식에서 사용 가능한 특수 컨텍스트 변수 목록입니다. 각 변수는 특정 스코프에서만 접근 가능합니다.
상태 접근 변수
| 변수 | 설명 | 사용 범위 |
|---|---|---|
_global |
전역 상태 (앱 전체 공유, 페이지 이동 시 유지) | 어디서나 |
_local |
로컬 상태 (현재 레이아웃 내) | 어디서나 |
_isolated |
격리된 상태 (isolatedState 컴포넌트 내) |
isolatedState 영역 |
_computed |
계산된 속성 (computed 정의 기반) | 어디서나 |
이벤트/콜백 변수
| 변수 | 설명 | 사용 범위 |
|---|---|---|
$event |
DOM 이벤트 객체 (type 필드 사용 시) |
액션 핸들러 params |
$args |
커스텀 콜백 인자 배열 (event 필드 사용 시) |
액션 핸들러 params |
$eventData |
컴포넌트 이벤트 데이터 (engine-v1.11.0+) | onComponentEvent 핸들러 |
계층 접근 변수 (engine-v1.16.0+)
| 변수 | 설명 | 사용 범위 |
|---|---|---|
$parent |
부모 컴포넌트의 상태 접근 | 모달, expandChildren |
$root |
루트 컴포넌트의 상태 접근 | 중첩 레이아웃 |
// 모달에서 부모 _local 접근
{ "text": "{{$parent._local.selectedUser?.name}}" }
// expandChildren에서 부모 전역 상태 접근
{ "value": "{{$parent._global.currency}}" }
$parent._local은 모달 마운트 시 스냅샷 — 모달 열린 후 부모 _local 변경 시 반영 안됨
✅ 리액티브 데이터 필요 시: setState로 모달 자체 _local에 저장 후 사용
✅ setState target: "$parent._local" 또는 "$parent._global"로 부모 상태 직접 수정 가능
반복/정렬 컨텍스트 변수
| 변수 | 설명 | 사용 범위 |
|---|---|---|
row |
현재 행 데이터 | DataGrid cellChildren, subRowChildren |
item / $item |
현재 반복 아이템 | iteration, sortable itemTemplate |
index / $index |
현재 반복 인덱스 | iteration, sortable itemTemplate |
$value |
현재 셀 값 | DataGrid cellChildren |
$sortedItems |
정렬 완료 후 재배열된 배열 (engine-v1.14.0+) | sortable onSortEnd 이벤트 |
$oldIndex / $newIndex |
정렬 전/후 인덱스 (engine-v1.14.0+) | sortable onSortEnd 이벤트 |
$activeId |
드래그 중인 아이템 ID (engine-v1.14.0+) | sortable onSortStart 이벤트 |
sequence 핸들러 컨텍스트 변수
| 변수 | 설명 | 사용 범위 |
|---|---|---|
$prev |
직전 액션의 결과값 (직접 참조) | sequence 내부 액션 |
$results |
모든 이전 액션 결과 배열 | sequence 내부 액션 |
{
"handler": "sequence",
"actions": [
{
"handler": "apiCall",
"target": "/api/products/{{row.id}}",
"params": { "method": "GET" }
},
{
"handler": "setState",
"params": {
"target": "local",
"productDetail": "{{$prev.data}}"
}
}
]
}
onSuccess/onError 콜백 변수
| 변수 | 설명 | 사용 범위 |
|---|---|---|
response |
API 성공 응답 전체 | onSuccess 콜백 |
response.data |
API 응답 data 필드 | onSuccess 콜백 |
result |
response와 동일 (하위 호환) |
onSuccess 콜백 |
error.message |
번역된 에러 메시지 | onError 콜백 |
error.status |
HTTP 상태 코드 | onError 콜백 |
error.errors |
필드별 에러 (422) | onError 콜백 |
error.data |
전체 API 응답 | onError 콜백 |
필수: response 사용 ($response 사용 금지 — $ 접두사 없음)
❌ "{{$response.data}}" → undefined
✅ "{{response.data}}" → 정상 동작
URL/라우트 변수
| 변수 | 설명 | 사용 범위 |
|---|---|---|
route |
URL 경로 파라미터 (예: /users/:id → route.id) |
어디서나 |
query |
쿼리 스트링 파라미터 (예: ?page=1 → query.page) |
어디서나 |
관련 문서
- 다국어 처리 - $localized(), $t:, 컴포넌트 다국어 처리
- g7core-api.md - G7Core 전역 API 레퍼런스
- index.md - 프론트엔드 가이드 인덱스
- layout-json.md - 레이아웃 JSON 스키마
- data-sources.md - 데이터 소스 시스템
- state-management.md - 전역 상태 관리