6강. State 관리
1. State 관리
1.1. State란?
State는 컴포넌트의 데이터 저장소다. 값이 변경되면 화면이 자동으로 다시 그려진다.
1.1.1. 기본 사용법
import { useState } from 'react';
function App() { const [count, setCount] = useState(0);
return ( <div> <p>횟수: {count}</p> <button onClick={() => setCount(count + 1)}>증가</button> </div> );}1.2. useState 동작 원리
useState는 두 개의 값을 배열로 반환한다.
- 첫 번째: 현재 상태 값
- 두 번째: 상태를 변경하는 함수
const [value, setValue] = useState(초기값);1.2.1. 예제 1: 카운터
function Counter() { const [num, setNum] = useState(0);
const plus = () => setNum(num + 1); const minus = () => setNum(num - 1);
return ( <div> <h2>{num}</h2> <button onClick={plus}>+</button> <button onClick={minus}>-</button> </div> );}1.3. 객체와 배열 State 관리
1.3.1. 객체 수정하기
주의
주의: 직접 수정 금지!
상태 객체나 배열을 직접 수정하면 안 된다. 반드시 새로운 객체/배열을 만들어야 한다.
const [user, setUser] = useState({ name: '철수', age: 20 });
// ✅ 올바른 방법: 전개 연산자(...) 사용setUser({ ...user, name: '영희' });전개 연산자(...)란?
...은 객체나 배열의 내용물을 꺼내서 펼치는 문법이다.
// 원본 객체const user = { name: '철수', age: 20 };
// ...user는 name: '철수', age: 20을 꺼내서 펼친다const newUser = { ...user, name: '영희' };// 결과: { name: '영희', age: 20 }// → 펼친 뒤 같은 키(name)를 덮어쓴다배열도 같은 원리다:
const fruits = ['사과', '바나나'];
const newFruits = [...fruits, '오렌지'];// 결과: ['사과', '바나나', '오렌지']// → 기존 항목을 펼치고 새 항목을 추가한다1.3.2. 배열 수정하기
const [items, setItems] = useState(['사과', '바나나']);
// ✅ 항목 추가setItems([...items, '오렌지']);
// ✅ 항목 삭제 (filter 사용)setItems(items.filter((item, index) => index !== 0));1.4. useEffect란?
useEffect는 컴포넌트가 화면에 나타날 때, 사라질 때, 또는 특정 값이 변할 때 실행할 코드를 정의한다.
1.4.1. 기본 사용법
import { useEffect } from 'react';
useEffect(() => { console.log('화면이 나타남 (Mount)');
return () => { console.log('화면에서 사라짐 (Unmount)'); };}, []); // 빈 배열: 처음 한 번만 실행1.4.2. 의존성 배열
[]: 처음 한 번만 실행[count]:count값이 변할 때마다 실행- 생략: 매번 렌더링될 때마다 실행
1.5. 실습: 간단한 Todo 앱
useState와 useEffect를 함께 활용하는 예제이다.
- App.jsx
- App.css
import { useState, useEffect } from 'react';.todo-app {
import { useState, useEffect } from 'react';import './App.css';
function App() { const [todos, setTodos] = useState([]); const [input, setInput] = useState(''); const [count, setCount] = useState(0);
// useEffect: todos가 변할 때마다 남은 개수를 업데이트 useEffect(() => { setCount(todos.filter(todo => !todo.done).length); }, [todos]);
// 추가 const addTodo = () => { if (!input) return; setTodos([...todos, { id: Date.now(), text: input, done: false }]); setInput(''); };
// 완료 토글 const toggleTodo = (id) => { setTodos(todos.map(todo => todo.id === id ? { ...todo, done: !todo.done } : todo )); };
// 삭제 const deleteTodo = (id) => { setTodos(todos.filter(todo => todo.id !== id)); };
return ( <div className="todo-app"> <h1>할 일 목록</h1> <p>남은 할 일: {count}개</p>
<div className="todo-input"> <input value={input} onChange={(e) => setInput(e.target.value)} placeholder="할 일을 입력하세요" /> <button onClick={addTodo}>추가</button> </div>
<ul className="todo-list"> {todos.map(todo => ( <li key={todo.id} className={todo.done ? 'done' : ''}> <span onClick={() => toggleTodo(todo.id)}> {todo.done ? '✅' : '⬜'} {todo.text} </span> <button onClick={() => deleteTodo(todo.id)}>삭제</button> </li> ))} </ul>
{/* 조건부 렌더링: 할 일이 없으면 메시지 표시 */} {todos.length === 0 && <p>할 일을 추가해보세요!</p>} </div> );}
export default App;
.todo-app { max-width: 400px; margin: 0 auto; padding: 20px;}
.todo-input { display: flex; gap: 8px; margin-bottom: 16px;}
.todo-input input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;}
.todo-input button { padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;}
.todo-list { list-style: none; padding: 0;}
.todo-list li { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #eee;}
.todo-list li.done span { text-decoration: line-through; color: #999;}
.todo-list li span { cursor: pointer;}
.todo-list li button { background: #dc3545; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer;}
이 예제에서 배운 것
- useState:
todos(배열), input(문자열), count(숫자) 3가지 상태 관리
- useEffect:
todos가 변할 때마다 남은 개수를 자동 계산
- 전개 연산자:
[...todos, 새항목]으로 배열에 추가
- filter/map: 삭제와 수정에 활용
- 조건부 렌더링:
{todos.length === 0 && <p>메시지</p>}
1.6. 정리
- State는 컴포넌트 내부에서 변하는 데이터를 관리한다
useState를 통해 상태와 상태 변경 함수를 얻는다
- 상태가 변하면 컴포넌트는 다시 렌더링된다
useEffect로 생명주기(Lifecycle)를 관리한다
- 객체나 배열 상태를 변경할 때는 불변성(Immutability)을 유지해야 한다