React 19 폼, 타입까지 설계해야 완성된다

React 19 폼, 타입까지 설계해야 완성된다

useFormState·useFormStatus로 서버 응답을 연결하고, JSON 자동 생성 타입의 허점을 직접 메워야 비로소 폼이 안전해진다.

React 19 useFormState useFormStatus Server Action TypeScript 타입 설계 JSON to TypeScript 폼 접근성 프로그레시브 인핸스먼트
광고

폼은 프론트엔드에서 가장 자주 만들면서도 가장 쉽게 망가지는 UI다. 제출 중 버튼은 여전히 활성화되어 있고, 서버가 돌려준 오류 메시지는 어디에도 보이지 않으며, 스크린 리더는 어떤 필드가 잘못됐는지 알 방법이 없다. React 19는 이 고질적인 문제들을 useFormStateuseFormStatus라는 두 훅으로 정면 돌파한다.

두 훅이 해결하는 문제

React 19 이전의 Server Action 폼은 useState로 pending 상태를 직접 관리하고, useTransition으로 로딩 피드백을 구현하며, 또 별도의 state로 오류 메시지를 화면에 띄워야 했다. 보일러플레이트가 폼 로직보다 많아지는 상황이 비일비재했다. dev.to에 공개된 Next.js App Router 예시는 이 흐름을 명쾌하게 정리한다. useFormStatus는 가장 가까운 부모 <form>의 제출 상태를 읽어 pending 여부를 알려주고, useFormState는 Server Action의 반환값을 클라이언트에서 곧바로 소비할 수 있게 연결한다. 두 훅을 함께 쓰면 수동 state 없이도 '제출 중 비활성 버튼 → 필드별 서버 오류 표시 → 성공 시 화면 전환'이라는 완결된 흐름이 만들어진다.

프로그레시브 인핸스먼트가 공짜로 따라온다

이 패턴의 가장 과소평가된 장점은 JavaScript 없이도 동작한다는 점이다. 네이티브 <form>action 속성에 Server Action을 연결하면, JS가 비활성화된 환경에서도 서버가 요청을 받아 응답을 돌려준다. JS가 활성화되어 있으면 페이지 새로고침 없이 pending 피드백이 추가되는 구조다. 접근성 측면에서도 aria-describedby로 오류 메시지를 필드에 연결하고, role="alert"로 스크린 리더에 즉시 고지하는 패턴이 자연스럽게 녹아든다. 폼 UX의 기본기를 한 번에 챙길 수 있는 조합이다.

그런데 타입은 누가 설계하는가

여기서 놓치기 쉬운 지점이 있다. useFormState가 반환하는 상태 객체, 즉 FormState 타입은 개발자가 직접 설계해야 한다. 그리고 많은 팀이 이 타입을 API 응답 샘플에서 자동 생성한 뒤 그냥 쓴다. dev.to의 JSON-to-TypeScript 기사는 이 습관의 위험성을 정확히 짚는다. 자동 생성기는 샘플 하나만 본다. "avatar": null이면 null로 추론하고, "tags": []이면 any[]로 만든다. 응답에 따라 middleName이 빠질 수 있는데 샘플에 있었다면 옵셔널 표시도 없다. status"active" | "banned" | "pending" 유니온이어야 하는데 샘플이 "active" 하나라면 그냥 string이 된다.

자동 생성 타입이 폼에서 구체적으로 무너지는 순간

FormState를 예로 들면, Server Action이 필드별 오류를 fieldErrors?: Record<string, string>으로 반환하는 구조가 흔하다. 자동 생성기에 성공 응답 샘플만 넣으면 fieldErrors는 아예 나타나지 않는다. 실패 샘플을 넣으면 fieldErrors는 등장하지만 success 필드가 false인 케이스만 반영되고, 두 케이스를 합친 유니온 타입은 손으로 써야 한다. 결국 자동 생성 타입은 초안이지 계약이 아니다. Nullable, Optional, 유니온—런타임 버그가 숨는 곳이 정확히 이 세 지점이다.

실무 워크플로우: 생성 → 검토 → 보강

현실적인 접근은 세 단계다. 첫째, json-schema-to-typescriptopenapi-typescript처럼 스키마 기반 생성기를 우선 활용한다. API 스펙이 있다면 Nullable과 옵셔널이 이미 인코딩되어 있어 샘플 기반보다 훨씬 정확하다. 둘째, 스키마가 없다면 샘플 자동 생성 후 필드를 전부 읽는다. 특히 null이 될 수 있는 필드, 배열 원소 타입, 서버가 조건부로 내보내는 필드를 직접 좁힌다. 셋째, FormState처럼 성공·실패 두 케이스를 가진 타입은 디스크리미네이티드 유니온으로 설계한다. { success: true; data: ... } | { success: false; fieldErrors: ...; error?: string } 형태가 되면 TypeScript가 분기마다 올바른 타입을 강제한다.

AI 도구와의 시너지

v0.dev나 Cursor로 폼 컴포넌트를 빠르게 스캐폴딩할 때도 같은 원칙이 적용된다. AI가 생성한 FormState 인터페이스는 대부분 단일 응답 케이스 기준으로 만들어진다. 이때 AI 출력을 그대로 믿기보다, 유니온 타입으로 보강하고 Server Action 시그니처와 클라이언트 소비 지점을 동시에 검토하는 루틴이 필요하다. '빠른 생성 → 타입 검토 → 보강'이라는 루프는 AI 도구에도, JSON 자동 생성기에도 동일하게 적용된다.

전망: 폼 설계의 무게중심 이동

React 19의 폼 훅들은 클라이언트 state 관리의 무게를 Server Action 쪽으로 옮겼다. 이는 개발 속도를 높이는 동시에, 서버-클라이언트 경계에서의 타입 정합성이 그만큼 중요해졌다는 의미이기도 하다. 서버가 돌려주는 FormState의 모든 케이스를 타입으로 표현하지 않으면, TypeScript를 쓰면서도 런타임 오류를 방지하지 못하는 역설에 빠진다. 폼을 완성한다는 것은 훅을 연결하는 것만이 아니라, 그 경계를 흐르는 데이터의 타입을 끝까지 설계하는 일이다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요