본문 바로가기

React

React에서 JSON Schema 기반 폼 생성하기

JSON Schema를 사용하면 폼 구조, 유효성 규칙, 표시 방식까지 데이터를 중심으로 선언적으로 정의할 수 있습니다. React에서는 라이브러리를 통해 스키마만으로 폼을 자동 생성하고 유지보수를 단순화할 수 있습니다. 실무에서 빠르게 적용하는 방법과 커스터마이즈 포인트를 정리합니다.

1. 라이브러리 선택

대표 옵션은 RJSF(@rjsf/core), JSON Forms, Uniforms입니다. RJSF는 문서와 생태계가 안정적이며 Ajv 기반 검증을 쉽게 붙일 수 있어 범용적으로 추천합니다. JSON Forms는 복잡한 엔터프라이즈 폼에 강하고, Uniforms는 스키마-UI 어댑터가 다양해 커스터마이징에 유연합니다.

2. RJSF 빠른 시작

// 설치
// npm i @rjsf/core @rjsf/validator-ajv8

import React from 'react';
import Form from '@rjsf/core';
import { validatorAjv8 } from '@rjsf/validator-ajv8';

const schema = {
  type: 'object',
  required: ['email'],
  properties: {
    name: { type: 'string', title: '이름', minLength: 2 },
    age: { type: 'integer', title: '나이', minimum: 0 },
    email: { type: 'string', format: 'email', title: '이메일' },
    newsletter: { type: 'boolean', title: '뉴스레터 구독' }
  }
};

const uiSchema = {
  age: { 'ui:widget': 'updown' },
  email: { 'ui:placeholder': 'you@example.com' }
};

export default function ProfileForm() {
  const onSubmit = ({ formData }) => {
    console.log('제출 데이터:', formData);
  };
  return (
    <Form schema={schema} uiSchema={uiSchema} validator={validatorAjv8} onSubmit={onSubmit} />
  );
}

스키마만으로 입력, 라벨, 검증까지 자동 구성됩니다. uiSchema는 표시 방식과 위젯 지정에 사용합니다.

3. UI Schema로 표시 제어

const uiSchema = {
  'ui:order': ['name', 'email', 'age', 'newsletter'],
  name: { 'ui:autofocus': true },
  newsletter: { 'ui:help': '매주 발송됩니다.' },
  age: {
    'ui:widget': 'range',
    'ui:options': { min: 0, max: 120 }
  }
};

필드 순서, 도움말, 위젯 옵션을 지정해 UX를 개선합니다.

4. 커스텀 위젯과 필드

import React from 'react';
import Form from '@rjsf/core';
import { validatorAjv8 } from '@rjsf/validator-ajv8';

const PasswordStrengthWidget = (props) => {
  const value = props.value || '';
  const score = [/[^a-zA-Z0-9]/.test(value), /[A-Z]/.test(value), /\d/.test(value), value.length >= 8]
    .filter(Boolean).length;
  const label = ['약함','보통','좋음','강함'][score] || '약함';
  return (
    <div>
      <input type="password" value={value} onChange={(e) => props.onChange(e.target.value)} />
      <small>강도: {label}</small>
    </div>
  );
};

const schema = {
  type: 'object',
  properties: { password: { type: 'string', title: '비밀번호', minLength: 8 } }
};

const uiSchema = { password: { 'ui:widget': PasswordStrengthWidget } };

export default function PasswordForm() {
  return <Form schema={schema} uiSchema={uiSchema} validator={validatorAjv8} />;
}

복잡한 입력 로직은 커스텀 위젯으로 캡슐화합니다. 필드 레벨 템플릿을 통해 레이아웃도 변경 가능합니다.

5. 조건부/동적 폼

// if/then/else를 사용한 조건부 필드
const schema = {
  type: 'object',
  properties: {
    newsletter: { type: 'boolean', title: '뉴스레터 구독' },
    frequency: { type: 'string', title: '수신 빈도', enum: ['daily','weekly','monthly'] }
  },
  allOf: [
    {
      if: { properties: { newsletter: { const: true } }, required: ['newsletter'] },
      then: { required: ['frequency'] }
    }
  ]
};

