useState Hook
3. useState Hook
useState는 React에서 가장 기본적이고 중요한 Hook입니다. 함수 컴포넌트에서 상태를 관리할 수 있게 해주는 핵심 기능입니다.
useState 기본 문법
import { useState } from 'react';
const [상태값, 상태변경함수] = useState(초기값);import { useState } from 'react';
const [상태값, 상태변경함수] = useState(초기값);예제: 간단한 카운터
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>카운트: {count}</h2>
<button onClick={() => setCount(count + 1)}>증가</button>
<button onClick={() => setCount(count - 1)}>감소</button>
<button onClick={() => setCount(0)}>리셋</button>
</div>
);
}import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>카운트: {count}</h2>
<button onClick={() => setCount(count + 1)}>증가</button>
<button onClick={() => setCount(count - 1)}>감소</button>
<button onClick={() => setCount(0)}>리셋</button>
</div>
);
}
useState의 동작 원리
1. 초기 렌더링
컴포넌트가 처음 렌더링될 때 useState의 초기값이 사용됩니다.
function App() {
// 처음에는 name이 '게스트'로 설정됨
const [name, setName] = useState('게스트');
return <h1>안녕하세요, {name}님!</h1>; // "안녕하세요, 게스트님!"
}function App() {
// 처음에는 name이 '게스트'로 설정됨
const [name, setName] = useState('게스트');
return <h1>안녕하세요, {name}님!</h1>; // "안녕하세요, 게스트님!"
}2. 상태 업데이트
setter 함수를 호출하면 React가 컴포넌트를 다시 렌더링합니다.
function NameChanger() {
const [name, setName] = useState('게스트');
const changeName = () => {
setName('김개발'); // 이 함수가 호출되면 컴포넌트가 다시 렌더링됨
};
return (
<div>
<h1>안녕하세요, {name}님!</h1>
<button onClick={changeName}>이름 변경</button>
</div>
);
}function NameChanger() {
const [name, setName] = useState('게스트');
const changeName = () => {
setName('김개발'); // 이 함수가 호출되면 컴포넌트가 다시 렌더링됨
};
return (
<div>
<h1>안녕하세요, {name}님!</h1>
<button onClick={changeName}>이름 변경</button>
</div>
);
}3. 리렌더링 과정
function RenderingDemo() {
const [count, setCount] = useState(0);
console.log('컴포넌트가 렌더링되었습니다!', count);
return (
<div>
<p>카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>
클릭 (콘솔 확인)
</button>
</div>
);
}function RenderingDemo() {
const [count, setCount] = useState(0);
console.log('컴포넌트가 렌더링되었습니다!', count);
return (
<div>
<p>카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>
클릭 (콘솔 확인)
</button>
</div>
);
}useState 활용 패턴
1. 입력 필드 관리
function InputDemo() {
const [text, setText] = useState('');
const [email, setEmail] = useState('');
return (
<div>
<div>
<label>텍스트: </label>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<p>입력된 텍스트: {text}</p>
</div>
<div>
<label>이메일: </label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<p>입력된 이메일: {email}</p>
</div>
</div>
);
}function InputDemo() {
const [text, setText] = useState('');
const [email, setEmail] = useState('');
return (
<div>
<div>
<label>텍스트: </label>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<p>입력된 텍스트: {text}</p>
</div>
<div>
<label>이메일: </label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<p>입력된 이메일: {email}</p>
</div>
</div>
);
}2. 체크박스와 라디오 버튼
function FormControls() {
const [isChecked, setIsChecked] = useState(false);
const [selectedOption, setSelectedOption] = useState('');
return (
<div>
<div>
<label>
<input
type="checkbox"
checked={isChecked}
onChange={(e) => setIsChecked(e.target.checked)}
/>
동의합니다
</label>
<p>동의 상태: {isChecked ? '동의함' : '동의 안함'}</p>
</div>
<div>
<p>좋아하는 색상을 선택하세요:</p>
{['빨강', '파랑', '녹색'].map(color => (
<label key={color}>
<input
type="radio"
name="color"
value={color}
checked={selectedOption === color}
onChange={(e) => setSelectedOption(e.target.value)}
/>
{color}
</label>
))}
<p>선택된 색상: {selectedOption}</p>
</div>
</div>
);
}function FormControls() {
const [isChecked, setIsChecked] = useState(false);
const [selectedOption, setSelectedOption] = useState('');
return (
<div>
<div>
<label>
<input
type="checkbox"
checked={isChecked}
onChange={(e) => setIsChecked(e.target.checked)}
/>
동의합니다
</label>
<p>동의 상태: {isChecked ? '동의함' : '동의 안함'}</p>
</div>
<div>
<p>좋아하는 색상을 선택하세요:</p>
{['빨강', '파랑', '녹색'].map(color => (
<label key={color}>
<input
type="radio"
name="color"
value={color}
checked={selectedOption === color}
onChange={(e) => setSelectedOption(e.target.value)}
/>
{color}
</label>
))}
<p>선택된 색상: {selectedOption}</p>
</div>
</div>
);
}3. 토글 기능
function ToggleDemo() {
const [isVisible, setIsVisible] = useState(true);
const [isDarkMode, setIsDarkMode] = useState(false);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
{isVisible ? '숨기기' : '보이기'}
</button>
{isVisible && (
<p>이 텍스트는 토글됩니다!</p>
)}
<button onClick={() => setIsDarkMode(!isDarkMode)}>
{isDarkMode ? '라이트 모드' : '다크 모드'}
</button>
</div>
);
}function ToggleDemo() {
const [isVisible, setIsVisible] = useState(true);
const [isDarkMode, setIsDarkMode] = useState(false);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
{isVisible ? '숨기기' : '보이기'}
</button>
{isVisible && (
<p>이 텍스트는 토글됩니다!</p>
)}
<button onClick={() => setIsDarkMode(!isDarkMode)}>
{isDarkMode ? '라이트 모드' : '다크 모드'}
</button>
</div>
);
}4. 배열 상태 관리
function TodoApp() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos([...todos, {
id: Date.now(),
text: inputValue.trim(),
completed: false
}]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<h2>할 일 목록</h2>
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="할 일을 입력하세요"
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>추가</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}function TodoApp() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos([...todos, {
id: Date.now(),
text: inputValue.trim(),
completed: false
}]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<h2>할 일 목록</h2>
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="할 일을 입력하세요"
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>추가</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}5. 객체 상태 관리
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: '',
city: ''
});
const handleInputChange = (field, value) => {
setUser(prevUser => ({
...prevUser,
[field]: value
}));
};
const resetForm = () => {
setUser({
name: '',
email: '',
age: '',
city: ''
});
};
return (
<div>
<h2>사용자 정보</h2>
<div>
<input
type="text"
placeholder="이름"
value={user.name}
onChange={(e) => handleInputChange('name', e.target.value)}
/>
</div>
<div>
<input
type="email"
placeholder="이메일"
value={user.email}
onChange={(e) => handleInputChange('email', e.target.value)}
/>
</div>
<div>
<input
type="number"
placeholder="나이"
value={user.age}
onChange={(e) => handleInputChange('age', e.target.value)}
/>
</div>
<div>
<input
type="text"
placeholder="도시"
value={user.city}
onChange={(e) => handleInputChange('city', e.target.value)}
/>
</div>
<button onClick={resetForm}>초기화</button>
<div>
<h3>입력된 정보</h3>
<p>이름: {user.name}</p>
<p>이메일: {user.email}</p>
<p>나이: {user.age}</p>
<p>도시: {user.city}</p>
</div>
</div>
);
}function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: '',
city: ''
});
const handleInputChange = (field, value) => {
setUser(prevUser => ({
...prevUser,
[field]: value
}));
};
const resetForm = () => {
setUser({
name: '',
email: '',
age: '',
city: ''
});
};
return (
<div>
<h2>사용자 정보</h2>
<div>
<input
type="text"
placeholder="이름"
value={user.name}
onChange={(e) => handleInputChange('name', e.target.value)}
/>
</div>
<div>
<input
type="email"
placeholder="이메일"
value={user.email}
onChange={(e) => handleInputChange('email', e.target.value)}
/>
</div>
<div>
<input
type="number"
placeholder="나이"
value={user.age}
onChange={(e) => handleInputChange('age', e.target.value)}
/>
</div>
<div>
<input
type="text"
placeholder="도시"
value={user.city}
onChange={(e) => handleInputChange('city', e.target.value)}
/>
</div>
<button onClick={resetForm}>초기화</button>
<div>
<h3>입력된 정보</h3>
<p>이름: {user.name}</p>
<p>이메일: {user.email}</p>
<p>나이: {user.age}</p>
<p>도시: {user.city}</p>
</div>
</div>
);
}고급 useState 패턴
1. 지연 초기 상태 (Lazy Initial State)
function ExpensiveComponent() {
// 매번 렌더링될 때마다 복잡한 계산이 실행됨 (비효율적)
const [data, setData] = useState(expensiveCalculation());
// 함수를 전달하면 초기 렌더링 시에만 실행됨 (효율적)
const [betterData, setBetterData] = useState(() => expensiveCalculation());
return <div>데이터: {betterData}</div>;
}
function expensiveCalculation() {
console.log('복잡한 계산 실행됨!');
return Math.random() * 1000;
}function ExpensiveComponent() {
// 매번 렌더링될 때마다 복잡한 계산이 실행됨 (비효율적)
const [data, setData] = useState(expensiveCalculation());
// 함수를 전달하면 초기 렌더링 시에만 실행됨 (효율적)
const [betterData, setBetterData] = useState(() => expensiveCalculation());
return <div>데이터: {betterData}</div>;
}
function expensiveCalculation() {
console.log('복잡한 계산 실행됨!');
return Math.random() * 1000;
}2. 함수형 업데이트
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
// 현재 값 기반 업데이트
setCount(count + 1);
};
const handleIncrementBetter = () => {
// 함수형 업데이트 (권장)
setCount(prevCount => prevCount + 1);
};
const handleMultipleIncrement = () => {
// 이렇게 하면 1만 증가함
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
const handleMultipleIncrementBetter = () => {
// 이렇게 하면 3 증가함
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return (
<div>
<p>카운트: {count}</p>
<button onClick={handleIncrement}>+1</button>
<button onClick={handleIncrementBetter}>+1 (Better)</button>
<button onClick={handleMultipleIncrement}>+3 (문제)</button>
<button onClick={handleMultipleIncrementBetter}>+3 (해결)</button>
</div>
);
}function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
// 현재 값 기반 업데이트
setCount(count + 1);
};
const handleIncrementBetter = () => {
// 함수형 업데이트 (권장)
setCount(prevCount => prevCount + 1);
};
const handleMultipleIncrement = () => {
// 이렇게 하면 1만 증가함
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
const handleMultipleIncrementBetter = () => {
// 이렇게 하면 3 증가함
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return (
<div>
<p>카운트: {count}</p>
<button onClick={handleIncrement}>+1</button>
<button onClick={handleIncrementBetter}>+1 (Better)</button>
<button onClick={handleMultipleIncrement}>+3 (문제)</button>
<button onClick={handleMultipleIncrementBetter}>+3 (해결)</button>
</div>
);
}3. 여러 상태 관리하기
function Calculator() {
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
const [operation, setOperation] = useState('+');
const [result, setResult] = useState(0);
const calculate = () => {
let newResult;
switch (operation) {
case '+':
newResult = parseFloat(num1) + parseFloat(num2);
break;
case '-':
newResult = parseFloat(num1) - parseFloat(num2);
break;
case '*':
newResult = parseFloat(num1) * parseFloat(num2);
break;
case '/':
newResult = parseFloat(num1) / parseFloat(num2);
break;
default:
newResult = 0;
}
setResult(newResult);
};
return (
<div>
<h2>계산기</h2>
<div>
<input
type="number"
value={num1}
onChange={(e) => setNum1(e.target.value)}
/>
<select
value={operation}
onChange={(e) => setOperation(e.target.value)}
>
<option value="+">+</option>
<option value="-">-</option>
<option value="*">×</option>
<option value="/">÷</option>
</select>
<input
type="number"
value={num2}
onChange={(e) => setNum2(e.target.value)}
/>
<button onClick={calculate}>=</button>
<span>
{result}
</span>
</div>
</div>
);
}function Calculator() {
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
const [operation, setOperation] = useState('+');
const [result, setResult] = useState(0);
const calculate = () => {
let newResult;
switch (operation) {
case '+':
newResult = parseFloat(num1) + parseFloat(num2);
break;
case '-':
newResult = parseFloat(num1) - parseFloat(num2);
break;
case '*':
newResult = parseFloat(num1) * parseFloat(num2);
break;
case '/':
newResult = parseFloat(num1) / parseFloat(num2);
break;
default:
newResult = 0;
}
setResult(newResult);
};
return (
<div>
<h2>계산기</h2>
<div>
<input
type="number"
value={num1}
onChange={(e) => setNum1(e.target.value)}
/>
<select
value={operation}
onChange={(e) => setOperation(e.target.value)}
>
<option value="+">+</option>
<option value="-">-</option>
<option value="*">×</option>
<option value="/">÷</option>
</select>
<input
type="number"
value={num2}
onChange={(e) => setNum2(e.target.value)}
/>
<button onClick={calculate}>=</button>
<span>
{result}
</span>
</div>
</div>
);
}useState 사용 시 주의사항
1. 상태 직접 수정 금지
// ❌ 잘못된 방법
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // 직접 수정하면 안됨!
// ✅ 올바른 방법
setItems([...items, 4]); // 새 배열 생성// ❌ 잘못된 방법
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // 직접 수정하면 안됨!
// ✅ 올바른 방법
setItems([...items, 4]); // 새 배열 생성2. 객체/배열 상태 업데이트
// ❌ 잘못된 방법
const [user, setUser] = useState({ name: 'John', age: 25 });
user.name = 'Jane'; // 직접 수정하면 안됨!
// ✅ 올바른 방법
setUser({ ...user, name: 'Jane' }); // 새 객체 생성// ❌ 잘못된 방법
const [user, setUser] = useState({ name: 'John', age: 25 });
user.name = 'Jane'; // 직접 수정하면 안됨!
// ✅ 올바른 방법
setUser({ ...user, name: 'Jane' }); // 새 객체 생성3. 비동기 특성 이해
function AsyncExample() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // 아직 이전 값이 출력됨!
};
return (
<div>
<p>카운트: {count}</p>
<button onClick={handleClick}>클릭</button>
</div>
);
}function AsyncExample() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // 아직 이전 값이 출력됨!
};
return (
<div>
<p>카운트: {count}</p>
<button onClick={handleClick}>클릭</button>
</div>
);
}실습: 간단한 게임
useState를 활용한 숫자 맞추기 게임을 만들어봅시다:
import { useState } from 'react';
function NumberGuessingGame() {
const [targetNumber] = useState(() => Math.floor(Math.random() * 100) + 1);
const [guess, setGuess] = useState('');
const [attempts, setAttempts] = useState(0);
const [message, setMessage] = useState('1부터 100 사이의 숫자를 맞춰보세요!');
const [gameOver, setGameOver] = useState(false);
const [history, setHistory] = useState([]);
const handleGuess = () => {
const guessNum = parseInt(guess);
if (isNaN(guessNum) || guessNum < 1 || guessNum > 100) {
setMessage('1부터 100 사이의 숫자를 입력해주세요!');
return;
}
const newAttempts = attempts + 1;
setAttempts(newAttempts);
setHistory([...history, guessNum]);
if (guessNum === targetNumber) {
setMessage(`🎉 정답입니다! ${newAttempts}번 만에 맞추셨네요!`);
setGameOver(true);
} else if (guessNum < targetNumber) {
setMessage(`더 큰 수를 입력해보세요! (시도: ${newAttempts}번)`);
} else {
setMessage(`더 작은 수를 입력해보세요! (시도: ${newAttempts}번)`);
}
setGuess('');
};
const resetGame = () => {
window.location.reload(); // 간단한 게임 리셋
};
return (
<div>
<h2>숫자 맞추기 게임</h2>
<p>{message}</p>
{!gameOver && (
<div>
<input
type="number"
value={guess}
onChange={(e) => setGuess(e.target.value)}
placeholder="숫자 입력"
min="1"
max="100"
onKeyPress={(e) => e.key === 'Enter' && handleGuess()}
/>
<button onClick={handleGuess}>추측</button>
</div>
)}
<p>시도 횟수: {attempts}</p>
{history.length > 0 && (
<div>
<h3>추측 기록</h3>
<p>{history.join(', ')}</p>
</div>
)}
{gameOver && (
<button onClick={resetGame}>
새 게임
</button>
)}
</div>
);
}
export default NumberGuessingGame;import { useState } from 'react';
function NumberGuessingGame() {
const [targetNumber] = useState(() => Math.floor(Math.random() * 100) + 1);
const [guess, setGuess] = useState('');
const [attempts, setAttempts] = useState(0);
const [message, setMessage] = useState('1부터 100 사이의 숫자를 맞춰보세요!');
const [gameOver, setGameOver] = useState(false);
const [history, setHistory] = useState([]);
const handleGuess = () => {
const guessNum = parseInt(guess);
if (isNaN(guessNum) || guessNum < 1 || guessNum > 100) {
setMessage('1부터 100 사이의 숫자를 입력해주세요!');
return;
}
const newAttempts = attempts + 1;
setAttempts(newAttempts);
setHistory([...history, guessNum]);
if (guessNum === targetNumber) {
setMessage(`🎉 정답입니다! ${newAttempts}번 만에 맞추셨네요!`);
setGameOver(true);
} else if (guessNum < targetNumber) {
setMessage(`더 큰 수를 입력해보세요! (시도: ${newAttempts}번)`);
} else {
setMessage(`더 작은 수를 입력해보세요! (시도: ${newAttempts}번)`);
}
setGuess('');
};
const resetGame = () => {
window.location.reload(); // 간단한 게임 리셋
};
return (
<div>
<h2>숫자 맞추기 게임</h2>
<p>{message}</p>
{!gameOver && (
<div>
<input
type="number"
value={guess}
onChange={(e) => setGuess(e.target.value)}
placeholder="숫자 입력"
min="1"
max="100"
onKeyPress={(e) => e.key === 'Enter' && handleGuess()}
/>
<button onClick={handleGuess}>추측</button>
</div>
)}
<p>시도 횟수: {attempts}</p>
{history.length > 0 && (
<div>
<h3>추측 기록</h3>
<p>{history.join(', ')}</p>
</div>
)}
{gameOver && (
<button onClick={resetGame}>
새 게임
</button>
)}
</div>
);
}
export default NumberGuessingGame;정리
useState Hook의 핵심을 학습했습니다:
- useState는 함수 컴포넌트에서 상태를 관리하는 Hook입니다
- [상태값, setter함수] = useState(초기값) 형태로 사용합니다
- 상태 변경 시 컴포넌트가 재렌더링됩니다
- 직접 수정 대신 setter 함수를 사용해야 합니다
- 함수형 업데이트로 더 안전한 상태 변경이 가능합니다
다음 장에서는 이벤트 처리에 대해 더 자세히 알아보겠습니다.