React JSON Schema Forms 실전 — 왜 문제가 발생하고 SurveyJS가 아키텍처를 해결하는 방법
Source: Dev.to
위에 제공된 내용 외에 번역할 텍스트가 없습니다. 번역이 필요한 본문을 제공해 주시면 한국어로 번역해 드리겠습니다.
The Root Cause
React JSON Schema 라이브러리들은 스키마를 both 구조 and 동작 두 가지로 취급합니다. 이들은 스키마 변형(의존성, oneOf, 동적으로 재생성된 스키마)에 의존하고, 로직을 적용하기 위해 React 재렌더링에 크게 의존합니다. 아래의 대부분 문제는 그 혼합에서 직접적으로 비롯됩니다.
SurveyJS는 다른 접근 방식을 취합니다: JSON은 declarative model로 취급되고, 비즈니스 로직은 폼 엔진(survey‑core)에 캡슐화되며, React는 only 렌더링에만 사용되고 오케스트레이션에는 사용되지 않습니다.
RJSF에서 조건부 필드
RJSF에서는 조건부 필드를 일반적으로 dependencies, oneOf 또는 동적으로 재생성된 스키마를 사용하여 구현합니다:
const schema = {
type: "object",
properties: {
hasCar: { type: "boolean" }
},
dependencies: {
hasCar: {
oneOf: [
{
properties: {
hasCar: { const: true },
carModel: { type: "string" }
}
},
{
properties: {
hasCar: { const: false }
}
}
]
}
}
};
이 방식은 스키마가 예기치 않게 형태가 바뀌지 않는 한 작동합니다. React JSON Schema 폼에서 스키마는 단순히 필드에 대한 설명이 아니라 폼 자체입니다. dependencies, oneOf, anyOf를 사용하면 런타임에 스키마 분기가 동적으로 전환됩니다. React 입장에서는 필드가 추가·제거되고 내부 폼 트리의 형태가 바뀌며, 이전에 마운트된 입력 요소가 언마운트되고 다시 마운트될 수 있습니다.
다음과 같은 경우에는 관리가 가능합니다:
- 스키마가 정적일 때.
- 조건이 단순할 때.
- 외부 데이터가 스키마에 영향을 주지 않을 때.
하지만 실제 제품 폼에서는 종종 다음과 같은 요구가 있습니다:
- 기능 플래그.
- 백엔드 기반 설정.
- 역할 기반 가시성.
- 점진적 공개.
스키마가 동적으로 수정될 때(예: useEffect에서 재생성) React는 이를 새로운 폼 정의로 인식하고, 이전 폼의 연속으로 보지 않습니다. 이로 인해 입력 상태, 기본값, 검증이 예상치 못하게 초기화될 수 있습니다.
렌더‑사이클 결합
React JSON Schema 폼 라이브러리는 또한 React의 렌더 사이클에 의존해 동작을 조정합니다:
- 필드 변경이
formData를 업데이트합니다. - React가 다시 렌더링합니다.
- 다른 스키마 분기가 선택되고 검증됩니다.
이 흐름은 업데이트가 완벽한 순서대로 일어날 때만 정상 작동합니다. React는 업데이트를 배치하거나 컴포넌트를 기회에 따라 재렌더링할 수 있는데, 이때 다음과 같은 문제가 발생할 수 있습니다:
- 필드 깜빡임.
- 검증 메시지 사라짐.
- 입력 포커스 손실.
이것은 React 버그가 아니라, 비즈니스 로직을 렌더 사이클에 얽어놓은 결과입니다.
SurveyJS: 정적 스키마 + 런타임 표현식
SurveyJS는 동작을 표현하기 위해 스키마를 변경하지 않습니다. 대신 조건은 survey‑core에서 평가되는 런타임 표현식입니다:
{
"name": "carModel",
"type": "text",
"visibleIf": "{hasCar} = true"
}
핵심 아이디어
- 스키마는 정적으로 유지됩니다—폼 구조가 한 번 정의되고 지속 가능한 계약으로 취급됩니다.
- 필드는 절대 제거되거나 다시 생성되지 않으며, 항상 모델에 존재하여 엔진에 안정적인 참조점을 제공합니다.
- 가시성은 엔진에 의해 런타임에 평가되며, 필드 표시가 결정적이고 React 렌더 타이밍과 무관하게 됩니다.
React는 폼을 다시 빌드하도록 요청받지 않습니다. 엔진이 가시적이거나 활성이라고 보고하는 내용만을 렌더링합니다. 상태 변화는 구조적 변형이 아니라 규칙 평가를 트리거하므로 스키마 재생성, 깊은 동등성 검사, 방어적 메모이제이션이 필요 없어집니다. 이러한 분리는 SurveyJS가 복잡한 조건 로직, 대규모 스키마, 빈번한 상태 변화를 취약해지지 않고 처리할 수 있게 합니다.
따라서 SurveyJS의 조건 로직은 예측 가능하고, 조합 가능하며, 확장 가능하여 매우 큰 폼에서도 안정적으로 동작합니다.
Source: …
검증
React JSON Schema 폼에서는 검증이 일반적으로 Ajv에 위임되고 React 상태로 다시 매핑됩니다:
// Example JSX (simplified)
<Form
schema={schema}
onChange={...}
validate={...}
/>
팀에서는 다음과 같은 문제를 자주 겪습니다:
- 스키마 업데이트 후 오류가 사라짐.
- 비동기 검증이 렌더링과 경쟁함.
- 사용자 정의 파이프라인이 필요한 복잡한 교차 필드 검증.
근본적인 문제는 Ajv가 아니라 검증이 React 렌더 사이클에 묶여 있다는 점입니다. 스키마나 상태가 변경될 때마다 오류 트리가 초기화될 위험이 있습니다.
SurveyJS 검증
SurveyJS는 검증을 일급 엔진 관점으로 다룹니다:
{
"name": "email",
"type": "text",
"isRequired": true,
"validators": [
{ "type": "email" }
]
}
- 검증이 React와 독립적으로 실행됩니다.
- 오류가 설문 모델에 지속됩니다.
- 교차 필드 또는 비동기 검증이 내장되어 있습니다.
검증을 React 밖으로 빼면 setState 호출을 순서대로 처리하거나, 검증을 수동으로 디바운스하거나, 비동기 결과를 렌더링과 조정해야 하는 필요성이 사라집니다. 복잡한 규칙도 해당 필드와 가깝게 선언되므로, 컴포넌트 로직에 흩어져 있지 않아 가독성이 유지됩니다.
동적 스키마 재생성 – 흔한 함정
RJSF에서는 스키마를 변경하면 보통 스키마 객체를 교체하는 것을 의미하며, 이는 종종 useEffect 안에서 이루어집니다:
const [schema, setSchema] = useState(baseSchema);
useEffect(() => {
if (formData.type === "advanced") {
setSchema(advancedSchema);
}
}, [formData]);
이 패턴은 전체 폼 재초기화, 사용자 입력 손실, 그리고 검증 오류를 초래합니다. SurveyJS는 스키마를 정적으로 유지하고 엔진 내부에서 모든 조건 로직을 처리함으로써 이를 방지합니다.
요약
| 항목 | React JSON Schema Forms (RJSF) | SurveyJS Form Library |
|---|---|---|
| 스키마 역할 | 구조 및 동작 (변경됨) | 순수 선언형 모델 (정적) |
| 조건부 로직 | dependencies, oneOf, 스키마 재생성 | 런타임 표현식 (visibleIf, enableIf 등) |
| 상태 처리 | React 렌더 사이클에 연결되어 있어 언마운트/리마운트를 일으킬 수 있음 | 엔진이 안정적인 필드 인스턴스를 유지함 |
| 검증 | Ajv + React 상태; 스키마 변경 시 리셋될 가능성이 있음 | 엔진 기반, 모델에 지속됨 |
| 확장성 | 대규모 동적 폼에서는 깨지기 쉬움 | 예측 가능하고, 조합 가능하며, 대규모 스키마에 적용 가능 |
JSON을 정적 계약으로 취급하고 비즈니스 로직을 전용 엔진으로 이동함으로써, SurveyJS는 폼의 모양과 동작을 분리합니다. 이는 특히 복잡하고 동적이며 대규모 애플리케이션을 다룰 때 더 신뢰성 있고 유지보수가 용이하며 확장 가능한 폼을 제공합니다.
Source: …
왜 SurveyJS가 대형·동적 폼에서 React JSON‑Schema Forms보다 뛰어난가
React JSON‑Schema 라이브러리의 문제점
- 스키마 변이 – JSON 스키마를 업데이트(필드 추가/제거, 검증 변경)하면 전체 재렌더링이 발생합니다.
- 상태 손실 – 스키마가 바뀔 때 사용자 입력, 검증 상태, UI 상태가 종종 초기화됩니다.
- 성능 저하 – 큰 스키마는 초기 렌더링이 느리고, 깊은 객체 비교가 비싸며, 관련 없는 필드까지 불필요하게 재렌더링됩니다.
개발자들은 메모이제이션, 깊은 병합, 스키마 차이(diff) 계산 등으로 이를 완화하려 하지만, 이러한 우회책은 복잡성을 높이고 근본적인 문제를 해결하지 못합니다.
SurveyJS의 접근 방식
- 정적 스키마 – 런타임에 JSON 스키마가 절대 변하지 않습니다.
- 런타임 표현식 – 조건부 가시성, 필수 필드 로직, 검증을 SurveyJS 엔진이 평가하는 수식으로 표현합니다.
- 관심사의 분리 –
- survey‑core – 플랫폼에 구애받지 않는 모델로, 폼 정의, 상태, 비즈니스 로직을 보관합니다.
- survey‑react‑ui – 엔진이 “활성”이라고 보고하는 것을 단순히 렌더링하는 얇은 React 래퍼입니다.
React는 렌더링만 담당하므로 조건을 평가하거나 검증 상태를 관리하거나 스키마 변화를 조정할 필요가 없습니다. 이로 인해 얻는 이점은 다음과 같습니다.
- 예측 가능한 동작
- 동적 변경 시에도 사용자 입력 및 검증 상태 유지
- 대형 폼에서 훨씬 나은 성능
성능상의 장점
| 문제 | React JSON‑Schema 라이브러리 | SurveyJS |
|---|---|---|
| 거대한 스키마의 초기 렌더링 | 느림 (전체 트리 렌더링) | 빠름 (점진적 렌더링) |
| 단일 필드 업데이트 | 전체 폼 재렌더링을 유발할 수 있음 | 영향을 받은 필드만 재평가 |
| 매 렌더링마다 깊은 비교 | 비용이 많이 듦 | 회피 – 엔진이 차이를 처리 |
| 페이지/질문 지연 로딩 | 거의 지원되지 않음 | 내장 지원, 보이는 항목만 렌더링 |
최소 예제 (React에서 SurveyJS)
import { Survey } from "survey-react-ui";
import { Model } from "survey-core";
const surveyJson = {
/* … your survey definition … */
};
export default function SurveyComponent() {
const survey = new Model(surveyJson); // 엔진이 로직을 처리
return <Survey model={survey} />; // React는 UI만 렌더링
}
내부 동작 – SurveyJS 엔진은 전체 스키마를 차이 비교하지 않고, 가시성 조건, 동적 규칙, 검증을 실시간으로 평가합니다.
사용할 패키지
- survey‑core – 플랫폼 독립적인 설문 모델(비즈니스 로직).
- survey‑react‑ui – React 전용 렌더러.
- survey‑creator‑core – 드래그‑앤‑드롭 Survey Creator용 모델.
- survey‑creator‑react – Survey Creator용 React UI.
아키텍처에 대해 더 알아보세요: SurveyJS Architecture (링크).
SurveyJS를 선택해야 할 경우
- 폼이 오래 지속되고 시간이 지나면서 진화하는 경우.
- 조건부 로직(보이기/숨기기, 활성/비활성, 동적 검증)이 핵심인 경우.
- 대규모(수백 개 필드, 다중 페이지 설문)에서 고성능이 필요할 때.
- 검증이 복잡(사용자 정의 식, 필드 간 규칙)할 때.
- 가변 JSON 스키마를 유지하는 것이 관리 부담이 되는 경우.
React JSON‑Schema 폼은 여전히 간단하고 정적인 폼에 적합하지만, 위와 같은 시나리오에서는 SurveyJS가 더 깔끔하고 빠르며 유지보수가 용이한 솔루션을 제공합니다.
리소스
- Demo: Content‑Heavy JSON Forms – SurveyJS가 대규모 동적 폼을 처리하는 실시간 예시.
- 문서:
“React JSON‑Schema form”에 대한 대부분의 문제는 버그가 아니라 고유한 아키텍처 제한 때문입니다. SurveyJS는 무거운 작업을 React 밖으로 옮겨, 제품 팀이 렌더링 사이클을 싸우는 대신 기능 구현에 집중할 수 있게 합니다.