React 독학 가이드 9 - 사용자 입력 관리하기

learning by Seven Fingers Studio 18분
ReactFormInputControlledComponent폼관리

웹앱에서 폼은 필수죠. 로그인, 회원가입, 검색, 게시글 작성… 거의 모든 곳에 폼이 있어요. React에서 폼을 다루는 방법을 제대로 배워봅시다.

Controlled vs Uncontrolled Component

React에서 입력을 다루는 방식은 두 가지예요.

Uncontrolled Component (비제어 컴포넌트)

DOM이 직접 입력값을 관리해요. HTML의 기본 동작이죠.

function UncontrolledForm() {
  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    console.log(formData.get("username"));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" defaultValue="" />
      <button type="submit">제출</button>
    </form>
  );
}

Controlled Component (제어 컴포넌트)

React State가 입력값을 관리해요. 대부분 이 방식을 씁니다.

function ControlledForm() {
  const [username, setUsername] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(username);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <button type="submit">제출</button>
    </form>
  );
}

왜 Controlled를 쓰나요?

  • 실시간 유효성 검사 가능
  • 입력값 가공/제한 가능
  • 다른 컴포넌트와 값 공유 쉬움
  • React스러운 방식

텍스트 입력 다루기

기본 텍스트 input

import { useState } from 'react';

function TextInput() {
  const [name, setName] = useState("");

  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="이름을 입력하세요"
      />
      <p>입력값: {name}</p>
    </div>
  );
}

실행 결과:

입력값: 홍길동

입력창에 타이핑하면 실시간으로 아래에 표시됩니다

입력값 제한하기

function LimitedInput() {
  const [text, setText] = useState("");
  const maxLength = 50;

  const handleChange = (e) => {
    if (e.target.value.length <= maxLength) {
      setText(e.target.value);
    }
  };

  return (
    <div>
      <input value={text} onChange={handleChange} />
      <p>{text.length} / {maxLength}</p>
    </div>
  );
}

숫자만 입력받기

function NumberOnlyInput() {
  const [number, setNumber] = useState("");

  const handleChange = (e) => {
    const value = e.target.value;
    // 숫자만 허용
    if (/^\d*$/.test(value)) {
      setNumber(value);
    }
  };

  return (
    <input
      value={number}
      onChange={handleChange}
      placeholder="숫자만 입력"
    />
  );
}

Textarea 다루기

HTML에서는 <textarea>내용</textarea>이지만, React에서는 value 속성을 써요.

function TextareaExample() {
  const [content, setContent] = useState("");

  return (
    <div>
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        rows={5}
        placeholder="내용을 입력하세요"
        style={{ width: '100%', padding: '10px' }}
      />
      <p>글자 수: {content.length}</p>
    </div>
  );
}

Select (드롭다운) 다루기

function SelectExample() {
  const [fruit, setFruit] = useState("apple");

  return (
    <div>
      <select value={fruit} onChange={(e) => setFruit(e.target.value)}>
        <option value="">선택하세요</option>
        <option value="apple">사과</option>
        <option value="banana">바나나</option>
        <option value="orange">오렌지</option>
      </select>
      <p>선택된 과일: {fruit}</p>
    </div>
  );
}

동적으로 옵션 생성

function DynamicSelect() {
  const [country, setCountry] = useState("");

  const countries = [
    { code: "KR", name: "한국" },
    { code: "US", name: "미국" },
    { code: "JP", name: "일본" },
    { code: "CN", name: "중국" }
  ];

  return (
    <select value={country} onChange={(e) => setCountry(e.target.value)}>
      <option value="">국가 선택</option>
      {countries.map((c) => (
        <option key={c.code} value={c.code}>
          {c.name}
        </option>
      ))}
    </select>
  );
}

Checkbox 다루기

단일 체크박스

function SingleCheckbox() {
  const [agreed, setAgreed] = useState(false);

  return (
    <label>
      <input
        type="checkbox"
        checked={agreed}
        onChange={(e) => setAgreed(e.target.checked)}
      />
      약관에 동의합니다
    </label>
  );
}

여러 개 체크박스

