[Project] TODO LIST
01
UI 구현
1. 폴더구조 잡기
먼저 컴포넌트의 역할을 명확히 구분하기 위해 폴더 구조를 설계했습니다.
각 컴포넌트가 독립적으로 관리될 수 있도록 /components 폴더를 만들고 기능별 디렉터리를 생성했습니다.
각 폴더에는 index.jsx파일을 배치해서 import 경로를 단순화했습니다.
이렇게 하면 코드가 모듈화 되어각 컴포넌트를 독립적으로 관리할 수 있게 됩니다.
또한 CSS는 module.css 파일을 활용해 스타일 충돌 방지를 했습니다.
\---src
|---App.css
| App.jsx
| index.css
\---components
+---header //상단 헤더
| index.jsx
| header.module.css
+---editor //할 일(todo) 입력창
| index.jsx
| editor.module.css
+---todoList //To-Do 목록
| index.jsx
| todoList.module.css
\---todoItem //개별 To-Do 아이템
index.jsx
todoItem.module.css
2. Header
페이지 최상단에서 앱 제목과 현재 시간을 표시합니다.
new Date()를 이용해 현재 시간을 불러오고 WEEKS와 MONTHS 배열을 활용해 포맷을 맞췄습니다.
import React from "react";
import styles from "./header.module.css";
const WEEKS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
const Header = () => {
const date = new Date();
const currentDate = `${WEEKS[date.getDay()]} ${
MONTHS[date.getMonth()]
} ${date.getDate()} ${date.getFullYear()}`;
return (
<section>
<h3 className={styles.headerSubTitle}>오늘은 📆</h3>
<h1 className={styles.headerTitle}>{currentDate}</h1>
</section>
);
};
export default Header;
3. Editor
사용자가 새로운 TODO를 입력하고 추가 버튼을 누르면 상태가 업데이트되도록 합니다.
import React from "react";
import styles from "./editor.module.css";
const Editor = () => {
return (
<section className={styles.todoEditor}>
<input
type="text"
placeholder="새로운 Todo..."
className={styles.todo_input}
/>
<button className={styles.todo_add}>추가</button>
</section>
);
};
export default Editor;
4. Todo List와 Todo Item
사용자가 입력한 할 일 목록을 렌더링 하는 TodoList와 개별 항목을 담당하는 TodoItem으로 구현했습니다.
Todo의 데이터는 App.jsx에서 mockTodos로 선언하고 사용했습니다.
const mockTodos = [
{ id: 0, content: "할 일 0", date: new Date().getTime(), isDone: true },
{ id: 1, content: "할 일 1", date: new Date().getTime(), isDone: false },
{ id: 2, content: "할 일 2", date: new Date().getTime(), isDone: false },
];
import React from "react";
import TodoItem from "../todoItem";
import styles from "./todoList.module.css";
const TodoList = ({ mockTodos }) => {
return (
<section className={styles.todoList}>
<h3>Todo List 🌱</h3>
<input type="text" placeholder="검색어를 입력하세요" />
{mockTodos.map((todo, index) => {
return (
<TodoItem
key={index}
id={todo.id}
text={todo.content}
isDone={todo.isDone}
date={todo.date}
/>
);
})}
</section>
);
};
export default TodoList;
import React from "react";
import styles from "./todoItem.module.css";
const TodoItem = ({ id, content, isDone, date }) => {
return (
<div className={styles.todoItem}>
<div className={styles.item}>
<input type="checkbox" checked={isDone} />
<p className={styles.todo}>
{content}
</p>
</div>
<div className={styles.item}>
<p className={styles.date}>{date}</p>
<button className={styles.delete_btn}>삭제</button>
</div>
</div>
);
};
export default TodoItem;
기능구현
1. Todo 추가하기
1️⃣ 새로운 Todo 생성
- App.jsx에서 새로운 Todo를 추가할 때 ID가 겹치지 않도록 idRef를 사용했습니다.
초기 moxkTodos의 마지막 ID가 2이므로 새로운 Todo의 id는 3부터 시작하도록 설정했습니다.
//App.jsx
const idRef = useRef(3);
const createTodo = (content) => {
const newTodo = {
//idRef를 이용해서 중복되지않게 id 생성
id: idRef.current++,
content: content,
date: new Date().getTime(),
isDone: false,
};
return newTodo;
};
2️⃣ Todo 추가 로직
- 새로운 Todo를 todos 배열에 추가하는 onCreate 함수를 만들어 Editor 컴포넌트로 전달했습니다.
//App.jsx
const onCreate = (content) => {
setTodos((prevTodos) => [...prevTodos, createTodo(content)]);
};
return (
<>
<main>
<Header />
<Editor onCreate={onCreate} />
<TodoList mockTodos={todos} />
</main>
</>
);
3️⃣ 입력 기능 추가
- 사용자가 input에 입력한 값을 content 상태에 저장하고 버튼을 누르면 onCreate 실행합니다.
- Enter 키를 눌러도 추가할 수 있도록 onKeyDown 이벤트 핸들러 추가했습니다.
- input이 비어있으면 focus를 이동해 입력을 유도했습니다.
import React, { useRef, useState } from "react";
import styles from "./editor.module.css";
const Editor = ({ onCreate }) => {
const contentRef = useRef();
const [content, setContent] = useState("");
const handleNewTodo = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
if (content.trim() === "") {
contentRef.current.focus();
return;
}
onCreate(content);
setContent("");
};
const onKeyDown = (e) => {
if (e.key === "Enter") {
onSubmit();
}
};
return (
<section className={styles.todoEditor}>
<input
type="text"
value={content}
onChange={handleNewTodo}
onKeyDown={onKeyDown}
ref={contentRef}
placeholder="새로운 Todo..."
className={styles.todo_input}
/>
<button onClick={onSubmit} className={styles.todo_add}>
추가
</button>
</section>
);
};
export default Editor;
2. Todo 렌더링 하기
1️⃣ Todo List 형태로 렌더링
mockTodos 데이터를 받아와 map 함수를 이용해 리스트 형태로 렌더링 했습니다.
초기에는 아래코드처럼 모든 속성을 개별적으로 TodoItem에 전달했었습니다.
{mockTodos.map((todo, index) => {
return (
<TodoItem
key={index}
id={todo.id}
content={todo.content}
isDone={todo.isDone}
date={todo.date}
/>
);
})}
이후... todo(구조분해할당)를 사용해 객체 전체를 props로 전달할 수 있도록 수정했습니다.
또한 key 값은 index값대신 고유 id값으로 설정해 렌더링 최적화를 했습니다.
{todos.map((todo) => {
return <TodoItem key={todo.id} {...todos} />;
})}
2️⃣ 검색 기능 추가
검색어를 keyword에 저장하고, getFilteredTodos 함수를 만들어 검색어가 포함된 Todo만 필터링했습니다.
- 검색어가 비어 있으면 전체 목록을 반환, filter를 사용해 검색어가 포함되어 있다면 포함된 todos 렌더링
- toLowerCase()를 사용해 대소문자 구분 없이 검색 가능하게 했습니다.
const TodoList = ({ todos }) => {
const [keyword, setKeyword] = useState("");
const onChangeKeyword = (e) => {
setKeyword(e.target.value);
};
const getFilteredTodos = () => {
if (keyword.trim() === "") {
return todos;
}
return todos.filter(
(todo) => todo.content.toLowerCase().includes(keyword.toLowerCase())
);
};
const filteredTodos = getFilteredTodos();
return (
<section className={styles.todoList}>
<h3>Todo List 🌱</h3>
<input
type="text"
value={keyword}
onChange={onChangeKeyword}
placeholder="검색어를 입력하세요"
/>
{filteredTodos.map((todo) => {
return <TodoItem key={todo.id} {...todo} />;
})}
</section>
);
};
3. Todo 수정하기
1️⃣ Todo 완료 상태 업데이트
onUpdate 함수를 통해 선택한 Todo의 isDone 값을 반대로 변경하도록 구현했습니다.
" 동작 과정 "
- todo.id === target이면 기존 Todo 객체를 복사하고 isDone 값을 반전시킴
- todo.id!== target이면 기존 Todo를 그대로 유지
//App.jsx
const onUpdate = (target) => {
const updatedTodos = todos.map((todo) => {
return (
todo.id === target ? { ...todo, isDone: !todo.isDone } : todo
);
});
setTodos(updatedTodos);
};
2️⃣ 체크박스 클릭 시 Todo 상태 변경
onUpdate 함수를 props로 전달하고, input [type="checkbox"]에서 onChange 이벤트가 발생하면 실행되도록 했습니다.
//TodoItem.jsx
import React from "react";
import styles from "./todoItem.module.css";
const TodoItem = ({ id, content, isDone, date, onUpdate }) => {
const onChangeCheck = () => {
onUpdate(id);
};
return (
<div className={styles.todoItem}>
<div className={styles.item}>
<input type="checkbox" checked={isDone} onChange={onChangeCheck} />
<p
className={`${styles.todo} ${
isDone ? styles.todo__done : styles.todo__yet
}`}
>
{content}
</p>
</div>
<div className={styles.item}>
<p className={styles.date}>{new Date(date).toLocaleDateString()}</p>
<button className={styles.delete_btn}>삭제</button>
</div>
</div>
);
};
export default TodoItem;
3️⃣ 완료된 Todo 스타일 적용
완료된 Todo는 빨간색 취소선 스타일을 추가했습니다.
이후 스타일의 추가를 위해 __done과 __yet 스타일 2개로 나누어 작업했습니다.
.todo__yet {
color: black;
}
.todo__done {
text-decoration: line-through;
color: red;
}
4. Todo 삭제하기
1️⃣ Todo 삭제
onDelete 함수를 통해 선택한 Todo를 삭제하도록 구현했습니다.
filter를 사용해 선택한 Todo의 id와 일치하지 않는 항목만 남긴 새 배열을 생성하고 setTodos를 통해 새로운 배열을 업데이트했습니다.
//App.jsx
const onDelete = (targetId) => {
const deletedTodos = todos.filter((todo) => {
return todo.id !== targetId;
});
setTodos(deletedTodos);
};
2️⃣ 삭제 버튼 클릭 시 Todo 삭제
onDelete 함수를 props로 전달하고, 삭제 버튼을 클릭하면 해당 Todo가 삭제되도록 설정했습니다.
//TodoItem.jsx
const onDeleteTodo = () => {
onDelete(id);
};
return (
<button className={styles.delete_btn} onClick={onDeleteTodo}>
삭제
</button>
);
업그레이드 (useReducer)
기존 useState를 사용한 방식 대신 useReducer를 활용하여 Todo의 생성, 수정, 삭제 상태를 관리해 보았습니다.
useReducer는 상태 업데이트 로직을 한 곳에서 관리할 수 있어 코드가 더 간결하고 명확 해질 수 있습니다.
- CREATE → 새로운 Todo를 추가
- UPDATE → isDone 상태를 반대로 변경
- DELETE → 특정 id를 가진 Todo를 삭제
1. useReducer를 사용한 상태 관리
모든 상태 업데이트 로직을 reducer 한 곳에서 관리합니다.
function reducer(state, action) {
switch (action.type) {
case "CREATE":
return [action.data, ...state];
case "UPDATE":
return state.map((todo) =>
todo.id === action.targetId ? { ...todo, isDone: !todo.isDone } : todo
);
case "DELETE":
return state.filter((todo) => todo.id !== action.targetId);
default:
return state;
}
}
2. Todo 추가 (onCreate)
onCreate는 dispatch를 사용하여 새로운 Todo를 추가합니다.
기존 createTodo와 onCreate를 각각 생성하여 관리하였지만 onCreate 하나로 통합했습니다.
const onCreate = (content) => {
dispatch({
type: "CREATE",
data: {
//idRef를 이용해서 중복되지않게 id 생성
id: idRef.current++,
content: content,
date: new Date().getTime(),
isDone: false,
},
});
};
3. Todo 완료 상태 변경 (onUpdate)
onUpdate는 선택된 Todo(targetId와 일치하는 항목)의 isDone 값을 반대로 변경합니다.
const onUpdate = (targetId) => {
dispatch({
type: "UPDATE",
targetId: targetId,
});
};
4. Todo 삭제 (onDelete)
onDelete는 해당 ID를 가진 Todo를 삭제합니다.
const onDelete = (targetId) => {
dispatch({
type: "DELETE",
targetId: targetId,
});
};
마무리
이번 프로젝트에서는 Todo 리스트를 만들면서 UI 설계, 상태 관리, 그리고 기능 구현까지 단계별로 진행했습니다.
추가, 업데이트, 검색, 삭제 기능을 구현해 보면서 AI 없이 손에 익을 수 있도록 코딩했습니다.
프로젝트에서는
초기에는 useState을 사용하다가, useReducer로 리팩터링 하면서코드가 더 깔끔하고 관리하기 쉬워졌습니다!
dispatch 하나로 추가, 수정, 삭제 로직을 한 곳에서 처리할 수 있어서 중복 코드도 줄었고,
나중에 기능을 더 추가할 때도 이제 액션만 추가하면 되니까 훨씬 편해졌습니다.