이번 학습 단계의 핵심: 배열 안의 특정 객체를 바꾸려면 map()으로 새로운 배열을 만든다.
Step 0. 실습에서 다룰 Todo 객체
{
id: 1,
text: "React 공부하기",
completed: false
}
객체에 완료 여부에 대한 completed 속성을 추가한다.
Step 1. 새 Todo에 completed 추가하기
handleSubmit 함수에서 newTodo 부분을 다음과 같이 변경한다.
// 기존
// const newTodo = {
// id: Date.now(),
// text: input.trim(),
// };
// 변경
const newTodo = {
id: Date.now(),
text: input.trim(),
completed: false,
};
Step 2. 완료 버튼 만들기
Todo 목록 부분을 수정해보자.
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
<button>
완료
</button>
<button onClick={() => handleDelete(todo.id)}>
삭제
</button>
</li>
))}
원하는 동작은 완료 버튼을 클릭하면 React 공부하기 [완료] → React 공부하기 [취소] 와 같이 변하는 것이다.
그러려면 클릭한 Todo의 completed를 변경해야 한다.
Step 3. map으로 State 배열 수정하기
먼저 코드부터 살펴보자.
function handleToggle(id) {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
);
}
map()이 모든 Todo를 하나씩 확인하여 원하는 Todo의 id인 경우,
completed 속성의 값을 토글하여 반대로 뒤집고,
원하는 id가 아닌 경우, 그대로 반환하는 방식으로 새로운 배열을 만든다.
Step 4. 버튼과 연결하기
버튼에서 Todo의 id를 넘겨준다.
<button onClick={() => handleToggle(todo.id)}>
완료
</button>
Step 5. 완료 여부를 화면에 표시하기
<li key={todo.id}>
{todo.completed ? "✅ " : "⬜️ "}
{todo.text}
<button onClick={() => handleToggle(todo.id)}>
{todo.completed ? "취소" : "완료"}
</button>
<button onClick={() => handleDelete(todo.id)}>삭제</button>
</li>
브라우저를 통해 확인해보면

정상적으로 동작하는 것을 확인할 수 있다.
현재까지의 전체 코드:
import { useState } from "react";
function App() {
const [input, setInput] = useState("");
const [todos, setTodos] = useState([]);
function handleChange(event) {
setInput(event.target.value);
}
function handleSubmit(event) {
event.preventDefault();
if (input.trim() === "") {
return;
}
const newTodo = {
id: Date.now(),
text: input.trim(),
completed: false,
};
setTodos((prevTodos) => [...prevTodos, newTodo]);
setInput("");
}
function handleDelete(id) {
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
}
function handleToggle(id) {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
);
}
return (
<div>
<h1>Todo List</h1>
<p>Todo 개수: {todos.length}개</p>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleChange}
placeholder="할 일을 입력하세요"
/>
<button type="submit">추가</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.completed ? "✅ " : "⬜️ "}
{todo.text}
<button onClick={() => handleToggle(todo.id)}>
{todo.completed ? "취소" : "완료"}
</button>
<button onClick={() => handleDelete(todo.id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}
export default App;
Step 6. 배열 State를 다루는 세 가지 핵심 패턴
추가
setTodos((prevTodos) => [...prevTodos, newTodo]);
삭제
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
수정
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
);
- 추가 → spread
- 삭제 → filter
- 수정 → map
Step 7. 완료된 Todo 개수 표시하기
이번에는 이미 가지고 있는 State에서 새로운 값을 계산해보자.
filter()를 사용하여 완료된 Todo만을 셀 수 있다.
const completedCount = todos.filter((todo) => todo.completed).length;
App 함수의 return 위에 작성한다.
그리고
<p>완료: {completedCount} / {todos.length}</p>
라고 표시한다.
App 함수의 return 위에 작성한다.
여기서 중요한 점은 완료된 작업의 개수를 별도의 State로 관리하지 않는다는 것이다. 기존 State를 이용해서 계산할 수 있는 값이기 때문이다.
전체 코드:
import { useState } from "react";
function App() {
const [input, setInput] = useState("");
const [todos, setTodos] = useState([]);
const completedCount = todos.filter((todo) => todo.completed).length;
function handleChange(event) {
setInput(event.target.value);
}
function handleSubmit(event) {
event.preventDefault();
if (input.trim() === "") {
return;
}
const newTodo = {
id: Date.now(),
text: input.trim(),
completed: false,
};
setTodos((prevTodos) => [...prevTodos, newTodo]);
setInput("");
}
function handleDelete(id) {
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
}
function handleToggle(id) {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
);
}
return (
<div>
<h1>Todo List</h1>
<p>Todo 개수: {todos.length}개</p>
<p>
완료: {completedCount} / {todos.length}
</p>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleChange}
placeholder="할 일을 입력하세요"
/>
<button type="submit">추가</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.completed ? "✅ " : "⬜️ "}
{todo.text}
<button onClick={() => handleToggle(todo.id)}>
{todo.completed ? "취소" : "완료"}
</button>
<button onClick={() => handleDelete(todo.id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}
export default App;
실습
이번 실습에서는 다음과 같이 "전체 완료" 버튼을 만들어 클릭하면 모든 Todo가 완료되도록 해보자.

답안
function handleCompleteAll() {
setTodos((prevTodos) =>
prevTodos.map((todo) => ({ ...todo, completed: true })),
);
}
<button onClick={handleCompleteAll}>전체 완료</button>
'개발 > React' 카테고리의 다른 글
| [React] useEffect (0) | 2026.09.24 |
|---|---|
| [React] 컴포넌트 분리와 State 끌어올리기 (0) | 2026.09.22 |
| [React] Todo List 만들기 실습 (1) [배열 항목 추가와 삭제] (0) | 2026.09.17 |
| [React] 조건부 렌더링과 리스트 렌더링 (0) | 2026.09.15 |
| [React] Input과 State (0) | 2026.09.15 |