function MultipleCheckbox() {
  const [selected, setSelected] = useState([]);

  const options = ["React", "Vue", "Angular", "Svelte"];

  const handleChange = (option) => {
    if (selected.includes(option)) {
      // 이미 있으면 제거
      setSelected(selected.filter((item) => item !== option));
    } else {
      // 없으면 추가
      setSelected([...selected, option]);
    }
  };

  return (
    <div>
      <p>관심 기술을 선택하세요:</p>
      {options.map((option) => (
        <label key={option} style={{ display: 'block' }}>
          <input
            type="checkbox"
            checked={selected.includes(option)}
            onChange={() => handleChange(option)}
          />
          {option}
        </label>
      ))}
      <p>선택됨: {selected.join(", ") || "없음"}</p>
    </div>
  );
}

실행 결과:

관심 기술을 선택하세요:

선택됨: React, Angular

체크박스를 클릭하면 선택 항목이 배열로 관리됩니다

Radio 버튼 다루기

function RadioExample() {
  const [gender, setGender] = useState("");

  return (
    <div>
      <p>성별:</p>
      <label>
        <input
          type="radio"
          name="gender"
          value="male"
          checked={gender === "male"}
          onChange={(e) => setGender(e.target.value)}
        />
        남성
      </label>
      <label>
        <input
          type="radio"
          name="gender"
          value="female"
          checked={gender === "female"}
          onChange={(e) => setGender(e.target.value)}
        />
        여성
      </label>
      <p>선택: {gender || "없음"}</p>
    </div>
  );
}

실전 예제: 회원가입 폼

import { useState } from 'react';

function SignupForm() {
  const [form, setForm] = useState({
    username: "",
    email: "",
    password: "",
    confirmPassword: "",
    agreeTerms: false
  });

  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setForm({
      ...form,
      [name]: type === "checkbox" ? checked : value
    });
  };

  const validate = () => {
    const newErrors = {};

    if (!form.username) {
      newErrors.username = "사용자명을 입력하세요";
    } else if (form.username.length < 3) {
      newErrors.username = "3자 이상 입력하세요";
    }

    if (!form.email) {
      newErrors.email = "이메일을 입력하세요";
    } else if (!/\S+@\S+\.\S+/.test(form.email)) {
      newErrors.email = "올바른 이메일 형식이 아닙니다";
    }

    if (!form.password) {
      newErrors.password = "비밀번호를 입력하세요";
    } else if (form.password.length < 6) {
      newErrors.password = "6자 이상 입력하세요";
    }

    if (form.password !== form.confirmPassword) {
      newErrors.confirmPassword = "비밀번호가 일치하지 않습니다";
    }

    if (!form.agreeTerms) {
      newErrors.agreeTerms = "약관에 동의해주세요";
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate()) {
      console.log("가입 성공!", form);
      alert("가입이 완료되었습니다!");
    }
  };

  const inputStyle = {
    width: '100%',
    padding: '10px',
    marginBottom: '5px',
    borderRadius: '4px',
    border: '1px solid #ddd'
  };

  const errorStyle = {
    color: 'red',
    fontSize: '12px',
    marginBottom: '10px'
  };

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '400px' }}>
      <h2>회원가입</h2>

      <div>
        <input
          type="text"
          name="username"
          value={form.username}
          onChange={handleChange}
          placeholder="사용자명"
          style={inputStyle}
        />
        {errors.username && <p style={errorStyle}>{errors.username}</p>}
      </div>

      <div>
        <input
          type="email"
          name="email"
          value={form.email}
          onChange={handleChange}
          placeholder="이메일"
          style={inputStyle}
        />
        {errors.email && <p style={errorStyle}>{errors.email}</p>}
      </div>

      <div>
        <input
          type="password"
          name="password"
          value={form.password}
          onChange={handleChange}
          placeholder="비밀번호"
          style={inputStyle}
        />
        {errors.password && <p style={errorStyle}>{errors.password}</p>}
      </div>

      <div>
        <input
          type="password"
          name="confirmPassword"
          value={form.confirmPassword}
          onChange={handleChange}
          placeholder="비밀번호 확인"
          style={inputStyle}
        />
        {errors.confirmPassword && (
          <p style={errorStyle}>{errors.confirmPassword}</p>
        )}
      </div>

      <div>
        <label>
          <input
            type="checkbox"
            name="agreeTerms"
            checked={form.agreeTerms}
            onChange={handleChange}
          />
          이용약관에 동의합니다
        </label>
        {errors.agreeTerms && <p style={errorStyle}>{errors.agreeTerms}</p>}
      </div>

      <button
        type="submit"
        style={{
          width: '100%',
          padding: '12px',
          backgroundColor: '#007bff',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
          marginTop: '10px'
        }}
      >
        가입하기
      </button>
    </form>
  );
}

