제어 컴포넌트 패턴으로 폼을 구현하는 방법, 다양한 입력 요소 처리, 폼 유효성 검사, 제출 처리까지 실전 폼 구현을 배웁니다.
📝 폼 처리의 두 가지 방식
리액트에서 폼(form)을 처리하는 방식은 크게 두 가지입니다. 제어 컴포넌트(Controlled Component)와 비제어 컴포넌트(Uncontrolled Component)입니다. 이 강의에서는 리액트의 표준 방식인 제어 컴포넌트를 집중적으로 다룹니다.
| 구분 | 제어 컴포넌트 | 비제어 컴포넌트 |
|---|---|---|
| 상태 관리 | React state | DOM 참조 (useRef) |
| 값 접근 | state 변수 | ref.current.value |
| 검증 시점 | 입력 즉시 가능 | 제출 시점만 |
| 사용 빈도 | 대부분의 경우 | 파일 업로드, 제3자 라이브러리 통합 |
🎛️ 제어 컴포넌트 — 실전 패턴
제어 컴포넌트란 value가 React state로 관리되는 입력 요소입니다. 입력값이 바뀔 때마다 state가 업데이트되고, state가 곧 입력창에 표시되는 값이 됩니다. state가 "단일 진실의 원천(single source of truth)"이 됩니다.
function ControlledInput() {
const [name, setName] = useState('')
return (
<div>
{/* value={name}: 상태가 입력값을 제어 */}
{/* onChange: 사용자 입력 → 상태 업데이트 */}
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="이름 입력"
/>
<p>입력된 이름: {name}</p>
</div>
)
}
📑 여러 필드를 하나의 state로 관리
회원가입 폼처럼 여러 필드가 있을 때, 필드마다 useState를 만들 수도 있지만 객체 하나로 묶어 관리하면 더 편합니다:
interface SignupForm {
name: string
email: string
password: string
agree: boolean
}
function SignupPage() {
const [form, setForm] = useState<SignupForm>({
name: '',
email: '',
password: '',
agree: false,
})
// 재사용 가능한 범용 핸들러
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const { name, value, type, checked } = e.target
setForm((prev) => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}))
}
return (
<form>
<input name="name" value={form.name} onChange={handleChange} placeholder="이름" />
<input name="email" type="email" value={form.email} onChange={handleChange} placeholder="이메일" />
<input name="password" type="password" value={form.password} onChange={handleChange} placeholder="비밀번호" />
<label>
<input name="agree" type="checkbox" checked={form.agree} onChange={handleChange} />
이용약관 동의
</label>
</form>
)
}
[name]: value는 컴퓨티드 프로퍼티 이름(computed property name)입니다. 입력 요소의 name 속성과 state 키가 일치하면 하나의 핸들러로 모든 필드를 처리할 수 있습니다.
✅ 폼 유효성 검사
제출 전에 입력값을 검증해야 합니다. 간단한 검증은 직접 구현합니다:
function LoginForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [errors, setErrors] = useState<Record<string, string>>({})
function validate(): boolean {
const newErrors: Record<string, string> = {}
if (!email) newErrors.email = '이메일을 입력하세요'
else if (!/^[^@]+@[^@]+\.[^@]+$/.test(email)) newErrors.email = '올바른 이메일 형식이 아닙니다'
if (!password) newErrors.password = '비밀번호를 입력하세요'
else if (password.length < 8) newErrors.password = '8자 이상 입력하세요'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!validate()) return // 검증 실패 시 제출 중단
// API 호출
console.log('로그인:', email, password)
}
return (
<form onSubmit={handleSubmit}>
<div>
<input value={email} onChange={(e) => setEmail(e.target.value)} type="email" />
{errors.email && <p className="text-red-500 text-sm">{errors.email}</p>}
</div>
<div>
<input value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
{errors.password && <p className="text-red-500 text-sm">{errors.password}</p>}
</div>
<button type="submit">로그인</button>
</form>
)
}
🎯 select와 textarea
select와 textarea도 제어 컴포넌트로 동일하게 처리합니다:
function FeedbackForm() {
const [category, setCategory] = useState('bug')
const [message, setMessage] = useState('')
return (
<form>
{/* select — value와 onChange로 제어 */}
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="bug">버그 신고</option>
<option value="feature">기능 요청</option>
<option value="other">기타</option>
</select>
{/* textarea — 동일한 방식 */}
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={5}
placeholder="내용을 입력하세요"
/>
</form>
)
}
✅ 정리
- 제어 컴포넌트:
value를 state로 관리하고onChange로 업데이트합니다. - 여러 필드는 객체 state +
[name]computed key로 하나의 핸들러가 처리합니다. onSubmit에서e.preventDefault()후 검증 → API 호출 순서로 진행합니다.- select와 textarea도 동일한 제어 패턴을 사용합니다.
관련 주제
- map으로 리스트 렌더링
- key 속성 역할
- key 주의점
- 동적 목록 생성
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 리액트 입문 — 컴포넌트로 만드는 웹
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
0/1000
불러오는 중...
