React 입문 9편 - 입력값 다루기

Controlled Component 처음 배울 때 “왜 이렇게 복잡하게 해?” 했어요. HTML input은 그냥 쓰면 되는데 value랑 onChange 둘 다 써야 하니까요. 근데 유효성 검사할 때 보니까 이게 훨씬 편하더라고요.
웹앱에서 폼은 필수죠. 로그인, 회원가입, 검색, 게시글 작성… 거의 모든 곳에 폼이 있어요. 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: 유효성 검사 스키마 정의
나중에 프로젝트가 커지면 사용해보세요!
운영자 실전 노트
실제 프로젝트 진행하며 겪은 문제
- Controlled vs Uncontrolled 선택 → 대부분 Controlled가 유지보수 편함
- 폼 필드 10개 이상 시 코드 복잡 → React Hook Form으로 전환 필수
이 경험을 통해 알게 된 점
- 실시간 유효성 검사는 UX에 큰 영향을 미친다
- onChange 핸들러 통합으로 코드 중복을 줄일 수 있다
다음 단계
폼 다루는 법을 배웠으니 다음 글에서는 useRef를 배운다. DOM에 직접 접근하거나 렌더링 없이 값을 저장할 때 쓰는 Hook이다. 폼은 연습이 많이 필요하다. 로그인 폼, 게시글 작성 폼, 설문조사 폼 등 여러 가지를 만들어보자.
React 시리즈 탐색:
← 블로그 목록으로