UMC 8th Web 워크북

🌐리액트로 Todo List 만들기⛏️

minnote29 2025. 3. 24. 03:26

더보기

폴더 구조는 다음과 같이 설정했다.

pages 폴더 안에 TodoList 폴더를 만들고 components 폴더 안에 CompletedList.tsx, TodoInput.tsx, TodoItem.tsx, TodoList.tsx를 나눠 컴포넌트 분리를 해서 구조를 이뤘다. 그리고 이 컴포넌트를 감싸는 Todo.tsx를 구성했고, Styles.ts 같은 경우는 한꺼번에 관리하도록 스타일 파일을 하나만 뒀다.

 

📌App.tsx

import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Todo from "./pages/TodoList/Todo";

const App: React.FC = () => {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Todo />} />
      </Routes>
    </Router>
  );
};

export default App;
  • 굳이 라우트를 설정해서 할 필요는 없지만 추가로 설정을 해봤다.

 

📌CompletedList.tsx

import React from "react";
import TodoItem from "./TodoItem";
import * as S from "../Styles"

interface CompletedListProps {
  completedTodos: string[];
  onDelete: (index: number) => void;
}

const CompletedList: React.FC<CompletedListProps> = ({ completedTodos, onDelete }) => {
  return (
    <S.CompletedContainer>
      <S.CompleteTitle>완료</S.CompleteTitle>
      {completedTodos.map((todo, index) => (
        <TodoItem key={index} text={todo} onDelete={() => onDelete(index)} />
      ))}
    </S.CompletedContainer>
  );
};

export default CompletedList;

1. CompletedList 컴포넌트

CompletedList는 완료된 할 일 목록을 표시하는 컴포넌트이다. 이 컴포넌트는 completedTodos라는 배열과 onDelete라는 함수 두 가지 props를 받는다.

  • completedTodos는 완료된 할 일 항목들의 배열이고, 각 항목은 문자열(string[])로 전달된다.
  • onDelete는 항목을 삭제할 때 호출되는 함수로, 삭제할 항목의 인덱스를 파라미터로 받는다.
  • S.CompletedContainer: 스타일링된 컨테이너로, 완료된 할 일 목록 전체를 감싸고 있다.
  • S.CompleteTitle: "완료"라는 제목을 표시하는 스타일링된 제목이다.
  • completedTodos.map()을 사용하여 completedTodos 배열의 각 항목을 순회하며, 각 항목마다 TodoItem 컴포넌트를 렌더링한다. 이때, 각 TodoItem에 key, text, 그리고 onDelete를 props로 전달한다.
  • key={index}: React에서는 리스트 항목을 렌더링할 때 각 항목에 고유한 key를 설정해야 하므로, 여기서는 index를 사용한다.
  • text={todo}: todo 항목의 내용을 TodoItem에 전달하여 표시한다.
  • onDelete={() => onDelete(index)}: onDelete 함수는 해당 항목을 삭제하기 위해 호출되며, 인덱스를 onDelete 함수에 전달한다.
  • TodoItem 컴포넌트는 각 할 일 항목을 나타내는 작은 컴포넌트로, 여기서는 text(할 일 내용)와 onDelete(삭제 기능)을 전달받아 사용한다.

2. 컴포넌트의 동작

  1. 완료된 할 일 목록을 표시: completedTodos 배열에 있는 항목들을 map() 함수로 순차적으로 렌더링하여 각 항목을 TodoItem 컴포넌트로 표시한다.
  2. 삭제 기능: 각 할 일 항목에는 삭제 버튼이 있으며, 이를 클릭하면 onDelete(index)가 호출되어 해당 항목을 삭제한다. onDelete는 상위 컴포넌트로부터 전달된 콜백 함수로, 완료된 할 일 목록에서 특정 항목을 제거한다.

 

📌TodoInput.tsx

import React, { useState } from "react";
import * as S from "../Styles";

interface TodoInputProps {
  onAddTodo: (text: string) => void;
}

