폼 다루기 제어 컴포넌트 왜 제어? 여러 입력 폼 제출 📝 문제풀이
◀ 이전 📋 목차 CH 09 ▶
📝 CHAPTER 08 · 중급

폼과 제어 컴포넌트

입력창·체크박스·선택창… 폼(form)은 사용자와 대화하는 통로예요. 리액트에서는 이 입력값을 state가 "단일 진실 공급원"으로 쥐고 관리합니다. 이 원리 하나만 잡으면 폼이 놀랄 만큼 단순해져요.

🎯 이 장을 끝내면
🗂️
폼 다루기
리액트에서 폼을 다루는 법
"입력값은 누가 진짜 주인인가?"가 핵심 질문이에요.

순수 HTML에서는 <input>자기 값을 스스로 기억해요. 사용자가 타이핑하면 그 값은 DOM 요소 안에 들어있고, 우리는 필요할 때 꺼내 읽죠. 그런데 리액트는 생각이 좀 달라요.

🔤
DOM — 브라우저가 HTML을 읽어 만든 "화면 요소들의 실제 구조"예요. <input> 하나하나가 이 DOM 안의 요소이고, 자바스크립트로 이 요소를 찾아 값을 읽거나 바꿀 수 있어요.
🎯 단일 진실 공급원 (Single Source of Truth)🟪 리액트 리액트에서는 입력값을 리액트 state가 유일한 "진짜 값"으로 관리해요. 화면에 보이는 입력창의 값은 state를 그대로 비춘 거울일 뿐이에요. 그래서 "지금 입력값이 뭐지?"의 답은 항상 state 한 곳에 있습니다. 입력값의 주인을 DOM이 아니라 state로 정하는 것 — 이게 리액트 폼의 출발점이에요.
📒
state = 원본(진짜 값)
"지금 입력값이 뭐지?"의 답은 언제나 여기 한 곳에 있어요.
🪞
화면 입력창 = 거울
state를 그대로 비출 뿐, 스스로 값을 기억하지 않아요.
🧭
큰 그림 먼저. "입력값 = state가 쥐고 있다"만 기억하세요. 그러면 이 장의 모든 내용이 이 한 문장 위에 얹혀요. 아래에서 그 구체적인 방법(제어 컴포넌트)을 배웁니다.
🎛️
제어 컴포넌트
value를 state로 묶는 "제어 컴포넌트"
input의 값을 state가 통제(control)한다고 해서 제어 컴포넌트예요.

제어 컴포넌트(controlled component)는 폼 요소의 valuestate에 묶고, 사용자가 입력할 때마다 onChangestate를 갱신하는 방식이에요. 두 가지를 항상 짝지어 씁니다.

import { useState } from "react";

function NameInput() {
  const [text, setText] = useState("");

  return (
    <input
      value={text}                          // ① state를 화면에 비춤
      onChange={e => setText(e.target.value)} // ② 입력 → state 갱신
    />
  );
}
🔁 두 줄이 만드는 순환🟪 리액트 ① value={text} — 화면의 입력창은 언제나 text state를 그대로 보여줘요.
② onChange — 사용자가 키를 누르면 e.target.value(새 입력값)로 setText를 호출해 state를 바꿔요.
state가 바뀌면 리액트가 다시 렌더링하고, ①에 의해 새 값이 화면에 나타나요. 이 순환 덕분에 화면과 state가 항상 일치합니다. 🍳 비유: input은 "칠판", state는 "노트". 칠판(화면)은 항상 노트(state)를 베껴 적어요. 누가 칠판에 쓰면 먼저 노트에 옮긴 뒤, 노트를 보고 칠판을 다시 그려요.
🔤
렌더링(rendering) — 리액트가 컴포넌트 함수를 다시 실행해 "지금 화면이 어떻게 생겨야 하는지"를 계산하고 그려내는 일이에요. state가 바뀌면 리액트는 그 부분을 자동으로 다시 렌더링해요.
⌨️
사용자 입력
키를 누름
🔔
onChange 발생
e.target.value 읽기
📝
setState
state 갱신
🖥️
재렌더
value에 반영
💡
핵심 공식. 제어 컴포넌트 = value={state} + onChange={e => setState(e.target.value)}. 이 두 개는 항상 세트로 다닌다고 외워두세요.
🤔
왜 제어?
굳이 왜 이렇게 할까? (제어 vs 비제어)
state가 값을 쥐고 있으면 할 수 있는 게 많아져요.

