React json-render 사용법: 안전한 생성형 UI 카탈로그 정의 및 렌더링

생성형 UI 환경에서 json-render의 역할과 가드레일 구조

구조화된 UI 스키마와 컴포넌트 연결 구조를 나타낸 개념도
사전 정의된 규격에 맞춰 안전하게 UI를 생성하는 가드레일 구조

생성형 인공지능(AI)을 활용해 인터페이스를 동적으로 구성할 때 중요한 과제는 보안성과 렌더링 안정성을 유지하는 것입니다. 대규모 언어 모델이 임의의 자바스크립트 코드나 검증되지 않은 JSX 마크업을 직접 생성하도록 허용하면 브라우저에서 스크립트 실행 오류가 발생하거나 예상치 못한 취약점에 노출될 수 있습니다. json-render는 이러한 위험을 완화하기 위해 사전에 정의된 컴포넌트 규격에 따라서만 JSON 인터페이스를 생성하도록 가드레일을 설정하는 생성형 UI 프레임워크입니다.

json-render를 활용한 인터페이스 구현 흐름은 네 가지 핵심 요소로 구성됩니다.

  • 카탈로그(Catalog): AI가 사용할 수 있는 허용 컴포넌트 목록과 각 컴포넌트의 props 규격을 정의하는 스키마 집합입니다.
  • 레지스트리(Registry): 카탈로그에 선언된 컴포넌트 이름에 실제 화면을 그리는 React 구현체를 연결한 매핑 객체입니다.
  • 사양(Spec): 트리 형태의 UI 배치를 기술한 flat JSON 사양 데이터입니다.
  • 렌더러(Renderer): spec 객체와 registry 객체를 전달받아 안전하게 사전 정의된 React 뷰로 변환하는 출력 컴포넌트입니다.

React 프로젝트에 json-render 기본 패키지 설치하기

React 환경에서 json-render를 시작하려면 핵심 카탈로그 스키마를 다루는 @json-render/core 패키지와 React 렌더링 런타임을 제공하는 @json-render/react 패키지를 함께 설치해야 합니다.

# for React
npm install @json-render/core @json-render/react

이 명령어는 프로젝트의 package.json 파일에 프레임워크 핵심 라이브러리를 추가합니다. @json-render/core는 스키마 정의 및 검증 규칙 생성을 담당하고, @json-render/react는 React 전용 컴포넌트 레지스트리 생성 함수와 렌더러 컴포넌트를 공급합니다.

프로젝트 환경에 따라 다른 프레임워크나 사전 구축된 UI 컴포넌트가 필요하다면 추가 패키지를 선택할 수 있습니다. 예를 들어 사전 구성된 shadcn/ui 컴포넌트를 함께 사용하려면 @json-render/shadcn을 설치하고, React Native 환경에서는 @json-render/react-native를 사용할 수 있습니다. 여기서는 순수 React 기본 패키지를 중심으로 주 경로를 진행합니다.

defineCatalog와 Zod로 허용 컴포넌트 규격 설계하기

설치가 끝난 뒤 수행할 핵심 작업은 AI가 생성할 수 있는 컴포넌트 규격을 선언하는 카탈로그 정의입니다. @json-render/coredefineCatalog 함수, @json-render/react/schema에서 제공하는 기본 schema, 그리고 zod 유효성 검증 라이브러리를 조합해 허용할 컴포넌트의 props 형태를 지정합니다.

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    Card: {
      props: z.object({ title: z.string() }),
      description: "A card container",
    },
    Metric: {
      props: z.object({
        label: z.string(),
        value: z.string(),
        format: z.enum(["currency", "percent", "number"]).nullable(),
      }),
      description: "Display a metric value",
    },
    Button: {
      props: z.object({
        label: z.string(),
        action: z.string(),
      }),
      description: "Clickable button",
    },
  },
  actions: {
    export_report: { description: "Export dashboard to PDF" },
    refresh_data: { description: "Refresh all metrics" },
  },
});

이 코드에서는 Card, Metric, Button 컴포넌트와 각 컴포넌트가 받아들일 수 있는 속성을 정의합니다. Card는 문자열 형태의 title 속성만 허용하고, Metric은 label, value, format 속성을 검증합니다. Button은 label과 action 속성을 명시합니다. 또한 UI 컴포넌트뿐 아니라 export_report, refresh_data와 같은 작업(actions) 목록도 함께 기술할 수 있습니다. 이렇게 규격을 선언하면 정의되지 않은 임의 속성이나 잘못된 자료형을 전달받았을 때 이를 조기에 식별할 수 있습니다.

defineRegistry를 통한 React 컴포넌트 구현체 연결

