2차 공부/TIL

24.06.21 TodoList 페이지 기능추가하기

공대탈출 2024. 6. 21. 17:58

초기 컴포넌트 구분

초기엔 이렇게 컴포넌트를 나눠 재사용하려 했다.

하지만 코드를 작성하면서 TodoBox의 존재이유를 잘 모르겠어서 TodoBox를 삭제하고 InputBox안에 합치는 컴포넌트를 하나 넣기로 하였다.

최종 컴포넌트 구분

이렇게 한 이유는 기능적인 부분에서 나왔다. InputBox에서 Todo를 추가하면 해당 TodoList 배열 변화가 아래 TodoList 컴포넌트에도 전달되어야한다.

만약 InputBox와 TodoList컴포넌트가 완전히 분리되어있었다면, App.js에서 state를 관리해야하므로, 쓸모없는 리렌더링이 일어날 수 있어 TodoList를 InputBox에 넣기로 결정하였다.

 

import React, { useState } from 'react';
import CustomInput from './commons/CustomInput';
import CustomButton from './commons/CustomButton';
import TodoList from './TodoList';

function InputBox() {

    const [title, setTitle] = useState('');
    const [desc, setDesc] = useState('');
    const [id, setId] = useState(crypto.randomUUID());
    const [isDone, setIsDone] = useState(false);
    const [todoList, setTodoList] = useState([]);
    const titleHandler = (e) => {
        setTitle(e.target.value);
    };
    const descHandler = (e) => {
        setDesc(e.target.value);
    };
    const addTodoHandler = () => {
        setId(crypto.randomUUID());
        setTodoList([...todoList, { title: title, desc: desc, isDone: isDone, id: id }]);
        setTitle('');
        setDesc('');
    };

    return (
        <div>
            <div style={inputBoxWrap}>
                <div style={inputTitleWrap}>
                    <p>제목</p>
                    <CustomInput customStyle={customInputStyle} value={title} onChange={titleHandler} />
                    <p>내용</p>
                    <CustomInput customStyle={customInputStyle} value={desc} onChange={descHandler} />
                </div>
                <CustomButton
                    customStyle={hover ? customButtonStyleOver : customButtonStyleOut}
                    mouseOver={handleMouseOver}
                    mouseOut={handleMouseOut}
                    onClick={addTodoHandler}
                >
                    추가하기
                </CustomButton>
            </div>
            <TodoList todoList={todoList} setTodoList={setTodoList} />
        </div>
    );
}

export default InputBox;

보기 편하도록 style관련 변수는 제거해둔다.

먼저 Todo에 필요한 title, desc, id, isDone을 useState로 만들어주었다. id는 이전에 사용해보았던 crypto.randomUUID()를 사용하여 만들었다. 그리고 isDone은 완료상태를 나타내는 것이기 때문에 boolean값을 담아두었다.

또한 TodoList는 배열형태로 만들어 많은 Todo들을 담을 수 있도록 하였다.

 

    const titleHandler = (e) => {
        setTitle(e.target.value);
    };
    const descHandler = (e) => {
        setDesc(e.target.value);
    };
    
<CustomInput customStyle={customInputStyle} value={title} onChange={titleHandler} />
<CustomInput customStyle={customInputStyle} value={desc} onChange={descHandler} />
import React from 'react';

const CustomInput = ({ customStyle, value, onChange }) => {
    return <input style={customStyle} value={value} onChange={onChange} />;
};

export default CustomInput;

input부터 보도록 하자. 

CustomInput컴포넌트에 value, onChange라는 props를 내려준다. 각각 제목과 내용에 해당하는 값과 함수를 내려주면 된다.

CustomInput에서는 받은 props를 input태그에 넣어 변할때 Handler가 작동하도록 설정하였다.

각 Handler는 event.target.value값, 즉 input에 써진 값을 setTitle, setDesc하여 state를 변경시켰다.

 

const addTodoHandler = () => {
    setId(crypto.randomUUID());
    setTodoList([...todoList, { title: title, desc: desc, isDone: isDone, id: id }]);
    setTitle('');
    setDesc('');
};

그렇게 state에 값이 담겨지면 Id값을 생성하고, TodoList state값을 깊은 복사하여 새로운값을 추가해 바꿔준다.

그리고 input값을 비워야 하므로 title과 desc값을 비운다.

 

<TodoList todoList={todoList} setTodoList={setTodoList} />

마지막으로 TodoList컴포넌트에 todoList와 setTodoList를 props로 내려준다.


 

import React from 'react';
import Title from './commons/Title';
import Todo from './commons/Todo';

