DEVELOPMENT/PROJECTS

[Project] TODO LIST

Watnu 2025. 2. 6. 23:04

실행 결과 미리보기



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;

 


02

기능구현

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 값을 반대로 변경하도록 구현했습니다.

 

" 동작 과정 "

  1. todo.id === target이면 기존 Todo 객체를 복사하고 isDone 값을 반전시킴
  2. 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>
  );

03

업그레이드 (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,
    });
  };

04

마무리

이번 프로젝트에서는 Todo 리스트를 만들면서 UI 설계, 상태 관리, 그리고 기능 구현까지 단계별로 진행했습니다.

추가, 업데이트, 검색, 삭제 기능을 구현해 보면서 AI 없이 손에 익을 수 있도록 코딩했습니다.

 

프로젝트에서는

초기에는 useState을 사용하다가, useReducer로 리팩터링 하면서코드가 더 깔끔하고 관리하기 쉬워졌습니다!
dispatch 하나로 추가, 수정, 삭제 로직을 한 곳에서 처리할 수 있어서 중복 코드도 줄었고,
나중에 기능을 더 추가할 때도 이제 액션만 추가하면 되니까 훨씬 편해졌습니다.