- 할 일 등록: 텍스트와 마감기한을 함께 입력
- 목록 초기화: 전체 항목 삭제 및 ID 초기화
- 할 일 삭제: 항목 개별 삭제
- 완료 체크: 체크박스를 통한 완료 여부 표시
- 목록 분류: 진행 중 / 완료 항목 분리
- 정렬: 마감기한 기준 오름차순 정렬
- 마감기한 수정: 날짜 직접 편집 가능
- Local Storage 저장: 새로고침 후에도 데이터 유지
src/
├─ components/ # 재사용 가능한 UI 컴포넌트들 모음
│ ├─ AddTodo.jsx # 새 할 일 입력 폼 (텍스트 + 마감일 입력)
│ ├─ ResetTodos.jsx # 전체 목록 초기화 버튼 컴포넌트
│ ├─ TodoItem.jsx # 단일 할 일 렌더링 (삭제, 체크, 수정 포함)
│ └─ TodoList.jsx # 할 일 배열을 받아 목록 렌더링
├─ hooks/
│ └─ useTodos.js # 상태 관리 커스텀 훅 (등록, 삭제, 수정 등)
├─ utils/
│ └─ storage.js # 로컬 스토리지 유틸 함수 모음
├─ App.jsx # 애플리케이션 루트, 훅과 컴포넌트 연결
| 이름 | 타입 | 설명 |
|---|---|---|
todos |
state | 현재 할 일 목록 상태 배열 (객체 형태) |
nextId |
ref | 다음 할 일의 고유 ID 값을 저장 |
addTodo |
함수 | 새 할 일을 todos에 추가 |
resetTodos |
함수 | 전체 초기화, todos 및 nextId 리셋 |
deleteTodo |
함수 | 특정 ID 항목 제거 |
toggleTodo |
함수 | 완료 상태 토글 (checked 필드 반전) |
editTodo |
함수 | 마감기한 수정 |
- 사용자가 텍스트 입력받아 새로운 할 일 등록
- 마감기한 입력 가능
| 이름 | 타입 | 설명 |
|---|---|---|
text |
string | 할 일 텍스트 입력값 |
deadline |
string | 마감기한 (YYYY-MM-DD) |
onAdd |
function | addTodo 실행 함수 |
newTodo |
object | 생성될 {id, text, checked, deadline} 객체 |
- form 제출 이벤트 발생
- text와 deadline 추출
- addTodo 함수 호출 → todos 상태에 새 객체 추가
- 입력 필드 초기화 처리
🔍 AddTodo 컴포넌트 코드
//AddTodo 컴포넌트
export default function AddTodo({ onAdd }) {
return (
<form
className="flex flex-wrap items-center justify-center gap-3 px-5 pb-5"
onSubmit={(event) => {
event.preventDefault();
//값 가져오기
const text = event.target.todo.value;
const deadline = event.target.deadline.value;
//상태 처리(addTodo 함수)
onAdd(text, deadline);
//입력 폼 초기화
event.target.todo.value = "";
event.target.deadline.value = "";
}}
>
<input type="text" name="todo" placeholder="새로운 할 일" />
<input type="date" name="deadline" />
<input type="submit" value="등록" />
</form>
);
}🔍 addTodo 함수 코드
//커스텀 훅 내부 추가 로직
const addTodo = (text, deadline) => {
//새로운 Todo 객체 생성
const newTodo = {
id: nextId.current,
text,
checked: false,
deadline,
};
//상태 업데이트
setTodos([newTodo, ...todos]);
nextId.current++;
};- 전체 할 일을 삭제하고 초기 상태로 되돌리는 기능 (confirm 확인 포함)
| 이름 | 타입 | 설명 |
|---|---|---|
onReset |
function | resetTodos 실행 함수 |
confirm()으로 사용자 의사 확인- true일 경우 상태를 초기 상태로 되돌림
🔍 ResetTodos 컴포넌트 코드
//ResetTodos 컴포넌트
export default function ResetTodos({ onReset }) {
return <button onClick={onReset}>리셋</button>; //클릭하면 resetTodos 함수 호출
}🔍 resetTodos 함수 코드
//커스텀 훅 내부 리셋 로직
const resetTodos = () => {
//한 번 더 확인
if (confirm("정말 초기화할까요?")) {
//상태 초기화
setTodos(initState);
nextId.current = initState.length + 1;
}
};- 특정 ID에 해당하는 항목을 목록에서 제거
| 이름 | 타입 | 설명 |
|---|---|---|
id |
number | 삭제 대상 항목의 고유 ID |
- 버튼이 눌린 항목의 id를 비교
- 삭제하려는 항목 제외하고 새로운 todos 구성 및 업데이트
🔍 deleteTodo 함수 코드
//커스텀 훅 내부 삭제 로직
const deleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};- 체크 표시를 통해 진행 or 완료 상태 표시
| 이름 | 타입 | 설명 |
|---|---|---|
id |
number | 상태를 반전시킬 항목 ID |
- 해당 항목의 id를 비교
- id 비교를 통해 checked 토글하여 새로운 todos 구성 및 업데이트
🔍 toggleTodo 함수 코드
//커스텀 훅 내부 토글 로직
const toggleTodo = (id) => {
setTodos(
todos.map((todo) => {
return todo.id === id ? { ...todo, checked: !todo.checked } : todo;
})
);
};- 완료 여부(
checked)에 따라 두 개의 리스트로 분리하여 렌더링
| 이름 | 타입 | 설명 |
|---|---|---|
undoneTodos |
Array | 진행 중인 항목 리스트 |
doneTodos |
Array | 완료된 항목 리스트 |
- App내에서 todos 배열을 checked 여부에 따라 두 그룹으로 분리
- App에서 목록 리턴할 때, 2개로 나뉜 리스트를 따로 보여주기
- 어차피 TodoList에 보여줄 목록을 넘겨주기 때문에 가능
🔍 App 컴포넌트 코드
//App.jsx에서 진행 중인 일, 완료된 일로 나누기
const undoneTodos = todos.filter((todo) => !todo.checked);
const doneTodos = todos.filter((todo) => todo.checked);
return (
<>
{/** 생략된 로직 */}
...
{/**조회(삭제, 수정) */}
<div>
{/**진행 중 */}
<section>
<h2>진행</h2>
<TodoList
todos={todos.filter((todo) => !todo.checked)}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
</section>
{/**완료됨 */}
<section>
<h2>완료</h2>
<TodoList
todos={todos.filter((todo) => todo.checked)}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
</section>
</div>
</>
);- 마감기한이 임박한 순으로 정렬
| 이름 | 타입 | 설명 |
|---|---|---|
sortedTodos |
Array | 정렬된 할 일 배열 |
aHasDeadline |
Boolean | 항목 A의 마감기한 존재 여부 |
bHasDeadline |
Boolean | 항목 B의 마감기한 존재 여부 |
- 기한이 있는 항목을 우선 표시
- 마감일이 빠를수록 위에 표시
- 마감일이 없는 항목은 등록 순서대로 유지
🔍 TodoList 컴포넌트 코드
//TodoList 컴포넌트
export default function TodoList({ todos, onToggle, onDelete, onEdit }) {
const sortedTodos = [...todos].sort((a, b) => {
//기한 있는지 확인
const aHasDeadline = !!a.deadline;
const bHasDeadline = !!b.deadline;
// 둘 다 기한 있음 -> 날짜 빠른 순 (오름차순)
if (aHasDeadline && bHasDeadline) {
return new Date(a.deadline) - new Date(b.deadline);
}
if (aHasDeadline) return -1; // a만 기한 있음 -> a가 앞
if (bHasDeadline) return 1; // b만 기한 있음 -> b가 앞
// 둘 다 기한 없음 -> 최근에 등록된 게 위로
return b.id - a.id;
});
//정렬된 목록으로 map 돌리기
return (
<ul>
{sortedTodos.map((todo) => {
return (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
onEdit={onEdit}
></TodoItem>
);
})}
</ul>
);
}- 기존 마감기한을 수정
| 이름 | 타입 | 설명 |
|---|---|---|
todo |
object | 수정 대상 항목 |
newDeadline |
string | 사용자가 입력한 새로운 날짜 |
onEdit |
function | editTodo 실행 함수 |
- 날짜 클릭 → 수정 모드로 전환
- 저장 클릭 시 editTodo 호출 → 상태 업데이트
- 취소 클릭 시 수정 모드 종료
🔍 TodoItem 컴포넌트 코드
//TodoItem 컴포넌트
export default function TodoItem({ todo, onToggle, onDelete, onEdit }) {
//기한을 수정 중인지에 따라 UI 달라짐 -> 상태로 표현
const [isEditing, setIsEditing] = useState(false);
return (
<li>
{/**생략된 로직 */}
...
{/**마감 기한(수정 가능) */}
{isEditing ? (
//수정 중일 때
<>
<input
type="date"
defaultValue={todo.deadline || ""}
/>
<button
onClick={(e) => {
//previousSibling : 현재 태그의 바로 전 형제 태그
const newDeadline = e.target.previousSibling.value; //여기선 date값을 읽어오는 것
onEdit(todo.id, newDeadline); //editTodo 호출
setIsEditing(false);
}}
>
저장
</button>
<button
onClick={() => setIsEditing(false)}
>
취소
</button>
</>
) : (
//수정 중 아닐 때
<span
onClick={() => setIsEditing(true)}
title="클릭하면 수정 가능"
>
{todo.deadline || "기한 없음"}
</span>
)}
</div>
{/** 생략된 로직 */}
...
</li>
);
}🔍 editTodo 함수 코드
//커스텀 훅 내부 수정 로직
const editTodo = (id, newDeadline) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, deadline: newDeadline } : todo
)
);
};- 사용자의 todos 상태를 브라우저 로컬 스토리지에 저장해 새로고침해도 유지
| 이름 | 타입 | 설명 |
|---|---|---|
initState |
Array | 초기 할 일 목록, 로컬 스토리지에 값이 없을 때 기본값으로 사용 |
getItem |
function | 저장된 todos 불러오기 |
setItem |
function | 변경된 todos 저장하기 |
- useTodos 초기 상태를
getItem()으로 설정- todos가 이미 존재한다면 존재하는 값으로, 없다면 initState로
- useEffect로 todos가 바뀔 때마다
setItem()실행
🔍 useTodos 커스텀 훅 코드
//커스텀 훅
export function useTodos(initState) {
//값이 있다면 가져오고 없다면 initState로 초기 설정
const [todos, setTodos] = useState(() => {
return getItem("todos", initState);
});
const nextId = useRef(todos.length + 1);
//변경될 때마다 스토리지에 저장
useEffect(() => {
setItem("todos", todos);
}, [todos]);
// 생략된 로직
...
}🔍 로컬 스토리 코드
//storage.js
const storage = window.localStorage;
export const setItem = (key, value) => {
try {
storage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error("Storage set srror : ", e);
}
};
export const getItem = (key, defaultValue) => {
try {
const storedValue = storage.getItem(key);
if (storedValue) {
//todos가 이미 존재하면 있는 거 반환
return JSON.parse(storedValue);
}
setItem(key, defaultValue); //아니면 초기값으로 설정 후 반환
return defaultValue;
} catch (e) {
console.error("Storage get error : ", e);
return defaultValue;
}
};