이 기사는 SurveyJS의 후원으로 작성되었습니다. 대부분의 React 개발자가 큰 소리로 논의하지 않고 공유하는 정신 모델이 있습니다. 해당 양식은 항상 구성 요소로 간주됩니다. 이는 다음과 같은 스택을 의미합니다.
로컬 상태를 위한 React Hook Form(최소한의 재렌더링, 인체공학적 필드 등록, 필수 상호작용). 유효성 검사를 위한 Zod(입력 정확성, 경계 유효성 검사, 유형 안전 구문 분석) 백엔드에 대한 React 쿼리: 제출, 재시도, 캐싱, 서버 동기화 등.
그리고 로그인 화면, 설정 페이지, CRUD 모달 등 대부분의 양식에서 이는 정말 잘 작동합니다. 각 부분은 해당 작업을 수행하고 깔끔하게 구성되며 실제로 제품을 차별화하는 애플리케이션 부분으로 이동할 수 있습니다. 그러나 때때로 양식은 이전 답변에 의존하는 가시성 규칙이나 세 필드를 통해 계단식으로 전달되는 파생 값과 같은 항목을 축적하기 시작합니다. 전체 페이지를 건너뛰거나 누적 합계를 기준으로 표시해야 할 수도 있습니다. useWatch와 인라인 분기를 사용하여 첫 번째 조건을 처리합니다. 괜찮습니다. 그리고 또 다른. 그런 다음 Zod 스키마가 일반적인 방식으로 표현할 수 없는 교차 필드 규칙을 인코딩하기 위해 superRefine을 사용하게 됩니다. 그런 다음 단계 탐색에서 비즈니스 로직이 유출되기 시작합니다. 어느 시점에서 당신은 당신이 만든 것을 보고 그 양식이 더 이상 실제로 UI가 아니라는 것을 깨닫습니다. 이는 결정 프로세스에 가깝고 구성 요소 트리는 우연히 이를 저장한 위치입니다. 이것이 바로 React의 양식에 대한 정신적 모델이 무너지는 지점이라고 생각하며 이는 실제로 누구의 잘못도 아닙니다. RHF + Zod 스택은 설계된 목적이 뛰어납니다. 문제는 대안이 형식에 대해 완전히 다른 사고 방식을 요구하기 때문에 추상화가 문제와 일치하는 지점을 지나서 계속 사용하는 경향이 있다는 것입니다. 이 문서는 그 대안에 관한 것입니다. 이를 보여주기 위해 정확히 동일한 다단계 양식을 두 번 작성하겠습니다.
제출을 위해 React Hook Form + Zod를 React 쿼리에 연결하여, 양식을 구성 요소 트리가 아닌 간단한 JSON 스키마인 데이터로 처리하는 SurveyJS를 사용합니다.
동일한 요구 사항, 동일한 조건부 논리, 결국 동일한 API 호출. 그런 다음 이동한 것과 남아 있는 것을 정확히 매핑하고 어떤 모델을 언제 사용해야 할지 결정하는 실용적인 방법을 제시합니다. 우리가 만들고 있는 양식은 다음과 같습니다.
이 양식은 4단계 흐름을 사용합니다. 1단계: 세부정보
이름(필수), 이메일(필수, 유효한 형식)
2단계: 주문
단가, 수량, 세율, 파생됨: 소계, 세금, 합계.
3단계: 계정 및 피드백
계정이 있나요? (예/아니요) 예 → 사용자 이름 + 비밀번호인 경우 둘 다 필요합니다. 아니요 → 이메일은 이미 1단계에서 수집되었습니다.
만족도 평점(1~5) ≥ 4인 경우 → “무엇을 좋아하셨나요?”라고 질문하세요. ≤ 2인 경우 → “무엇을 개선할 수 있나요?”라고 질문하세요.
4단계: 검토
총합이 100보다 큰 경우에만 나타납니다. 최종 제출.
이것은 극단적이지 않습니다. 그러나 아키텍처의 차이점을 드러내는 것만으로도 충분합니다. 1부: 컴포넌트 기반(React Hook Form + Zod) 설치 npm 설치 반응 후크 형식 zod @hookform/resolvers @tanstack/react-query
조드 스키마 Zod 스키마부터 시작하겠습니다. 일반적으로 Zod 스키마에서 양식의 모양이 설정되기 때문입니다. 처음 두 단계(개인 정보 및 주문 입력)의 경우 필수 문자열, 최소값이 포함된 숫자, 열거형 등 모든 것이 간단합니다. 흥미로운 부분은 조건부 규칙을 표현하려고 할 때 시작됩니다.
"zod"에서 {z }를 가져옵니다.
import const formSchema = z.object({ firstName: z.string().min(1, "필수"), 이메일: z.string().email("잘못된 이메일"), 가격: z.number().min(0), 수량: z.number().min(1), TaxRate: z.number(), hasAccount: z.enum(["Yes", "No"]), 사용자 이름: z.string().ional(), 비밀번호: z.string().옵션(), 만족도: z.number().min(1).max(5), positiveFeedback: z.string().옵션(), 개선피드백: z.string().옵션(),}).superRefine((data, ctx) => { if (data.hasAccount === "예") { if (!data.username) { ctx.addIssue({ 코드: "custom", 경로: ["username"], 메시지: "필수" }) } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "custom", 경로: ["password"], message: "최소 6자" });
if (data.satisfaction >= 4 && !data. positiveFeedback) { ctx.addIssue({ code: "custom", path: ["PositiveFeedback"], message: "좋아요를 공유해 주세요." }); }
if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ 코드: "custom", 경로:["improvementFeedback"], 메시지: "개선할 점을 알려주세요." }); }});
내보내기 유형 FormData = z.infer
사용자 이름과 비밀번호는 조건부 필수임에도 불구하고 선택 사항()으로 입력됩니다. Zod의 유형 수준 스키마는 필드가 중요한 경우를 관리하는 규칙이 아니라 객체의 모양을 설명하기 때문입니다. 조건부 요구 사항은 모양이 검증된 후 실행되고 전체 개체에 액세스할 수 있는 superRefine 내부에 있어야 합니다. 그 분리는 결함이 아닙니다. 이는 도구가 설계된 목적입니다. superRefine은 스키마 구조 자체에서 표현될 수 없을 때 교차 필드 논리가 사용되는 곳입니다. 여기서 주목할만한 점은 이 스키마가 표현하지 않는 것입니다. 페이지 개념도 없고, 어떤 필드가 어느 시점에 표시되는지에 대한 개념도 없으며, 탐색 개념도 없습니다. 그 모든 것은 다른 곳에 살 것입니다. 양식 구성 요소
import { useForm, useWatch } from "react-hook-form";import { zodResolver } from "@hookform/resolvers/zod";import { useMutation } from "@tanstack/react-query";import { useState, useMemo } from "react";import { formSchema, type FormData } from "./schema";
const STEPS = ["세부정보", "주문", "계정", "검토"];
type OrderPayload = FormData & { 소계: 숫자; 세금: 번호; 총계: 개수 };
내보내기 함수 RHFMultiStepForm() { const [step, setStep] = useState(0);
const 돌연변이 = useMutation({ mutationFn: 비동기(페이로드: OrderPayload) => { const res = 가져오기를 기다립니다("/api/orders", { 방법: "POST", 헤더: { "Content-Type": "application/json" }, 본문: JSON.stringify(페이로드), }); if (!res.ok) throw new Error("제출 실패"); res.json()을 반환합니다. }, });
const { 등록, 제어, handlerSubmit, formState: { 오류 }, } = useForm
return (
);}sixextinction의 펜 설문조사JS-03-RHF [forked]를 참조하세요. 여기서는 꽤 많은 일이 일어나고 있으며, 일이 어떻게 끝났는지 알아차리는 데 속도를 늦출 가치가 있습니다.
파생된 값(소계, 세금, 합계)은 라이브 필드 값에 의존하고 다른 자연스러운 위치가 없기 때문에 useWatch 및 useMemo를 통해 구성 요소에서 계산됩니다. 사용자 이름, 비밀번호, positiveFeedback 및 ImprovementFeedback에 대한 가시성 규칙은 JSX에 인라인 조건으로 적용됩니다. 단계 건너뛰기 논리(전체가 100보다 큰 경우에만 나타나는 검토 페이지)는 showSubmit 변수와 3단계의 렌더링 조건에 포함됩니다. 탐색 자체는 수동으로 증가시키는 useState 카운터일 뿐입니다. React Query는 재시도, 캐싱 및 무효화를 처리합니다. 양식은 검증된 데이터로 mutation.mutate를 호출합니다.
그 자체로는 잘못된 것이 없습니다. 이것은 여전히 관용적인 React이며, RHF가 재렌더링을 분리하는 방법 덕분에 구성 요소의 성능이 상당히 뛰어납니다. 그러나 이를 작성하지 않은 사람에게 건네주고 리뷰 페이지가 어떤 조건에서 나타나는지 설명해달라고 요청한다면 그들은 showSubmit, 3단계 렌더링 조건 및 탐색 버튼 로직(세 개의 별도 위치)을 통해 추적하여 한 줄에 기술할 수 있는 규칙을 재구성해야 합니다. 예, 양식은 작동하지만 동작을 시스템으로 실제로 검사할 수는 없습니다. 정신적으로 실행해야합니다. 더 중요한 것은 이를 변경하려면 엔지니어링 참여가 필요하다는 것입니다. 검토 단계가 표시되는 시기 조정과 같은 작은 조정이라도 구성 요소 편집, 유효성 검사 업데이트, 풀 요청 열기, 검토 대기 및 다시 배포를 의미합니다. 2부: 스키마 기반(SurveyJS) 이제 스키마를 사용하여 동일한 흐름을 구축해 보겠습니다. 설치 npm 설치 설문 조사-핵심 조사-반응-ui @tanstack/react-query
Survey-coreSurveyJS의 양식 렌더링을 지원하는 MIT 라이선스 플랫폼 독립적 런타임 엔진입니다. 여기서 우리가 관심을 두는 부분입니다. JSON 스키마를 가져와 내부 모델을 구축하고 가시성 표현 평가, 파생 값 계산, 페이지 상태 관리, 유효성 검사 추적, 실제로 표시된 페이지에 따라 "완료"가 무엇을 의미하는지 결정 등 React 구성 요소에 존재할 모든 것을 처리합니다.
Survey-react-ui해당 모델을 React에 연결하는 UI/렌더링 레이어입니다. 본질적으로 엔진 상태가 변경될 때마다 다시 렌더링하는
이 두 가지를 함께 사용하면 단 한 줄의 제어 흐름도 작성하지 않고도 완전한 기능을 갖춘 다중 페이지 양식 런타임을 제공할 수 있습니다. 스키마 형식 자체는 앞서 말했듯이 JSON일 뿐이며 DSL이나 독점 항목은 없습니다. 인라인화하거나, 파일에서 가져오거나, API에서 가져오거나, 데이터베이스 열에 저장하고 런타임에 수화할 수 있습니다. 데이터와 동일한 형식 이번에는 JSON 개체로 표현된 동일한 형식이 있습니다. 스키마는 구조, 유효성 검사, 가시성 규칙, 파생 계산, 페이지 탐색 등 모든 것을 정의하고 이를 런타임에 평가하는 모델에 전달합니다. 전체 내용은 다음과 같습니다.
import const SurveySchema = { title: "주문 흐름", showProgressBar: "top", 페이지: [ { name: "details", elements: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true, validators: [{ type: "email", text: "잘못된 이메일" }] } ] }, { 이름: "주문", 요소: [ { 유형: "텍스트", 이름: "가격", inputType: "번호", defaultValue: 0 }, { 유형: "텍스트", 이름: "수량", inputType: "번호", defaultValue: 1 }, { 유형: "드롭다운",이름: "taxRate", defaultValue: 0.1, 선택: [ { 값: 0.05, 텍스트: "5%" }, { 값: 0.1, 텍스트: "10%" }, { 값: 0.15, 텍스트: "15%" } ] }, { 유형: "식", 이름: "소계", 식: "{price} {수량}" }, { 유형: "식", 이름: "tax", 표현식: "{subtotal} {taxRate}" }, { type: "expression", name: "total", 표현식: "{subtotal} + {tax}" } ] }, { name: "account", elements: [ { type: "radiogroup", name: "hasAccount",Choices: ["Yes", "No"] }, { type: "text", name: "username", visibleIf: "{hasAccount} = '예'", isRequired: true }, { type: "text", name: "password", inputType: "password", visibleIf: "{hasAccount} = 'Yes'", isRequired: true, validators: [{ type: "text", minLength: 6, text: "최소 6자" }] }, { type: "등급", name: "satisfaction", rateMin: 1, rateMax: 5 }, { type: "comment", name: "PositiveFeedback", visibleIf: "{satisfaction} >= 4" }, { type: "comment", name: "improvementFeedback", visibleIf: "{satisfaction} <= 2" } ] }, { name: "review", visibleIf: "{total} >= 100", 요소: [] } ]};
잠시 이것을 RHF 버전과 비교해 보세요.
조건부로 사용자 이름과 비밀번호를 요구하는 superRefine 블록이 사라졌습니다. visibleIf: "{hasAccount} = 'Yes'" isRequired: true와 결합하여 필드 자체에서 두 가지 문제를 함께 처리합니다. 소계, 세금 및 합계를 계산한 useWatch + useMemo 체인은 서로 이름을 참조하는 세 개의 표현식 필드로 대체됩니다. RHF 버전에서는 3단계 렌더링 분기인 showSubmit을 통한 추적을 통해서만 재구성할 수 있었던 리뷰 페이지 조건입니다. 마지막으로 탐색 버튼 논리는 페이지 개체의 단일 visibleIf 속성입니다.
같은 논리가 있습니다. 단지 스키마는 구성 요소 전체에 분산되어 있지 않고 별도로 표시되는 곳에 살 수 있는 장소를 제공하는 것뿐입니다. 또한 스키마는 소계, 세금 및 합계에 'expression' 유형을 사용합니다. 표현식은 읽기 전용이며 주로 계산된 값을 표시하는 데 사용됩니다. SurveyJS는 정적 콘텐츠에 대해 type: 'html'도 지원하지만 계산된 값의 경우 표현식이 올바른 선택입니다. 이제 React 쪽입니다. 렌더링 및 제출 매우 간단합니다. useMutation 또는 일반 가져오기를 통해 동일한 방식으로 onComplete를 API에 연결합니다.
import { useState, useEffect, useRef } from "react";import { useMutation } from "@tanstack/react-query";import { Model } from "survey-core";import { Survey } from "survey-react-ui";import "survey-core/survey-core.css";
내보내기 함수 SurveyForm() { const [model] = useState(() => new Model(surveySchema));
const 돌연변이 = useMutation({ mutationFn: 비동기(데이터) => { const res = 가져오기를 기다립니다("/api/orders", { 방법: "POST", 헤더: { "Content-Type": "application/json" }, 본문: JSON.stringify(데이터), }); if (!res.ok) throw new Error("제출 실패"); res.json()을 반환합니다. }, });
const mutationRef = useRef(돌연변이); mutationRef.current = 돌연변이; useEffect(() => { const handler = (sender) => mutationRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // ref는 렌더링할 때마다 핸들러를 다시 등록하는 것을 방지합니다(돌연변이 객체 ID 변경).
반환 ( <> <설문조사 모델={model} /> {mutation.isError &&
sixextinction의 Pen SurveyJS-03-SurveyJS [forked]를 참조하세요.
onComplete는 사용자가 마지막으로 표시되는 페이지의 끝에 도달하면 실행됩니다. 따라서 총계가 100을 넘지 않고 검토 페이지를 건너뛰더라도 SurveyJS는 "마지막 페이지"의 의미를 결정하기 전에 가시성을 평가하므로 여전히 올바르게 실행됩니다. 그런 다음 sender.data에는 계산된 값(소계, 세금, 합계)과 함께 모든 답변이 일급 필드로 포함되므로 API 페이로드는 RHF 버전이 onSubmit에서 수동으로 수집한 것과 동일합니다. 그만큼mutationRef 패턴은 렌더링할 때마다 변경되는 값에 대한 안정적인 이벤트 핸들러가 필요한 모든 곳에서 사용할 수 있는 것과 동일하며 SurveyJS와 관련된 것은 아닙니다.
React 구성 요소에는 더 이상 비즈니스 로직이 전혀 포함되어 있지 않습니다. useWatch도 없고, 조건부 JSX도 없고, 걸음 수 카운터도 없고, useMemo 체인도 없고, superRefine도 없습니다. React는 실제로 잘하는 일, 즉 구성 요소를 렌더링하고 이를 API 호출에 연결하는 작업을 수행합니다. React에서 나온 것은 무엇입니까?
우려 RHF 스택 SurveyJS 가시성 JSX 브랜치 보이는 경우 파생된 값 useWatch / useMemo 표현 교차 필드 규칙 슈퍼정제 스키마 조건 네비게이션 단계 상태 페이지가 표시되는 경우 규칙 위치 여러 파일에 분산됨 스키마에 중앙 집중화
React에 남아 있는 것은 레이아웃, 스타일링, 제출 배선 및 앱 통합입니다. 즉, React가 실제로 설계된 것입니다. 다른 모든 항목은 스키마로 이동했으며 스키마는 JSON 개체이므로 데이터베이스에 저장하거나 애플리케이션 코드와 독립적으로 버전을 지정하거나 배포할 필요 없이 내부 도구를 통해 편집할 수 있습니다. 리뷰 페이지를 트리거하는 임계값을 변경해야 하는 제품 관리자는 구성 요소를 건드리지 않고도 이를 수행할 수 있습니다. 이는 양식 동작이 자주 진화하고 항상 엔지니어에 의해 주도되지 않는 팀에게는 의미 있는 운영상의 차이입니다. 각 접근 방식을 언제 사용해야 합니까? 나에게 맞는 좋은 경험 법칙은 다음과 같습니다. 양식을 완전히 삭제한다고 상상해 보세요. 무엇을 잃게 될까요?
화면이라면 구성 요소 기반 양식이 필요합니다. 실제 결정을 인코딩하는 임계값, 분기 규칙, 조건부 요구 사항과 같은 비즈니스 논리라면 스키마 엔진이 필요합니다.
마찬가지로, 앞으로 다가올 변경 사항이 대부분 레이블, 필드 및 레이아웃에 관한 것이라면 RHF가 도움이 될 것입니다. 운영팀이나 법무팀이 티켓을 제출하지 않고 화요일 오후에 조정해야 할 조건, 결과, 규칙에 관한 것이라면 SurveyJS를 사용한 스키마 모델이 더 정직하게 적합합니다. 이 두 가지 접근 방식은 실제로 서로 경쟁 관계에 있지 않습니다. 그들은 다양한 종류의 문제를 다루며, 피해야 할 실수는 추상화와 논리의 가중치가 일치하지 않는 것입니다. 즉, 익숙한 도구이기 때문에 규칙 시스템을 구성 요소처럼 취급하거나 양식이 3단계로 증가하고 조건부 필드를 획득했기 때문에 정책 엔진에 도달하는 것입니다. 우리가 여기서 구축한 형태는 의도적으로 경계 근처에 위치하며 차이를 드러낼 만큼 복잡하지만 비교가 조작된 것처럼 느껴질 정도로 극단적이지는 않습니다. 코드베이스에서 다루기 어려워진 대부분의 실제 형식은 아마도 동일한 경계 근처에 있을 것이며, 문제는 일반적으로 누군가가 실제로 그것이 무엇인지 명명했는지 여부입니다. 다음과 같은 경우 React Hook Form + Zod를 사용하세요.
양식은 CRUD 지향적입니다. 논리는 얕고 UI 중심입니다. 엔지니어는 모든 행동을 소유합니다. 백엔드는 여전히 진실의 원천입니다.
다음과 같은 경우 SurveyJS를 사용하세요.
양식은 비즈니스 결정을 인코딩합니다. 규칙은 UI와 독립적으로 발전합니다. 논리는 표시, 감사 또는 버전 관리가 가능해야 합니다. 엔지니어가 아닌 사람도 행동에 영향을 미칩니다. 동일한 양식이 여러 프런트엔드에서 실행되어야 합니다.