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

12 KiB

G7Core 헬퍼 API

관련 문서: data-binding.md | components.md | template-development.md


목차

  1. 개요
  2. renderItemChildren
  3. 헬퍼 모듈 구조
  4. 향후 확장

개요

G7Core는 템플릿 엔진이 전역으로 노출하는 네임스페이스입니다. 템플릿 개발자가 커스텀 composite 컴포넌트에서 템플릿 엔진의 기능을 활용할 수 있도록 헬퍼 함수들을 제공합니다.

핵심 원칙

✅ 템플릿 개발자는 엔진 내부 구조를 이해하지 않아도 됨
✅ 헬퍼 함수를 통해 표현식 평가, 다국어 번역 등 엔진 기능 활용
✅ 반복 렌더링 시 캐싱 문제 자동 처리

사용 방법

// 컴포넌트 내에서 G7Core 접근
const G7Core = (window as any).G7Core;

if (G7Core?.renderItemChildren) {
  // 헬퍼 함수 사용
}

renderItemChildren

반복 아이템의 자식 컴포넌트를 렌더링합니다. DataGrid의 cellChildren, ul/li 패턴 등 반복 컨텍스트에서 사용합니다.

시그니처

function renderItemChildren(
  children: ComponentDefinition[],
  itemContext: Record<string, any>,
  componentMap: Record<string, React.ComponentType<any>>,
  keyPrefix?: string
): React.ReactNode[]

파라미터

파라미터 타입 필수 설명
children ComponentDefinition[] ✅ 렌더링할 컴포넌트 정의 배열
itemContext Record<string, any> ✅ 반복 아이템 컨텍스트 (row, value, index 등)
componentMap Record<string, React.ComponentType> ✅ 컴포넌트 맵
keyPrefix string ❌ React key 접두사

반환값

React.ReactNode[] - 렌더링된 React 노드 배열

지원 기능

기능 설명
데이터 바인딩 {{row.name}}, {{value}} 등
복잡한 표현식 삼항 연산자, 논리 연산자
다국어 번역 $t:key 형식
조건부 렌더링 if 속성 지원 (중첩 자식도 평가)
캐시 비활성화 반복 렌더링 시 자동 적용
반응형 오버라이드 responsive 속성 지원

사용 예시

DataGrid의 cellChildren 렌더링

// DataGrid.tsx
const renderCellChildren = (
  cellChildren: DataGridCellChild[],
  row: any,
  value: any,
  keyPrefix: string = ''
): React.ReactNode => {
  const G7Core = (window as any).G7Core;

  if (G7Core?.renderItemChildren) {
    // 조건부 필터링 (condition 처리)
    const filteredChildren = cellChildren.filter((child) => {
      if (!child.condition) return true;
      return evaluateCondition(child.condition, row, value);
    });

    // 컨텍스트 구성
    const context = {
      row,      // 현재 행 데이터
      value,    // 현재 셀 값
      $value: value,  // 별칭
    };

    return G7Core.renderItemChildren(
      filteredChildren,
      context,
      componentMap,
      keyPrefix
    );
  }

  console.warn('[DataGrid] G7Core.renderItemChildren을 사용할 수 없습니다.');
  return null;
};

리스트 아이템 렌더링

// CustomList.tsx
const renderListItems = (items: any[], itemTemplate: ComponentDefinition[]) => {
  const G7Core = (window as any).G7Core;

  if (!G7Core?.renderItemChildren) return null;

  return items.map((item, index) => (
    <li key={`item-${index}`}>
      {G7Core.renderItemChildren(
        itemTemplate,
        { item, index },
        componentMap,
        `list-${index}`
      )}
    </li>
  ));
};

레이아웃 JSON 예시

renderItemChildren이 처리하는 레이아웃 JSON 구조:

{
  "columns": [
    {
      "key": "status",
      "label": "$t:user.status",
      "cellChildren": [
        {
          "id": "status_badge",
          "type": "basic",
          "name": "Span",
          "props": {
            "className": "{{row.status_variant === 'success' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}} px-2.5 py-0.5 rounded-full text-xs font-medium"
          },
          "text": "{{row.status_label}}"
        }
      ]
    }
  ]
}

