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: '영희' });
전개 연산자 미사용 (Object.assign)
setUser(Object.assign({}, user, { name: '영희' }));빈 객체()에 user를 복사하고, name을 ‘영희’로 덮어씌운 새 객체를 반환한다
전개 연산자(...)란?
...은 객체나 배열의 내용물을 꺼내서 펼치는 문법이다.
// 원본 객체
const user = { name: '철수', age: 20 };
// ...user는 name: '철수', age: 20을 꺼내서 펼친다
const newUser = { ...user, name: '영희' };
// 결과: { name: '영희', age: 20 }
// → 펼친 뒤 같은 키(name)를 덮어쓴다배열도 같은 원리다:
const fruits = ['사과', '바나나'];
// 1. 전개 연산자 사용
const newFruits = [...fruits, '오렌지'];
// 결과: ['사과', '바나나', '오렌지']
// → 기존 항목을 펼치고 새 항목을 추가한다
// 2. 전개 연산자 미사용 (concat 함수 사용)
const newFruits2 = fruits.concat('오렌지');
// 결과: ['사과', '바나나', '오렌지']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';
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;
}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)을 유지해야 한다