const TodoInput: React.FC<TodoInputProps> = ({ onAddTodo }) => {
  const [text, setText] = useState("");

  const handleAddTodo = () => {
    if (text.trim()) {
      onAddTodo(text);
      setText("");
    }
  };

  return (
    <S.InputContainer>
      <S.Input
        type="text"
        placeholder="할 일 입력"
        value={text}
        onChange={(e) => setText(e.target.value)}
        onKeyPress={(e) => e.key === "Enter" && handleAddTodo()}
      />
      <S.AddButton onClick={handleAddTodo}>할 일 추가</S.AddButton>
    </S.InputContainer>
  );
};

export default TodoInput;

1. TodoInput 컴포넌트

사용자가 할 일을 입력하고, 입력 후 "할 일 추가" 버튼을 클릭하거나 Enter 키를 눌러 할 일을 추가할 수 있게 해주는 기능을 한다.

  1. Props 정의: TodoInput 컴포넌트는 onAddTodo라는 함수 형태의 props를 받는다. 이 함수는 새로운 할 일 텍스트를 인자로 받아서 상위 컴포넌트에 전달하는 역할을 한다. TodoInput 컴포넌트는 이 props를 사용하여 사용자가 입력한 텍스트를 추가하는 기능을 수행한다.
  2. 상태 관리 (useState): 컴포넌트 내부에서 text라는 상태 변수를 사용하여 사용자가 입력한 텍스트를 관리한다. text 상태는 useState 훅을 통해 초기값은 빈 문자열("")로 설정되고, 사용자가 입력 필드에 값을 입력할 때마다 상태가 업데이트된다. setText는 상태 업데이트 함수로, 입력 필드의 값이 변경될 때 호출된다.
  3. 할 일 추가 함수: handleAddTodo 함수는 사용자가 "할 일 추가" 버튼을 클릭하거나 Enter 키를 눌렀을 때 실행된다. 이 함수는 입력된 텍스트(text)가 비어 있지 않은지 확인하고, 비어 있지 않으면 onAddTodo 함수로 텍스트를 전달한다. 텍스트가 전달된 후에는 setText를 호출하여 입력 필드를 비운다.
  4. 렌더링 부분: JSX에서는 S.InputContainer로 감싼 입력 필드버튼을 렌더링한다.
    • 입력 필드 (S.Input): 이 필드는 사용자가 할 일을 입력하는 부분. value 속성에 text 상태를 바인딩하여 입력 필드의 값이 상태와 동기화되도록 한다. onChange 이벤트 핸들러는 사용자가 입력할 때마다 setText를 호출하여 상태를 업데이트한다. onKeyPress 이벤트에서는 Enter 키가 눌렸을 때 handleAddTodo를 호출하여 할 일을 추가하도록 한다.
    • 할 일 추가 버튼 (S.AddButton): 이 버튼을 클릭하면 handleAddTodo 함수가 호출되어 입력된 텍스트가 할 일 목록에 추가된다.

2. 컴포넌트의 동작

  • 사용자가 입력 필드에 할 일 텍스트를 입력한다.
  • 텍스트가 변경될 때마다 setText를 호출하여 상태가 업데이트된다.
  • 사용자가 Enter 키를 누르거나 할 일 추가 버튼을 클릭하면, handleAddTodo 함수가 호출된다.
  • handleAddTodo는 입력된 텍스트가 공백이 아니면 onAddTodo 함수를 호출하여 텍스트를 상위 컴포넌트로 전달하고, 그 후 입력 필드를 비운다.

 

📌TodoItem.tsx

import React from "react";
import * as S from "../Styles"

interface TodoItemProps {
  text: string;
  onComplete?: () => void;
  onDelete?: () => void;
}

const TodoItem: React.FC<TodoItemProps> = ({ text, onComplete, onDelete }) => {
  return (
    <S.ItemContainer>
      <S.ItemText>{text}</S.ItemText>
      {onComplete && <S.CompleteButton onClick={onComplete}>완료</S.CompleteButton>}
      {onDelete && <S.DeleteButton onClick={onDelete}>삭제</S.DeleteButton>}
    </S.ItemContainer>
  );
};

export default TodoItem;

1. TodoItem 컴포넌트

각 할 일 항목을 렌더링하고, 해당 항목에 대해 완료 버튼과 삭제 버튼을 제공한다. 사용자가 버튼을 클릭하면 각각 할 일의 완료 상태를 변경하거나 삭제된다.