"그냥 input이 알아서 값을 기억하면 되지, 왜 state로 옮기지?" 싶을 수 있어요. 이유는 state가 값을 쥐고 있으면 그 값을 마음대로 다룰 수 있기 때문이에요.

✅ 제어 컴포넌트가 쉬워지는 것들🟪 리액트 검증 — 입력값을 state에서 바로 검사해 "10자 이하만" 같은 규칙을 즉시 적용.
변형 — 입력하는 순간 대문자로 바꾸거나, 숫자만 남기는 등 가공이 자유로움.
초기화setText("") 한 줄로 입력창 비우기.
조건부 비활성화 — 다른 값에 따라 버튼을 disabled 처리하기 쉬움.

반대편에는 비제어 컴포넌트(uncontrolled component)가 있어요. 값을 state로 묶지 않고 DOM이 스스로 기억하게 둔 뒤, 필요할 때만 ref로 값을 읽어오는 방식이에요. (ref는 다음 장 CH 09에서 자세히 배워요.)

구분제어 컴포넌트비제어 컴포넌트
값의 주인리액트 stateDOM(요소 자신)
값 읽는 법state에서 바로필요할 때 ref
핵심 코드value + onChangeref (+ defaultValue)
실시간 검증·가공쉬움 (매 입력마다)어려움
리액트 권장대부분의 경우 ✅단순 폼·특수 상황
📌
정리. 리액트는 제어 컴포넌트를 기본으로 권장해요. 비제어는 값을 실시간으로 다룰 필요가 없는 아주 단순한 폼이나 특수 상황에서 씁니다. 자세한 ref 사용법은 CH 09에서!
🧩
여러 입력
입력이 여러 개일 때 · 체크박스 · select
핸들러를 하나로 합치고, 요소마다 다른 속성을 챙겨요.

입력창이 여러 개라고 핸들러를 열 개 만들 필요는 없어요. 각 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} />
    </>
  );
}
⚠️ 요소마다 읽는 속성이 달라요🟦 순수 JS 텍스트 input · select — 값은 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를 쓰면 원하는 대로 동작하지 않습니다.
📤
폼 제출
폼 제출과 preventDefault
기본 새로고침을 막고, 리액트답게 처리해요.

폼을 제출할 때는 <form>onSubmit 핸들러를 걸어요. 그런데 브라우저는 폼을 제출하면 기본적으로 페이지를 새로고침(서버로 전송)하려 해요. SPA인 리액트에서는 이걸 막아야 하므로 e.preventDefault()를 가장 먼저 호출합니다.

🔤
SPA(Single Page Application) — 페이지를 통째로 새로 불러오지 않고, 한 페이지 안에서 자바스크립트로 화면만 바꿔 끼우는 방식의 웹앱이에요. 리액트가 대표적이죠. 그래서 폼 제출 때 일어나는 "전체 새로고침"을 막아줘야 해요.
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>
  );
}
🚫 왜 preventDefault()가 필요할까🟦 순수 JS 막지 않으면 폼 제출 순간 브라우저가 페이지를 통째로 새로고침해버려요. 그러면 리액트 앱의 state가 전부 초기화되고 화면이 깜빡이죠. e.preventDefault()기본 동작을 취소한 뒤, 값 검증·서버 전송 등을 우리 코드로 직접 처리합니다. 제어 컴포넌트로 값을 이미 state가 쥐고 있으니, 제출 때는 그 state를 그대로 꺼내 쓰면 돼요.
🧠 이 장 핵심 요약
🎁 보너스 팁

제어 컴포넌트에서 value만 주고 onChange를 빼먹으면, 입력창이 state 값에 고정되어 타이핑이 안 되는(읽기 전용) 상태가 돼요. 리액트가 콘솔로 경고도 해줍니다. value와 onChange는 항상 짝이라는 걸 다시 한번!

📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 개념결과)을 섞어 두었어요.
CH 09 📌 useRef와 기타 훅 — 렌더 없이 값을 붙잡고, DOM을 직접 만지기