FrontEnd/기능구현

투두리스트 (TodoList) - 리액트(React)

3일마다 작심3일 2022. 3. 8. 14:39

투두리스트란?

말 그대로 투두리스트이다. 다만 우리가 개발 공부를 할 때 TodoList를  투두리스트는 개발의 기본인 CRUD를 전부 구현해야 하기 때문에 공부용 예제 및 연습으로 많이 사용한다.

 

우선 styled-components를 이용해서 UI부터 구현해보자.

 

TodoList.js

import React, { useState } from "react";
import styled from "styled-components";

const TodoListContainer = styled.div`
    display: flex;
    flex-direction: column;
    width: 30rem;
    margin: 10rem auto;
    height: 50rem;
    border: 1px solid black;

    h2 {
        display: flex;
        align-items: center;
        justify-content: center;
        background: #8edeb2;
        height: 10%;
        margin: 0;
    }

    .content {
        width: 100%;
        height: 65%;
        display: flex;
        align-items: center;

        ul {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0 2rem;
            overflow-x: scroll;

            
            li {
                display: flex;
                width: 100%;
                height: 10%;
                align-items: center;
                list-style: none;
                border-bottom: 1px solid black;

                span {
                    margin-left: 1rem;
                }
            }

            li + li {
                border-top: none;
            }
        }
    }
`

const Check = styled.div`
    cursor: pointer;
    width: 2rem;
    height: 2rem;
    background: white;
    border: 1px solid black;
    border-radius: 100%;
`

const Input = styled.input`
    height: 3rem;
    padding: 0 1rem;
    width: 90%;
    box-sizing: border-box;
    margin: 1rem 1rem;
    border-radius: 0.5rem;
`

const Footer = styled.footer`
    display: flex;
    width: 100%;
    justify-content: center;
    align-items: center;
    border-top: 1px solid black;
    height: 20%;
`

const TodoList = () => {
    const [ todoList, setTodoList ] = useState([]);
    

    return (
        <TodoListContainer>
            <h2>TodoList</h2>
            <Input />
            <div className="content">
                <ul>
                    <li><Check/> <span>something</span></li>
                    <li><Check/> <span>something</span></li>
                    <li><Check/> <span>something</span></li>
                    <li><Check/> <span>something</span></li>
                    <li><Check/> <span>something</span></li>
                    <li><Check/> <span>something</span></li>
                    <li><Check/> <span>something</span></li>
                    <li><Check/> <span>something</span></li>
                    <li><Check/> <span>something</span></li>
                    <li><Check/> <span>something</span></li>
                    <li><Check/> <span>something</span></li>
                </ul>
            </div>
            <Footer>footer</Footer>
        </TodoListContainer>
    )
}

export default TodoList

이렇게 UI를 만들어 놓고 이제 기능을 하나씩 추가해 봅시다!

 

우선 처음에는 데이터가 없으니 mock데이터를 만들어줍시다.

mock데이터는 필수로 미리 작성해야 하는건 아니지만 편의를 위해 미리 만들어두고 뿌려주는 용도입니다.

 

mock/todoMock.json

[
  {
    "id": 1,
    "content": "오늘 할 일",
    "isActive": true
  },
  {
    "id": 2,
    "content": "오늘 할 일",
    "isActive": true
  },
  {
    "id": 3,
    "content": "오늘 할 일",
    "isActive": true
  }
]

 

 

 

 

TodoList.js

import React, { useState } from "react";
import styled from "styled-components";
import todoMock from '../mock/todoMock.json'

const TodoListContainer = styled.div`
    display: flex;
    flex-direction: column;
    width: 30rem;
    margin: 10rem auto;
    height: 50rem;
    border: 1px solid black;

    h2 {
        display: flex;
        align-items: center;
        justify-content: center;
        background: #8edeb2;
        height: 10%;
        margin: 0;
    }

    .content {
        width: 100%;
        height: 65%;
        display: flex;
        align-items: center;

        ul {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0 2rem;
            overflow-x: scroll;

            
            li {
                display: flex;
                width: 100%;
                height: 10%;
                align-items: center;
                list-style: none;
                border-bottom: 1px solid black;

                span {
                    flex: 1;
                    text-align: start;
                    margin-left: 1rem;
                }
            }

            li + li {
                border-top: none;
            }
        }
    }
`

const Check = styled.div`
    cursor: pointer;
    width: 2rem;
    height: 2rem;
    background: ${({ isActive }) => isActive ? '#8edeb2' : 'white' };
    border: 1px solid black;
    border-radius: 100%;
`