카탈로그로 규격을 정의한 후에는 각 컴포넌트가 브라우저에 표시될 실제 React 마크업을 연결해야 합니다. 이 작업은 @json-render/reactdefineRegistry 함수를 사용하여 수행합니다.

import { defineRegistry } from "@json-render/react";

const { registry } = defineRegistry(catalog, {
  components: {
    Card: ({ props, children }) => (
      <div className="card">
        <h3>{props.title}</h3>
        {children}
      </div>
    ),
    Metric: ({ props }) => (
      <div className="metric">
        <span>{props.label}</span>
        <span>{props.value}</span>
      </div>
    ),
    Button: ({ props, emit }) => (
      <button onClick={() => emit("press")}>{props.label}</button>
    ),
  },
});

defineRegistry는 첫 번째 인자로 앞서 선언한 catalog를 받고, 두 번째 인자로 각 컴포넌트 이름에 대응하는 렌더링 함수들을 객체로 받습니다. 개별 컴포넌트는 검증된 props, 중첩 구조를 나타내는 children, 그리고 이벤트 처리를 위한 emit 함수를 전달받아 표준 React 엘리먼트를 반환합니다. 이를 통해 카탈로그에 선언된 이름과 실제 UI 렌더링 로직이 일대일로 연결됩니다.

Renderer 컴포넌트로 JSON 사양(spec) 화면에 출력하고 검증하기

React 화면에 렌더링된 컴포넌트 인터페이스 결과물
Renderer 컴포넌트를 통해 화면에 마운트된 UI 사양

카탈로그와 레지스트리가 준비되면, 이제 AI가 생성하는 형태의 JSON 사양(spec)을 React 뷰 컴포넌트인 Renderer에 전달하여 화면에 마운트합니다. 아래 예제에서 사용하는 spec은 컴포넌트 렌더링 동작을 확인하기 위한 공식 테스트 입력 객체이며, 실제 서비스 운영 시 AI 모델이 실시간으로 해당 JSON을 생성해 클라이언트에 전달하는 API 연동은 별도의 단계로 구성됩니다.

import React from "react";
import { Renderer } from "@json-render/react";

// 렌더링 확인용 공식 Flat spec 입력 객체
const spec = {
  root: "card-1",
  elements: {
    "card-1": {
      type: "Card",
      props: { title: "Hello" },
      children: ["button-1"],
    },
    "button-1": {
      type: "Button",
      props: { label: "Click me" },
      children: [],
    },
  },
};

function Dashboard({ spec }) {
  return <Renderer spec={spec} registry={registry} />;
}

// 공식 spec 입력을 전달하여 실제 렌더링 호출
export default function App() {
  return <Dashboard spec={spec} />;
}

위 구성으로 화면을 렌더링하고 동작을 검증하는 구체적인 절차는 다음과 같습니다.

  1. 패키지 모듈 확인: @json-render/core@json-render/react 패키지가 설치되어 있는지 확인하고 필요한 모듈을 불러옵니다.
  2. 카탈로그 및 레지스트리 바인딩: defineCatalog로 선언한 스키마를 defineRegistry의 첫 번째 매개변수로 전달해 registry 객체를 생성합니다.
  3. spec 객체 구성 및 호출: root 키로 최상위 컴포넌트 식별자를 지정하고 elements 맵에 각 엘리먼트의 type, props, children 배열을 정의한 후 Renderer 컴포넌트에 specregistry를 함께 전달합니다.

정상 결과 확인법: 코드가 브라우저에 마운트되면 화면에 card-1에 해당하는 <div class="card"> 요소가 생성되고, 그 내부 제목으로 Hello 텍스트가 표시됩니다. 또한 카드 내부의 하위 요소로 Click me라는 라벨을 가진 <button>이 렌더링되는 것을 브라우저 화면에서 확인할 수 있습니다.

대표적인 실패 원인과 점검 방법: 화면에 컴포넌트가 나타나지 않거나 렌더링 오류가 발생한다면 다음 항목을 확인해야 합니다.

  • 타입 식별자 불일치: spec.elements 내부에 지정된 type 문자열(예: Card, Button)이 defineCatalogdefineRegistry에 등록된 컴포넌트 이름과 대소문자까지 정확히 일치하는지 확인합니다.
  • Zod 스키마 검증 실패: props로 넘긴 데이터의 형태나 자료형이 카탈로그에서 Zod로 선언한 규칙(예: title: z.string())과 다를 경우 렌더링이 실패할 수 있으므로 spec 객체의 속성값을 점검합니다.
  • 트리 참조 식별자 누락: spec.root에 지정된 키나 children 배열에 나열된 엘리먼트 ID가 spec.elements 객체 내부의 키로 실제로 존재하는지 점검합니다.

참고 자료