2. 컴포넌트의 동작

  • text: 할 일 항목의 내용을 나타내는 문자열이다.
  • onComplete: 할 일이 완료되었을 때 호출할 선택적 함수. 이 함수는 할 일을 완료 처리하는 역할을 하며, 완료 버튼을 클릭하면 호출된다.
  • onDelete: 할 일을 삭제할 때 호출할 선택적 함수. 이 함수는 삭제 버튼을 클릭할 때 실행된다.
  • 컴포넌트는 S.ItemContainer로 감싸져서 화면에 표시된다. 이 컨테이너 안에는 할 일 내용이 S.ItemText로 표시된다.
  • onComplete와 onDelete가 각각 존재하면, 완료 버튼과 삭제 버튼이 화면에 렌더링.
  • 완료 버튼은 onComplete 함수가 전달되었을 때만 나타나며, 클릭 시 onComplete 함수 실행.
  • 삭제 버튼은 onDelete 함수가 전달되었을 때만 나타나며, 클릭 시 onDelete 함수 실행.

 

 

📌TodoList.tsx

import React from "react";
import TodoItem from "./TodoItem";
import * as S from "../Styles";

interface TodoListProps {
  todos: string[];
  onComplete: (index: number) => void;
}

const TodoList: React.FC<TodoListProps> = ({ todos, onComplete }) => {
  return (
    <S.ListContainer>
      <S.Title>할 일</S.Title>
      {todos.map((todo, index) => (
        <TodoItem key={index} text={todo} onComplete={() => onComplete(index)} />
      ))}
    </S.ListContainer>
  );
};

export default TodoList;

1. TodoList 컴포넌트

사용자가 입력한 할 일 목록을 표시하고, 각 할 일 항목에 대해 완료 기능을 한다. 상위 컴포넌트에서 받은 todos 배열을 사용해 목록을 생성하고, 각 항목에 대해 완료 기능을 처리하는 onComplete 함수를 전달한다.

2. 컴포넌트의 동작

 

  • todos: 완료되지 않은 할 일 항목들의 배열이다. 각 항목은 문자열로 전달된다.
  • onComplete: 각 할 일 항목이 완료되었을 때 호출할 함수이다. 함수는 해당 항목의 인덱스를 인자로 받아, 할 일을 완료로 처리하는 역할을 한다.
  • TodoList 컴포넌트는 todos 배열을 map 함수로 순회하며, 각 할 일 항목을 TodoItem 컴포넌트로 변환하여 렌더링한다. 각 TodoItem은 하나의 할 일을 나타내며, 그에 대해 onComplete를 호출할 수 있다.
  • TodoItem 컴포넌트는 각 할 일 항목을 표시하고, 완료 버튼을 클릭하면 onComplete 함수가 호출되어 해당 항목을 완료 상태로 처리하게 된다.
  •  todos 배열에 있는 항목들을 순차적으로 렌더링하며, 각 항목에 대해 TodoItem을 표시이다. 

 

📌Todo.tsx

import React, { useState } from "react";
import TodoInput from "./components/TodoInput";
import TodoList from "./components/TodoList";
import CompletedList from "./components/CompletedList";
import * as S from "./Styles"; 

const Todo: React.FC = () => {
  const [todos, setTodos] = useState<string[]>([]);
  const [completedTodos, setCompletedTodos] = useState<string[]>([]);

  const addTodo = (text: string) => {
    setTodos([...todos, text]);
  };

  const completeTodo = (index: number) => {
    const newTodos = [...todos];
    const completedTask = newTodos.splice(index, 1)[0] ?? ""; 
    setTodos(newTodos);
    setCompletedTodos([...completedTodos, completedTask]);
  };

  const deleteTodo = (index: number) => {
    setCompletedTodos((prevCompleted) => prevCompleted.filter((_, i) => i !== index));
  };

  return (
    <S.Container>
      <S.Title>UMC TODOLIST</S.Title>
      <TodoInput onAddTodo={addTodo} />
      <S.Wrapper>
        <TodoList todos={todos} onComplete={completeTodo} />
        <CompletedList completedTodos={completedTodos} onDelete={deleteTodo} />
      </S.Wrapper>
    </S.Container>
  );
};