if 조건부 렌더링 예시

상태에 따라 다른 라벨을 표시하는 패턴:

{
  "cellChildren": [
    {
      "id": "active-label",
      "type": "basic",
      "name": "Span",
      "if": "{{row.status == 'active'}}",
      "props": { "className": "text-green-600" },
      "text": "$t:status.active"
    },
    {
      "id": "inactive-label",
      "type": "basic",
      "name": "Span",
      "if": "{{row.status == 'inactive'}}",
      "props": { "className": "text-gray-500" },
      "text": "$t:status.inactive"
    }
  ]
}

중요: renderItemChildren은 중첩된 자식 컴포넌트의 if 조건도 평가합니다. DynamicRenderer와 동일한 로직(evaluateIfCondition)을 사용하여 일관성을 보장합니다.

캐싱 처리

renderItemChildren은 내부적으로 skipCache: true 옵션을 사용합니다.

이유: 반복 렌더링에서 같은 경로(row.name)가 다른 값을 가져야 하기 때문입니다.

// 내부 동작 (RenderHelpers.ts)
return bindingEngine.resolve(singleBindingPath, context, { skipCache: true });

캐싱을 비활성화하지 않으면:

  • 첫 번째 행의 row.name 값이 캐시됨
  • 모든 행에서 같은 값이 표시됨 (버그)

헬퍼 모듈 구조

헬퍼 함수들은 카테고리별로 분리된 모듈 구조로 관리됩니다.

디렉토리 구조

resources/js/core/template-engine/helpers/
├── index.ts              # 통합 export (진입점)
├── RenderHelpers.ts      # 렌더링 관련 헬퍼
├── FormatHelpers.ts      # 포맷팅 헬퍼 (향후)
├── ValidationHelpers.ts  # 검증 헬퍼 (향후)
└── StyleHelpers.ts       # 스타일 헬퍼 (향후)

현재 제공되는 헬퍼

모듈 함수 설명
RenderHelpers renderItemChildren 반복 아이템 자식 렌더링
RenderHelpers bindComponentActions 액션 배열을 이벤트 핸들러로 변환
RenderHelpers evaluateIfCondition if 조건 평가 (내부용, DynamicRenderer와 공유)
RenderHelpers resolveIterationSource iteration source 표현식 평가
RenderHelpers resolveExpressionString 바인딩 표현식 문자열 치환
RenderHelpers resolveExpressionObject 객체 내 바인딩 표현식 치환

새 헬퍼 추가 방법

  1. 카테고리에 맞는 파일 생성 또는 수정
// helpers/FormatHelpers.ts
export function formatCurrency(value: number, locale?: string): string {
  return new Intl.NumberFormat(locale || 'ko-KR', {
    style: 'currency',
    currency: 'KRW',
  }).format(value);
}
  1. helpers/index.ts에서 re-export
export { formatCurrency } from './FormatHelpers';
  1. template-engine.ts에서 G7Core에 노출
import { formatCurrency } from './template-engine/helpers';
(window as any).G7Core.formatCurrency = formatCurrency;

bindComponentActions

컴포넌트의 actions 배열을 이벤트 핸들러 props로 변환합니다. DynamicRenderer와 renderItemChildren에서 동일한 로직을 공유합니다.

시그니처

function bindComponentActions(
  props: Record<string, any>,
  actions: ActionDefinition[] | undefined,
  context: Record<string, any>,
  options?: BindComponentActionsOptions
): Record<string, any>

파라미터

파라미터 타입 필수 설명
props Record<string, any> ✅ 컴포넌트의 기존 props
actions ActionDefinition[] ❌ 레이아웃 JSON의 actions 배열
context Record<string, any> ✅ 데이터 바인딩 컨텍스트
options BindComponentActionsOptions ❌ 추가 옵션

BindComponentActionsOptions

interface BindComponentActionsOptions {
  componentContext?: {
    state?: any;
    setState?: (updates: any) => void;
  };
  actionDispatcher?: ActionDispatcher;
}
필드 설명
componentContext 로컬 상태 관리용 컨텍스트
actionDispatcher ActionDispatcher 인스턴스 (미제공 시 싱글톤 사용)