const TodoList = ({ todoList, setTodoList }) => {
    const todoBox = {
        display: 'flex',
        flexDirection: 'row',
        flexWrap: 'wrap',
        gap: '10px',
    };

    const toggleTodo = (id) => {
        let targetIndex = todoList.findIndex((todo) => todo.id === id);
        let newTodo = [...todoList];
        if (newTodo[targetIndex].isDone) {
            newTodo[targetIndex] = { ...newTodo[targetIndex], isDone: false };
        } else {
            newTodo[targetIndex] = { ...newTodo[targetIndex], isDone: true };
        }
        setTodoList([...newTodo]);
    };
    const deleteTodo = (id) => {
        let newTodo = todoList.filter((todo) => todo.id !== id);
        setTodoList([...newTodo]);
    };

    return (
        <div>
            <Title>Working... 🔥</Title>
            <div style={todoBox}>
                {todoList.map((todo) =>
                    todo.isDone === false ? (
                        <Todo todo={todo} toggleTodo={toggleTodo} deleteTodo={deleteTodo} key={todo.id} />
                    ) : null
                )}
            </div>
            <Title>Done..! 🎉</Title>
            <div style={todoBox}>
                {todoList.map((todo) =>
                    todo.isDone === true ? (
                        <Todo todo={todo} toggleTodo={toggleTodo} deleteTodo={deleteTodo} key={todo.id} />
                    ) : null
                )}
            </div>
        </div>
    );
};

export default TodoList;

다음으로 TodoList컴포넌트를 살펴보자.

TodoList컴포넌트는 Title - Todo - Title - Todo형식으로 사용자에게 보여진다. 각 Todo는 완료된 상태에 따라 나눠지며 해당 값은 Todo의 isDone값으로 구분된다.

 

const toggleTodo = (id) => {
    let targetIndex = todoList.findIndex((todo) => todo.id === id);
    let newTodo = [...todoList];
    if (newTodo[targetIndex].isDone) {
        newTodo[targetIndex] = { ...newTodo[targetIndex], isDone: false };
    } else {
        newTodo[targetIndex] = { ...newTodo[targetIndex], isDone: true };
    }
    setTodoList([...newTodo]);
};
const deleteTodo = (id) => {
    let newTodo = todoList.filter((todo) => todo.id !== id);
    setTodoList([...newTodo]);
};

 

toggleTodo는 id값을 인자로 받아 해당 id값을 가진 Todo의 index를 찾아내 해당 인덱스의 isDone만 반대로 바꿔주는 함수이다.

deleteTodo는 id를 인자로 받아 해당 id만 삭제하는 함수이다.

 

<div>
    <Title>Working... 🔥</Title>
    <div style={todoBox}>
        {todoList.map((todo) =>
            todo.isDone === false ? (
                <Todo todo={todo} toggleTodo={toggleTodo} deleteTodo={deleteTodo} key={todo.id} />
            ) : null
        )}
    </div>
    <Title>Done..! 🎉</Title>
    <div style={todoBox}>
        {todoList.map((todo) =>
            todo.isDone === true ? (
                <Todo todo={todo} toggleTodo={toggleTodo} deleteTodo={deleteTodo} key={todo.id} />
            ) : null
        )}
    </div>
</div>

todoList의 요소중 todo.isDone이 false인 것은 완료되지 않은 todo이므로 Working부분에 보이도록 하고, 반대는 Done부분에 보이도록 한다.

그리고 Todo컴포넌트에  todo, toggleTodo함수, deleteTodo함수, key값을 내려준다.

여기서 key값은 리액트에서 map을 사용할 때 넘겨주도록 권장하는 값이다.

리액트에서 map을 사용할 때 key값을 줘야하는 이유

 


import React from 'react';
import CustomButton from './CustomButton';

const Todo = ({ todo, toggleTodo, deleteTodo }) => {

    return (
        <div style={todoWrap}>
            <h2>{todo.title}</h2>
            <p>{todo.desc}</p>
            <div style={btnWrap}>
                <CustomButton customStyle={deleteStyle} onClick={() => deleteTodo(todo.id)}>
                    삭제하기
                </CustomButton>
                {todo.isDone ? (
                    <CustomButton customStyle={cancelStyle} onClick={() => toggleTodo(todo.id)}>
                        취소
                    </CustomButton>
                ) : (
                    <CustomButton customStyle={completeStyle} onClick={() => toggleTodo(todo.id)}>
                        완료
                    </CustomButton>
                )}
            </div>
        </div>
    );
};

export default Todo;

마지막으로 Todo컴포넌트이다. TodoList에서 받아온 todo값을 통해 todo.title과 todo.desc를 사용자에게 보여주며, deleteTodo, toggleTodo함수를 CustomButton에 넘겨주어 사용한다.

그리고 todo.isDone값에 따라 취소버튼이나 완료버튼이 보이도록 하였다.

 


쉬울것이라 생각했던 프로젝트였다. 그런데 어려웠다. 생각보다도 많이.

state를 어떻게 이동시킬 것인지, props의 이동이 어떻게 이루어지는지, 렌더링을 유발시키려면 state를 어디에 만들어서 어떻게 변화시켜야하는지, 컴포넌트를 어떻게 나눌것인지 등등

고민할게 많았던 코딩이었다. 과거엔 어떻게 딱딱 나눠 했는지 신기하기도 하다.

주말과 다음주는 tailwind를 새로 공부해보거나 styled-component를 다시 사용해볼 생각이다.