export default Todo;

1. Todo 컴포넌트

사용자가 할 일을 입력하고, 완료된 할 일을 표시하며, 삭제 기능도 제공하고, 상태 관리와 자식 컴포넌트 간 데이터 전달을 처리한다. 

 

  • todos는 완료되지 않은 할 일 목록을 관리하는 상태. useState<string[]>([])로 초기값을 빈 배열로 설정한다.
  • completedTodos는 완료된 할 일 목록을 관리하는 상태이다. 
  • 함수
    • addTodo: 사용자가 새로운 할 일을 입력하면 이 함수가 호출된다. 입력받은 텍스트를 todos 배열에 추가하는 함수이다. setTodos([...todos, text])로 새 할 일을 배열에 추가한다.
    • completeTodo: 할 일을 완료 상태로 변경하는 함수이다. 인덱스를 받아서 해당 할 일을 todos 배열에서 제거하고, completedTodos 배열에 추가한다. splice를 사용해 해당 항목을 todos에서 삭제하고, 그 항목에 추가한다.
    • deleteTodo: 완료된 할 일 중에서 삭제하는 함수이다. completedTodos 배열에서 특정 인덱스를 가진 항목을 삭제하고, filter를 사용하여 해당 인덱스를 제외한 새로운 배열을 만들어 상태를 업데이트한다.

2. 컴포넌트의 동작

  • TodoInput 컴포넌트에서 사용자가 입력한 새로운 할 일 텍스트는 addTodo 함수를 통해 todos 상태에 추가된다.
  • TodoList 컴포넌트에서 각 할 일 항목에 대해 완료 버튼이 클릭되면 completeTodo 함수가 호출되어 해당 할 일이 todos에서 삭제되고, completedTodos에 추가된다.
  • CompletedList 컴포넌트에서 삭제 버튼이 클릭되면 deleteTodo 함수가 호출되어 완료된 할 일이 completedTodos에서 삭제된다.

 

📌 스타일 컴포넌트를 이용한 스타일 적용

import styled from "styled-components";

export const InputContainer = styled.div`
  display: flex;
  justify-content: start;
  margin: 10px 0;
`;

export const Input = styled.input`
  flex: 1;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 5px;
`;

export const AddButton = styled.button`
  background-color: #28a745;
  color: white;
  padding: 8px 12px;
  margin-left: 5px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  
  &:hover {
    background-color: #218838;
  }
`;

export const ItemContainer = styled.div`
  display: flex;
  background-color: white;
  margin: 5px 0;
  padding: 10px;
  border-radius: 5px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
`;

export const ItemText = styled.span`
  text-align: left;
  flex: 1;
`;

export const CompleteButton = styled.button`
  background-color: #28a745;
  color: white;
  padding: 5px 10px;
  border: none;
  border-radius: 5px;
  cursor: pointer;

  &:hover {
    background-color: #218838;
  }
`;

export const DeleteButton = styled.button`
  background-color: #dc3545;
  color: white;
  padding: 5px 10px;
  border: none;
  border-radius: 5px;
  cursor: pointer;

  &:hover {
    background-color: #c82333;
  }
`;

export const ListContainer = styled.div`
  margin: 5px 0;
  width: 240px;
  min-height: fit-content; 
  transition: height 0.3s ease-in-out;
`;

export const Title = styled.h2`
  font-size: 20px;
  margin-bottom: 15px;
`;

export const CompletedContainer = styled.div`
  margin: 5px 0;
  width: 240px;
`;

export const CompleteTitle = styled.h2`
  font-size: 20px;
  margin-bottom: 10px;
`;

export const Container = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center; 
  align-items: center; 
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 15px;
  width: fit-content; 
  max-width: 360px;
  min-height: 200px;
  background: white;
  border-radius: 10px;
  box-shadow: 0 4px 10px #ccc;
  text-align: center;
  transition: height 0.3s ease-in-out;
`;

export const Wrapper = styled.div`
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
  gap: 10px;
  transition: height 0.3s ease-in-out;
`;