입력창·체크박스·선택창… 폼(form)은 사용자와 대화하는 통로예요. 리액트에서는 이 입력값을 state가 "단일 진실 공급원"으로 쥐고 관리합니다. 이 원리 하나만 잡으면 폼이 놀랄 만큼 단순해져요.
value + onChange로 직접 만들 수 있어요.e.preventDefault()가 왜 필요한지 이해해요.순수 HTML에서는 <input>이 자기 값을 스스로 기억해요. 사용자가 타이핑하면 그 값은 DOM 요소 안에 들어있고, 우리는 필요할 때 꺼내 읽죠. 그런데 리액트는 생각이 좀 달라요.
<input> 하나하나가 이 DOM 안의 요소이고, 자바스크립트로 이 요소를 찾아 값을 읽거나 바꿀 수 있어요.제어 컴포넌트(controlled component)는 폼 요소의 value를 state에 묶고, 사용자가 입력할 때마다 onChange로 state를 갱신하는 방식이에요. 두 가지를 항상 짝지어 씁니다.
import { useState } from "react";
function NameInput() {
const [text, setText] = useState("");
return (
<input
value={text} // ① state를 화면에 비춤
onChange={e => setText(e.target.value)} // ② 입력 → state 갱신
/>
);
}
text state를 그대로 보여줘요.e.target.value(새 입력값)로 setText를 호출해 state를 바꿔요.value={state} + onChange={e => setState(e.target.value)}. 이 두 개는 항상 세트로 다닌다고 외워두세요."그냥 input이 알아서 값을 기억하면 되지, 왜 state로 옮기지?" 싶을 수 있어요. 이유는 state가 값을 쥐고 있으면 그 값을 마음대로 다룰 수 있기 때문이에요.
setText("") 한 줄로 입력창 비우기.disabled 처리하기 쉬움.
반대편에는 비제어 컴포넌트(uncontrolled component)가 있어요. 값을 state로 묶지 않고 DOM이 스스로 기억하게 둔 뒤, 필요할 때만 ref로 값을 읽어오는 방식이에요. (ref는 다음 장 CH 09에서 자세히 배워요.)
| 구분 | 제어 컴포넌트 | 비제어 컴포넌트 |
|---|---|---|
| 값의 주인 | 리액트 state | DOM(요소 자신) |
| 값 읽는 법 | state에서 바로 | 필요할 때 ref로 |
| 핵심 코드 | value + onChange | ref (+ defaultValue) |
| 실시간 검증·가공 | 쉬움 (매 입력마다) | 어려움 |
| 리액트 권장 | 대부분의 경우 ✅ | 단순 폼·특수 상황 |
ref 사용법은 CH 09에서!입력창이 여러 개라고 핸들러를 열 개 만들 필요는 없어요. 각 input에 name을 주고, 하나의 핸들러에서 e.target.name으로 어떤 필드가 바뀌었는지 분기하면 됩니다. state는 객체 하나로 묶어 관리해요.
function SignupForm() {
const [form, setForm] = useState({ id: "", email: "" });
function handleChange(e) {
const { name, value } = e.target; // 어떤 필드? 무슨 값?
setForm(prev => ({ ...prev, [name]: value })); // 그 필드만 갱신
}
return (
<>
<input name="id" value={form.id} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
</>
);
}
e.target.value에서 읽고, value 속성으로 제어해요.value가 아니라 checked로 제어하고 e.target.checked(true/false)를 읽어요.
// 체크박스는 checked로 제어!
<input
type="checkbox"
checked={agree} // value가 아니라 checked
onChange={e => setAgree(e.target.checked)} // .value가 아니라 .checked
/>
// select는 value로 제어 (텍스트 input과 동일)
<select value={city} onChange={e => setCity(e.target.value)}>
<option value="seoul">서울</option>
<option value="busan">부산</option>
</select>
checked — 값(value)이 아니라 선택 여부(true/false)를 다뤄요. 여기서 value를 쓰면 원하는 대로 동작하지 않습니다.폼을 제출할 때는 <form>에 onSubmit 핸들러를 걸어요. 그런데 브라우저는 폼을 제출하면 기본적으로 페이지를 새로고침(서버로 전송)하려 해요. SPA인 리액트에서는 이걸 막아야 하므로 e.preventDefault()를 가장 먼저 호출합니다.
function LoginForm() {
const [id, setId] = useState("");
function handleSubmit(e) {
e.preventDefault(); // ① 새로고침(기본 동작) 막기
console.log("제출된 값:", id); // ② 여기서 값 처리 / 서버 전송 등
}
return (
<form onSubmit={handleSubmit}>
<input value={id} onChange={e => setId(e.target.value)} />
<button type="submit">로그인</button>
</form>
);
}
e.preventDefault()로 기본 동작을 취소한 뒤, 값 검증·서버 전송 등을 우리 코드로 직접 처리합니다.
제어 컴포넌트로 값을 이미 state가 쥐고 있으니, 제출 때는 그 state를 그대로 꺼내 쓰면 돼요.
value={state} + onChange={e => setState(e.target.value)}. 화면과 state가 항상 일치.ref로 읽는 비제어와 대조돼요.name + 하나의 핸들러 + 객체 state로. 체크박스는 checked, select는 value.onSubmit에서 e.preventDefault()로 새로고침을 막고 처리해요.제어 컴포넌트에서 value만 주고 onChange를 빼먹으면, 입력창이 state 값에 고정되어 타이핑이 안 되는(읽기 전용) 상태가 돼요. 리액트가 콘솔로 경고도 해줍니다. value와 onChange는 항상 짝이라는 걸 다시 한번!