반환값

Record<string, any> - 이벤트 핸들러가 추가된 props 객체

작동 원리

  1. actions 배열이 없거나 비어있으면 원본 props 반환
  2. ActionDispatcher의 bindActionsToProps 호출
  3. 각 액션의 event 필드에 따라 해당 이벤트 핸들러 생성
    • onClick, onChange, onSubmit, onItemClick 등
  4. actions prop 제거 (HTML 속성으로 렌더링 방지)

사용 예시

// DynamicRenderer에서 사용
props = bindComponentActions(
  props,
  effectiveComponentDef.actions,
  extendedDataContext,
  { componentContext, actionDispatcher }
);

// renderItemChildren에서 사용
const finalProps = bindComponentActions(resolvedProps, childDef.actions, itemContext, {
  actionDispatcher: options?.actionDispatcher,
});

레이아웃 JSON 예시

{
  "name": "Button",
  "props": { "variant": "primary" },
  "text": "설치",
  "actions": [
    {
      "type": "click",
      "handler": "sequence",
      "actions": [
        { "handler": "setState", "params": { "target": "global", "selectedModule": "{{row}}" } },
        { "handler": "openModal", "target": "install_modal" }
      ]
    }
  ]
}

위 JSON은 bindComponentActions에 의해 다음과 같이 변환됩니다:

{
  variant: "primary",
  onClick: async (event) => {
    // sequence 핸들러 실행
    // 1. setState 실행
    // 2. openModal 실행
  }
}

ActionDispatcher 싱글톤 패턴

템플릿 엔진에서 ActionDispatcher는 싱글톤 패턴으로 관리됩니다.

초기화

template-engine.ts에서 ActionDispatcher 생성 후 반드시 싱글톤으로 설정해야 합니다:

import { ActionDispatcher, setActionDispatcherInstance } from './template-engine/ActionDispatcher';

// ActionDispatcher 생성
const actionDispatcher = new ActionDispatcher(config, translationEngine, translationContext);

// 싱글톤 인스턴스로 설정 (필수)
setActionDispatcherInstance(actionDispatcher);

사용

다른 모듈에서는 getActionDispatcher()로 싱글톤 인스턴스를 가져옵니다:

import { getActionDispatcher } from './ActionDispatcher';

const dispatcher = getActionDispatcher();
dispatcher.dispatch(action, context);

헬퍼 함수에서의 사용

bindComponentActions와 같은 헬퍼 함수에서는 옵션으로 전달받은 인스턴스를 우선 사용하고, 없으면 싱글톤을 사용합니다:

const actionDispatcher = options?.actionDispatcher ?? getActionDispatcher();

주의사항

setActionDispatcherInstance 호출 전에 getActionDispatcher() 사용 시 불완전한 인스턴스 반환
✅ template-engine.ts에서 ActionDispatcher 생성 직후 setActionDispatcherInstance 호출 필수
✅ 헬퍼 함수에서는 가능하면 옵션으로 actionDispatcher를 전달받아 사용

향후 확장

FormatHelpers (계획)

// 숫자 포맷팅
G7Core.formatNumber(1234567);  // "1,234,567"
G7Core.formatCurrency(50000);  // "₩50,000"

// 날짜 포맷팅
G7Core.formatDate(new Date(), 'YYYY-MM-DD');  // "2025-11-30"
G7Core.formatRelativeTime(date);  // "3일 전"

ValidationHelpers (계획)

// 필드 검증
G7Core.validateEmail('test@example.com');  // true
G7Core.validateRequired(value);  // boolean

StyleHelpers (계획)

// 조건부 클래스 조합
G7Core.classNames('base', { 'active': isActive, 'disabled': isDisabled });

주의사항

✅ G7Core 존재 여부 항상 확인 (G7Core?.renderItemChildren)
✅ 컴포넌트 맵은 템플릿에서 ComponentRegistry를 통해 구성
✅ keyPrefix를 적절히 사용하여 React key 충돌 방지
G7Core는 템플릿 엔진 초기화 후에만 사용 가능
SSR 환경에서는 window 객체 접근 주의

관련 문서