React 독학 가이드 9 - 사용자 입력 관리하기
웹앱에서 폼은 필수죠. 로그인, 회원가입, 검색, 게시글 작성… 거의 모든 곳에 폼이 있어요. 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 시리즈 탐색:
← 블로그 목록으로