할 일 목록 만들기 (useReducer)

튜토리얼 — 할 일 목록 만들기 (useReducer)
ReducerCounter의 상태는 숫자 하나, 지시는 문자열 하나("increment")였습니다. 이번엔 한 걸음 더 갑니다 — 상태가 할 일 목록(배열) 이고, 지시가 데이터를 함께 싣는 경우입니다. "어떤 할 일을 추가할지", "어떤 할 일을 지울지" 같은 정보를 지시에 담아 보내야 하니까요.
개념
카운터의 지시는 "increment" 한마디면 충분했습니다. 더 정보가 필요 없었죠.
하지만 할 일 목록은 다릅니다.
- "추가해라" → 무엇을 추가할지(할 일 텍스트)도 알려 줘야 합니다.
- "지워라" → 어느 것을 지울지(id)도 알려 줘야 합니다.
그래서 지시를 문자열 대신 객체로 보냅니다. type(무슨 지시인지)과 함께 필요한 데이터를 같이 싣습니다.
dispatch({ type: "add", text: "우유 사기" }); // 추가 + 데이터(text) dispatch({ type: "remove", id: 3 }); // 삭제 + 데이터(id)
이렇게 지시에 실어 보내는 데이터를 흔히 페이로드(payload) 라고 부릅니다. 카운터에서 한 단계 올라온 부분은 이것 하나입니다 — 지시가 문자열에서 객체가 됐다.
함께 해보기
1단계 — 할 일과 action 타입 정하기
먼저 "할 일 하나"의 모양과, 리듀서가 받을 지시의 종류를 타입으로 정합니다.
type Todo = { id: number; text: string; done: boolean; }; type TodoAction = | { type: "add"; text: string } // 추가 — 할 일 텍스트를 싣고 | { type: "toggle"; id: number } // 완료 토글 — 대상 id를 싣고 | { type: "remove"; id: number }; // 삭제 — 대상 id를 싣고
TodoAction은 3.5절의 유니온 타입입니다. 카운터에서는 "increment" | "decrement" | "reset"처럼 문자열들의 유니온이었죠. 여기서는 객체들의 유니온입니다. 지시는 이 세 가지 객체 모양 중 하나입니다.
2단계 — reducer 함수
목록을 바꾸는 로직을 한 함수에 모읍니다.
function todoReducer(state: Todo[], action: TodoAction): Todo[] { if (action.type === "add") { const newTodo: Todo = { id: Date.now(), text: action.text, done: false }; return [...state, newTodo]; // 기존 목록 + 새 할 일 } if (action.type === "toggle") { return state.map((todo) => todo.id === action.id ? { ...todo, done: !todo.done } : todo, ); } if (action.type === "remove") { return state.filter((todo) => todo.id !== action.id); } return state; }
카운터와 모양은 똑같습니다 — if로 action.type을 갈라 새 상태를 return. 한 가지 새 규칙만 기억하세요.
상태가 배열·객체일 때는, 원래 것을 고치지 말고 새로 만들어 return 합니다.
state.push(...)로 기존 배열을 건드리면 React가 "바뀌었다"를 알아채지 못합니다.
그래서 추가는[...state, 새것], 변경은map, 삭제는filter— 전부 새 배열을 만듭니다.
카운터에서는 state + 1이라 이 고민이 없었습니다(숫자는 원래 못 고침). 배열·객체부터 이 규칙이 필요합니다.
3단계 — 컴포넌트
components/TodoList.tsx를 만듭니다.
"use client"; import { useReducer, useState } from "react"; type Todo = { id: number; text: string; done: boolean }; type TodoAction = | { type: "add"; text: string } | { type: "toggle"; id: number } | { type: "remove"; id: number }; function todoReducer(state: Todo[], action: TodoAction): Todo[] { if (action.type === "add") { const newTodo: Todo = { id: Date.now(), text: action.text, done: false }; return [...state, newTodo]; } if (action.type === "toggle") { return state.map((todo) => todo.id === action.id ? { ...todo, done: !todo.done } : todo, ); } if (action.type === "remove") { return state.filter((todo) => todo.id !== action.id); } return state; } export default function TodoList() { // 로직이 많은 상태(목록) → useReducer const [todos, dispatch] = useReducer(todoReducer, []); // 단순한 입력칸 상태 → useState const [text, setText] = useState(""); const handleAdd = () => { if (text.trim() === "") return; // 빈 입력은 무시 dispatch({ type: "add", text }); // 지시 + 페이로드(text) setText(""); // 입력칸 비우기 }; return ( <div className="p-4 space-y-2"> <div className="space-x-2"> <input value={text} onChange={(e) => setText(e.target.value)} placeholder="할 일을 입력하세요" className="rounded border px-3 py-1" /> <button className="rounded bg-blue-500 px-3 py-1 text-white" onClick={handleAdd} > 추가 </button> </div> <ul className="space-y-1"> {todos.map((todo) => ( <li key={todo.id} className="space-x-2"> <span onClick={() => dispatch({ type: "toggle", id: todo.id })} style={{ textDecoration: todo.done ? "line-through" : "none", cursor: "pointer", }} > {todo.text} </span> <button className="text-sm text-red-500" onClick={() => dispatch({ type: "remove", id: todo.id })} > 삭제 </button> </li> ))} </ul> </div> ); }
눈여겨볼 곳:
useReducer와useState를 같이 씁니다. 목록은 동작이 셋(추가·토글·삭제)이라useReducer로, 입력칸은 그냥 글자만 담으면 되니useState로. "로직이 많으면 reducer, 단순하면 state" — 이 절의 기준 그대로입니다.- 버튼·항목은 전부
dispatch({ type: ..., ... })로 지시만 보냅니다. 목록을 어떻게 바꿀지는todoReducer한 곳에 모여 있습니다. todos.map(...)으로 목록을 그리고, 각<li>에key={todo.id}를 줍니다.
4단계 — 페이지에 올리고 확인
app/todo/page.tsx를 만듭니다.
import TodoList from "@/components/TodoList"; export default function TodoPage() { return ( <main style={{ padding: 24 }}> <h2>할 일 목록</h2> <TodoList /> </main> ); }
/todo에서 확인합니다.
- 입력 후 "추가" → 목록에 할 일이 쌓입니다. ✅
- 할 일 텍스트를 클릭 → 취소선이 생겼다 사라졌다 합니다(완료 토글). ✅
- "삭제" → 그 항목만 사라집니다. ✅
확인 ✅ — 동작이 셋으로 늘었지만, 상태를 바꾸는 코드는 전부 todoReducer 한 함수 안에 있습니다. 카운터에서 본 "한 곳에 모음"의 가치가, 동작이 많아진 지금 더 분명하게 느껴집니다.
정리
- 지시에 데이터가 필요하면, 문자열 대신 객체로 보냅니다 —
dispatch({ type: "add", text }). 이때 실어 보내는 데이터를 페이로드라 부릅니다. - action 타입은 객체들의 유니온으로 정합니다(
{ type: "add"; text: string } | ...). - 상태가 배열·객체면 원본을 고치지 말고
[...]·map·filter로 새것을 만들어 return 합니다. - 로직이 많은 상태는
useReducer, 단순한 상태는useState— 한 컴포넌트에서 둘을 같이 써도 됩니다.
연습 거리
- 할 일 개수와 완료 개수를 "3개 중 1개 완료" 처럼 목록 위에 표시하기
{ type: "clearDone" }지시를 추가해, 완료된 할 일을 한 번에 모두 삭제하기{ type: "edit"; id: number; text: string }지시를 추가해, 기존 할 일의 텍스트를 수정하기

댓글
댓글을 작성하려면 이 필요합니다.