컴포넌트·reducer·폼·파생 상태를 업무형 화면으로 통합합니다.
입력, 도메인 상태 전이, 파생 목록과 빈 상태를 함께 다룹니다. reducer는 업무 규칙을 소유하고 컴포넌트는 action을 전달합니다.
완료 개수는 tasks에서 계산하고 같은 정보를 별도 state로 중복 저장하지 않습니다.
API 저장을 붙일 때 중복 제출 방지, 실패 복구, 서버 검증과 낙관적 업데이트 정책을 설계합니다.
입력, API, 필터와 렌더를 큰 컴포넌트 하나에 쌓으면 테스트와 변경 영향이 커집니다.
로컬 프로젝트에서 확인해보세요삭제 action과 진행 중 항목 필터를 추가하세요.
import { useReducer, useState } from "react";
function reducer(tasks, a) {
if (a.type === "add")
return [...tasks, { id: crypto.randomUUID(), title: a.title, done: false }];
if (a.type === "toggle")
return tasks.map((t) => (t.id === a.id ? { ...t, done: !t.done } : t));
return tasks;
}
export default function App() {
const [tasks, dispatch] = useReducer(reducer, [
{ id: "1", title: "React 복습", done: false },
]);
const [title, setTitle] = useState("");
function submit(e) {
e.preventDefault();
if (!title.trim()) return;
dispatch({ type: "add", title: title.trim() });
setTitle("");
}
return (
<main>
<h1>학습 업무</h1>
<form onSubmit={submit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
aria-label="새 업무"
/>
<button>추가</button>
</form>
<ul>
{tasks.map((t) => (
<li key={t.id}>
<label>
<input
type="checkbox"
checked={t.done}
onChange={() => dispatch({ type: "toggle", id: t.id })}
/>
{t.title}
</label>
</li>
))}
</ul>
<p>
완료 {tasks.filter((t) => t.done).length}/{tasks.length}
</p>
</main>
);
}