메인 콘텐츠로 바로가기
노트 목록

할 일 목록 만들기 (useReducer)

김성박801
목차9

튜토리얼 — 할 일 목록 만들기 (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 — 한 컴포넌트에서 둘을 같이 써도 됩니다.

연습 거리

  1. 할 일 개수와 완료 개수를 "3개 중 1개 완료" 처럼 목록 위에 표시하기
  2. { type: "clearDone" } 지시를 추가해, 완료된 할 일을 한 번에 모두 삭제하기
  3. { type: "edit"; id: number; text: string } 지시를 추가해, 기존 할 일의 텍스트를 수정하기

댓글

불러오는 중...

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