React

Form

읽는 시간: 4

React 폼 관리#

비제어 컴포넌트 (Uncontrolled Components)#

useRef를 사용하여 DOM 요소에 직접 접근하는 방식. 입력값을 React State로 관리하지 않고 제출 시점에 필요한 값을 DOM에서 읽는다.

주의사항#

  • 기본값은 defaultValue 속성으로 지정
  • value 속성을 사용하면 값이 변경되지 않음

언제 쓰나#

  • 단순 폼: 즉각적인 유효성 검사/상태 동기화가 중요하지 않을 때
  • 파일 입력: 파일 인풋은 제어가 불가하여 보통 ref로 접근
  • 성능 고려: 매우 많은 입력이 있을 때 렌더링을 최소화하고 싶을 때

예제#

text
import { useRef } from "react";

function UncontrolledForm() {
  const nameRef = useRef(null);
  const ageRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const name = nameRef.current?.value ?? "";
    const age = Number(ageRef.current?.value ?? 0);
    alert(`${name} / ${age}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} defaultValue="홍길동" placeholder="이름" />
      <input ref={ageRef} type="number" defaultValue={20} placeholder="나이" />
      <button type="submit">저장</button>
    </form>
  );
}

파일 입력 예제#

text
import { useRef } from "react";

function FileInput() {
  const fileRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const file = fileRef.current?.files?.[0];
    if (!file) return alert("파일을 선택하세요");
    console.log(file.name, file.size);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={fileRef} type="file" accept="image/*" />
      <button type="submit">업로드</button>
    </form>
  );
}

State 관리#

스프레드 구문 (얕은 복제)#

text
...value

얕은 복제는 객체/배열의 1단계 프로퍼티만 복제한다. 중첩 객체는 참조가 유지된다.

text
// 객체 업데이트
const [user, setUser] = useState({ name: "", age: 0 });
setUser((prev) => ({ ...prev, age: prev.age + 1 }));

// 배열 업데이트
const [todos, setTodos] = useState([{ id: 1, text: "a", done: false }]);
setTodos((prev) => prev.map((t) => (t.id === 1 ? { ...t, done: !t.done } : t)));

중첩된 State 관리#

  • 예시: address { city, do }와 같은 중첩 구조
  • address 업데이트 시 추가 복제 필요
  • 권장: State를 평평하게 구성 (State 정규화)
text
// 중첩 상태를 전개할 때는 단계별로 복제
const [profile, setProfile] = useState({
  name: "",
  address: { city: "", do: "" },
});

setProfile((prev) => ({
  ...prev,
  address: { ...prev.address, city: "서울" },
}));

정규화(평평하게 만들기) 예시#

text
// before
const [profile, setProfile] = useState({
  name: "",
  address: { city: "", do: "" },
});

// after: 평평한 구조 → 부분 업데이트가 단순해짐
const [name, setName] = useState("");
const [city, setCity] = useState("");
const [do_, setDo] = useState("");

Immer 라이브러리#

불가피하게 중첩된 State를 사용해야 할 때 권장되는 라이브러리. 불변성을 자동으로 유지하며, 직관적인 "변경 코드"를 작성할 수 있다.

text
npm i immer
text
import { useState } from "react";
import { produce } from "immer";

function Profile() {
  const [profile, setProfile] = useState({
    name: "",
    address: { city: "", do: "" },
  });

  const updateCity = () => {
    setProfile((prev) =>
      produce(prev, (draft) => {
        draft.address.city = "서울";
      })
    );
  };

  return (
    <div>
      <div>{profile.address.city}</div>
      <button onClick={updateCity}>도시 변경</button>
    </div>
  );
}

배열 예제:

text
const [todos, setTodos] = useState([
  { id: 1, text: "a", done: false },
  { id: 2, text: "b", done: true },
]);

const toggle = (id) => {
  setTodos((prev) =>
    produce(prev, (draft) => {
      const item = draft.find((t) => t.id === id);
      if (item) item.done = !item.done;
    })
  );
};

제어 컴포넌트 (Controlled Components)#

입력값을 React State로 완전히 관리하는 방식. valueonChange를 통해 UI ↔ 상태를 동기화한다.

장단점#

  • 장점: 즉각 유효성 검증, 버튼 비활성 같은 UI 제어, 예측 가능한 상태
  • 단점: 입력 수가 매우 많으면 렌더링 비용 증가 가능

예제#

text
import { useState } from "react";

function ControlledForm() {
  const [name, setName] = useState("");
  const [age, setAge] = useState("");

  const isValid = name.trim().length >= 2 && Number(age) > 0;

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!isValid) return;
    console.log({ name, age: Number(age) });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="이름"
      />
      <input
        value={age}
        onChange={(e) => setAge(e.target.value)}
        type="number"
        placeholder="나이"
      />
      <button type="submit" disabled={!isValid}>
        저장
      </button>
    </form>
  );
}

#

  • 파일 입력은 제어 불가 → ref 사용
  • 큰 폼은 하이브리드: 입력은 비제어 + 제출 시 FormData로 한 번에 수집
  • 성능이 문제면 onChange를 디바운스하거나 React Hook Form 같은 라이브러리 고려