Files
Gnuboard7/docs/frontend/data-binding.md
T
2026-04-01 10:30:52 +09:00

19 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}})

목차

  1. 기본 문법
  2. 데이터 바인딩 표현식 (engine-v1.1.0+)
  3. Optional Chaining & Nullish Coalescing
  4. 파이프 함수 (engine-v1.9.0+)
  5. $get 헬퍼 함수 (engine-v1.9.0+)
  6. $switch 표현식 (engine-v1.9.0+)
  7. 반복 컨텍스트에서의 표현식
  8. 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

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}}"
}

동작 방식

  1. _global.errorMessage가 설정되어 있으면 해당 값 표시
  2. 설정되지 않으면 $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) 어디서나

관련 문서