const Input = styled.input`
    height: 3rem;
    padding: 0 1rem;
    width: 90%;
    box-sizing: border-box;
    margin: 1rem 1rem;
    border-radius: 0.5rem;
`

const Footer = styled.footer`
    display: flex;
    width: 100%;
    justify-content: center;
    align-items: center;
    border-top: 1px solid black;
    height: 20%;
`

const TodoList = () => {
    const [ todoList, setTodoList ] = useState(todoMock); // todoList의 상태를 저장할 useState선언 기본값으로는 mock데이터 를 기본값으로 설정해줍시다.
    const [ input, setInput ] = useState(''); // input의 상태를 저장할 useState선언

    const validate = () => { // input이 비어있다면 length 가 0이 됩니다, 0은 falsy값이므로 return 값으로 false입니다.
        return !!input.length
    }

    const handleChangeInput = (e) => { // event 객체의 e.target.value는 인풋에 입력된 값을 줍니다.
        setInput(e.target.value)
    }

    const addTodo = () => {
        setTodoList([ ...todoList, { //  스프레드 연산자를 이용해 이미 있는 배열을 복사해 펼쳐주고 새로 추가해주십다.
            id: todoList[todoList.length - 1].id + 1, // id값은 중복이 되면 안되기때문에 todoList의 맨마지막 id값을 가져와 + 1을 해줍니다. 배열은 순서가 있는 배열이기때문에 무조건 가장 큰 값이 됩니다.
            content: input, // 현재 input값을 넣어줍니다.
            isActive: false // 기본값은 false
        }])

        setInput(""); // todo를 추가하고 input을 다시 초기화해줍니다.
    }

    const handleEnterTodoInput = (e) => { // Enter를 누를때 addTodo가 실행됩니다.
        if(e.key === "Enter" && validate()) addTodo(); // validate함수의 값이 true라면 addTodo를 실행시켜 줍니다. 
    }

    const removeTodo = (id) => { // 매개변수로 받아온 id값을 이용해 배열함수 filter를 이용해 받아온 id값ㅇ을 지워줍니다..
        setTodoList(todoList.filter((todo) => todo.id !== id))
    }

    const updateTodoActive = (id) => { // removeTodo와 같이 id를 받아오고 isActive값을 기존상태의 반대값이 되기 해줍니다.
        setTodoList(todoList.map((todo) => {
            if(todo.id === id) {
                return {
                    ...todo,
                    isActive: !todo.isActive
                }
            } else {
                return todo
            }
        }))
    }
    

    return (
        <TodoListContainer>
            <h2>TodoList</h2>
            <Input onChange={handleChangeInput} onKeyPress={handleEnterTodoInput} value={input} />
            <div className="content">
                <ul>
                {todoList.map((todo) => (
                    <li key={todo.id}>
                        <Check onClick={() => updateTodoActive(todo.id)} isActive={todo.isActive} />
                        <span>{todo.content}</span>
                        <button onClick={() => removeTodo(todo.id)}>삭제</button>
                    </li>
                ))}
                </ul>
            </div>
            <Footer>footer</Footer>
        </TodoListContainer>
    )
}

export default TodoList

 

 

 

완성!

 

아래는 테스트코드 입니다.

 

describe("add Todo", () => {
  const mockTodo = require("../../mock/todoMock.json");

  it("should add comment", () => {
    const addTodo = () => {
      return [
        ...mockTodo,
        {
          id: mockTodo[mockTodo.length - 1].id + 1,
          content: "add Todo",
          isActive: false,
        },
      ];
    };

    const expected = [
      ...mockTodo,
      {
        id: 8,
        content: "add Todo",
        isActive: false,
      },
    ];

    expect(addTodo()).toEqual(expected);
  });

  it("should remove comment", () => {
    const removeTodo = (id) => {
      return mockTodo.filter((todo) => todo.id !== id);
    };

    const expected = mockTodo.filter((todo) => todo.id !== 3);

    expect(removeTodo(3)).toEqual(expected);
  });

  it("should update Active", () => {
    const updateTodoActive = (id) => {
      return mockTodo.map((todo) =>
        todo.id === id
          ? {
              ...todo,
              isActive: !todo.isActive,
            }
          : todo
      );
    };

    const expected = mockTodo.map((todo) =>
      todo.id === 3 ? { ...todo, isActive: false } : todo
    );

    expect(updateTodoActive(3)).toEqual(expected);
  });
});

다음 포스팅에는 이번에 만든 TodoList를 db.json을 이용해 API처럼 불러오고 컴포넌트 분리 (관심사의 분리)를 해서

가독성, 분리성을 높이는 작업을 하겠습니다.