실전 예제: 검색 필터

import { useState } from 'react';

function SearchFilter() {
  const [filters, setFilters] = useState({
    keyword: "",
    category: "all",
    minPrice: "",
    maxPrice: "",
    inStock: false
  });

  const products = [
    { id: 1, name: "에어팟", category: "electronics", price: 200000, inStock: true },
    { id: 2, name: "맥북", category: "electronics", price: 2000000, inStock: false },
    { id: 3, name: "티셔츠", category: "fashion", price: 30000, inStock: true },
    { id: 4, name: "청바지", category: "fashion", price: 50000, inStock: true }
  ];

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFilters({
      ...filters,
      [name]: type === "checkbox" ? checked : value
    });
  };

  const filteredProducts = products.filter((product) => {
    // 키워드 필터
    if (filters.keyword && !product.name.includes(filters.keyword)) {
      return false;
    }
    // 카테고리 필터
    if (filters.category !== "all" && product.category !== filters.category) {
      return false;
    }
    // 가격 필터
    if (filters.minPrice && product.price < Number(filters.minPrice)) {
      return false;
    }
    if (filters.maxPrice && product.price > Number(filters.maxPrice)) {
      return false;
    }
    // 재고 필터
    if (filters.inStock && !product.inStock) {
      return false;
    }
    return true;
  });

  return (
    <div>
      <div style={{ marginBottom: '20px', padding: '15px', background: '#f5f5f5' }}>
        <input
          type="text"
          name="keyword"
          value={filters.keyword}
          onChange={handleChange}
          placeholder="검색어"
          style={{ marginRight: '10px', padding: '8px' }}
        />

        <select
          name="category"
          value={filters.category}
          onChange={handleChange}
          style={{ marginRight: '10px', padding: '8px' }}
        >
          <option value="all">전체</option>
          <option value="electronics">전자기기</option>
          <option value="fashion">패션</option>
        </select>

        <input
          type="number"
          name="minPrice"
          value={filters.minPrice}
          onChange={handleChange}
          placeholder="최소 가격"
          style={{ width: '100px', marginRight: '10px', padding: '8px' }}
        />

        <input
          type="number"
          name="maxPrice"
          value={filters.maxPrice}
          onChange={handleChange}
          placeholder="최대 가격"
          style={{ width: '100px', marginRight: '10px', padding: '8px' }}
        />

        <label>
          <input
            type="checkbox"
            name="inStock"
            checked={filters.inStock}
            onChange={handleChange}
          />
          재고 있음만
        </label>
      </div>

      <div>
        <h3>검색 결과 ({filteredProducts.length}개)</h3>
        {filteredProducts.map((product) => (
          <div key={product.id} style={{
            padding: '10px',
            borderBottom: '1px solid #ddd'
          }}>
            <strong>{product.name}</strong>
            <span> - {product.price.toLocaleString()}원</span>
            <span style={{ color: product.inStock ? 'green' : 'red' }}>
              {product.inStock ? ' (재고 있음)' : ' (품절)'}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
}

폼 라이브러리 소개

복잡한 폼은 라이브러리를 쓰면 편해요:

  • React Hook Form: 성능 좋고 사용하기 쉬움
  • Formik: 기능이 풍부함
  • Yup: 유효성 검사 스키마 정의

나중에 프로젝트가 커지면 사용해보세요!

다음 단계

폼 다루는 법을 배웠으니, 다음 글에서는 useRef를 배울 거예요. DOM에 직접 접근하거나 렌더링 없이 값을 저장할 때 쓰는 Hook이에요.

폼은 연습이 많이 필요해요. 로그인 폼, 게시글 작성 폼, 설문조사 폼 등 여러 가지를 만들어보세요!


React 시리즈 탐색:

← 블로그 목록으로