// dependencies로도 가능
// dependencies: { newsletter: { oneOf: [ { properties: { newsletter: { const: true }, frequency: { type: 'string' } }, required: ['frequency'] }, { properties: { newsletter: { const: false } } } ] } }

스키마만으로 조건부 필드 표시 및 필수 여부를 제어합니다.

6. 서버 연동과 추가 검증

import Form from '@rjsf/core';
import { validatorAjv8 } from '@rjsf/validator-ajv8';

const schema = {
  type: 'object',
  properties: { username: { type: 'string', title: '아이디', minLength: 4 } },
  required: ['username']
};

function customValidate(formData, errors) {
  if (formData.username && formData.username.includes('admin')) {
    errors.username.addError('admin을 포함할 수 없습니다.');
  }
  return errors;
}

async function onSubmit({ formData }) {
  const res = await fetch('/api/users/check', {
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData)
  });
  const json = await res.json();
  if (!json.ok) {
    alert('서버 검증 실패: ' + json.message);
  } else {
    alert('저장되었습니다');
  }
}

export default function UserForm() {
  return (
    <Form schema={schema} validator={validatorAjv8} customValidate={customValidate} onSubmit={onSubmit}
      transformErrors={(errors) => errors.map(e => ({ ...e, message: e.message.replace('should', '요구사항:') }))} />
  );
}

customValidate로 비즈니스 규칙을 추가하고, transformErrors로 메시지를 사용자 친화적으로 변경합니다.

7. API 기반 동적 스키마

import React, { useEffect, useState, useMemo } from 'react';
import Form from '@rjsf/core';
import { validatorAjv8 } from '@rjsf/validator-ajv8';

export default function DynamicForm() {
  const [schema, setSchema] = useState(null);
  const [uiSchema, setUiSchema] = useState(null);
  useEffect(() => {
    (async () => {
      const res = await fetch('/api/formSchema');
      const { schema, uiSchema } = await res.json();
      setSchema(schema);
      setUiSchema(uiSchema);
    })();
  }, []);

  const memoSchema = useMemo(() => schema, [schema]);
  const memoUi = useMemo(() => uiSchema, [uiSchema]);

  if (!memoSchema) return <p>로딩 중...</p>;
  return <Form schema={memoSchema} uiSchema={memoUi} validator={validatorAjv8} />;
}

서버에서 폼 정의를 내려받아 배포 없이 폼을 바꿀 수 있습니다.

8. 성능 최적화 포인트

스키마와 uiSchema는 useMemo로 고정하고, onChange는 디바운스로 과도한 렌더를 줄입니다. 대형 폼은 필드 그룹을 나눠 스텝 폼으로 구성합니다.

import { useMemo, useRef } from 'react';

const memoSchema = useMemo(() => schema, []);
const memoUi = useMemo(() => uiSchema, []);

const onChange = (() => {
  let t; return (e) => { clearTimeout(t); t = setTimeout(() => console.log(e.formData), 150); };
})();

9. 접근성과 국제화

라벨과 도움말을 스키마 title/description에 채워 넣고, idPrefix로 고유 ID를 지정합니다. 다국어는 서버에서 스키마 텍스트를 로케일별로 주입하거나, transformErrors와 uiSchema를 로케일에 따라 교체합니다.

10. 테스트와 유지보수

Testing Library로 렌더와 상호작용을 검증합니다. 스키마는 도메인별 파일로 분리하고 공통 규칙은 공용 스키마 조각으로 재사용합니다.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

render(<ProfileForm />);
await userEvent.type(screen.getByLabelText('이메일'), 'bad');
await userEvent.click(screen.getByText('Submit'));
expect(screen.getByText(/email/)).toBeInTheDocument();

11. 흔한 이슈

포맷 검증이 동작하지 않으면 Ajv 버전과 validator를 확인합니다. 조건부 필드가 표시되지 않으면 if/then/else 스키마 위치와 required 설정을 점검합니다. 커스텀 위젯의 value/onChange 계약을 지키지 않으면 값 반영이 되지 않습니다.

12. 마무리

JSON Schema 기반 폼은 스키마 중심 개발로 생산성과 일관성을 확보합니다. RJSF를 시작점으로 커스텀 위젯, 조건부 로직, 서버 검증을 단계적으로 붙여 실무에 적용해 보세요.