React 교재 · 기초 10편

React Form — controlled vs uncontrolled

input 1개부터 회원가입 폼까지. 두 패턴 비교 + 다중 input 표준 패턴 + 제출 처리.

입력 필드와 제출 버튼이 있는 깨끗한 폼 일러스트 — React Form 컨셉

실전 웹 앱의 절반은 폼. 로그인·회원가입·검색·결제·설정. React 에서 폼 처리는 두 가지 큰 갈래 — controlled (state 가 input 값을 직접 추적) 와 uncontrolled (DOM 이 자체 관리, React 는 필요할 때만 읽음). 둘 다 알아야 한다.

이번 10편에서 두 패턴 비교 + 다중 input 의 표준 useState 패턴 + 제출 처리 + 기본 검증까지. 21편에서는 Zod + React Hook Form 같은 라이브러리 패턴으로 확장.

1. controlled — useState 가 single source of truth

가장 흔한 패턴. input 의 value 를 state 에 묶고 onChange 로 갱신.

function LoginForm() { const [email, setEmail] = useState(''); const [pwd, setPwd] = useState(''); return ( <form> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /> <input type="password" value={pwd} onChange={(e) => setPwd(e.target.value)} /> </form> ); }

매 키 입력마다 setState → re-render. 처음엔 비효율 같지만 React 가 빠르고, 입력 도중 검증·자동 변환·미리보기를 자유롭게 할 수 있는 게 진짜 가치.

왜 single source of truth 인가 — state 만 보면 input 값이 보장됨. 외부에서 reset 버튼으로 setEmail('') 하면 input 도 자동 클리어. DOM 따로 추적할 필요 0. 디버깅·테스트 압도적 우위.

2. uncontrolled — useRef 로 필요할 때 읽기

매 입력마다 state 갱신이 부담스러운 경우 (10000자 textarea 등) 또는 input 값이 바뀌어도 다른 UI 가 안 바뀌는 경우. useRef 로 DOM 참조를 잡고 제출 시점에 한 번 읽는다.

import { useRef } from 'react'; function FeedbackForm() { const textareaRef = useRef(null); const handleSubmit = (e) => { e.preventDefault(); const text = textareaRef.current.value; fetch('/api/feedback', { method: 'POST', body: text }); }; return ( <form onSubmit={handleSubmit}> <textarea ref={textareaRef} defaultValue="" /> <button>전송</button> </form> ); }

value 대신 defaultValue 사용 — DOM 이 자체 관리. React 는 ref 를 통해 필요할 때만 값을 가져온다. 재렌더 0.

3. 두 패턴 한 줄 비교

관점controlleduncontrolled
값 추적useState (매 입력 재렌더)DOM (재렌더 0)
실시간 검증·변환★ 쉽다 (state 보고)어렵다 (DOM 이벤트 직접)
외부 reset/제어★ setState 한 줄ref.current.value = '' (수동)
대량 텍스트매 입력 부담★ 부담 0
파일 input (file upload)불가 (브라우저 보안)★ 필수

실전 — 99% controlled. file input·대용량 텍스트 만 uncontrolled. 헷갈리면 controlled 부터 시작.

4. 다중 input — 객체 state 패턴

회원가입 폼처럼 input 이 5개+ 면 useState 5개는 지저분. 객체 하나로 묶고 name 속성 활용.

function SignupForm() { const [form, setForm] = useState({ email: '', password: '', name: '', age: '' }); const handleChange = (e) => { const { name, value } = e.target; setForm(prev => ({ ...prev, [name]: value })); // 6편 함수형 + 동적 키 }; const handleSubmit = (e) => { e.preventDefault(); // 검증 if (!form.email.includes('@')) return alert('이메일 형식 오류'); if (form.password.length < 8) return alert('비밀번호 8자 이상'); // 전송 fetch('/api/signup', { method: 'POST', body: JSON.stringify(form) }); }; return ( <form onSubmit={handleSubmit}> <input name="email" type="email" value={form.email} onChange={handleChange} /> <input name="password" type="password" value={form.password} onChange={handleChange} /> <input name="name" value={form.name} onChange={handleChange} /> <input name="age" type="number" value={form.age} onChange={handleChange} /> <button>가입</button> </form> ); }

핵심은 handleChange 한 함수로 모든 input 처리. name 속성을 객체 키로 사용하는 동적 키 ([name]) 가 패턴. input 개수가 늘어도 핸들러는 그대로.

검증 라이브러리 사전 예고 — 검증 로직이 5개 넘어가면 직접 if 분기 대신 Zod + React Hook Form 조합이 표준. 회사 stack 도 그렇다 (Zod·RHF 둘 다 stack 에 있음). 21편에서 정식 도입. 그때까지는 위 패턴이면 충분.

입문/기초 단계 마무리 단원이다. 11편부터 중급 — useEffect 로 side effect (데이터 fetch·구독·timer) 다루기. 진짜 인터랙티브 앱의 시작.

다음 글

React 교재 11편 — useEffect 로 side effect 다루기. 언제 실행되나, dependency array, cleanup function, 무한 루프 함정.

© 2026 주나이테크(주) @JUNAITECH