본문 바로가기

오픈소스/노드

[Node] React - TodoList 만들기

 리액트로 TODO List 를 만들어보도록 하겠습니다. TODO List의 프로젝트를 통해 좀 더 styled-components와 Hooks를 사용하는 방법을 배워보고 익숙하게 개발하기 위함입니다. TODO List 프로젝트를 만들 때에는 크게 3가지의 분류로 나뉘게 됩니다.

 

 먼저 컴포넌트를 구성하는 UI를 만들어야 하는 단계가 있고, Context API를 통해 reducer로 만든 데이터나 함수들을 각 컴포넌트에 전송해야 합니다. 전송한 데이터나 함수를 각 컴포넌트에 넣어줘서 기능을 수행할 수 있게 만드는 단계로 나누어 개발하도록 하겠습니다.

 

 천천히 프로젝트 내용을 통해 구성하고 연결하여 좋은 개발하시기 바랍니다. 그럼 지금부터 시작하도록 하겠습니다.

 

# Contents


  • 컴포넌트 만들기
  • Context API를 활용한 상태 관리
  • 기능 구현하기

 

# 컴포넌트 만들기


 새로운 프로젝트를 생성하고 라이브러리를 설치하겠습니다.

 

아래 명령어를 통해 프로젝트와 라이브러리를 설치합니다.

 

npx create-creat-app todolist
cd styled-components-test
npm install --save styled-components

 

 먼저 만들어야 하는 컴포넌트를 확인하도록 하겠습니다.

 컴포넌트의 종류는 총 5가지입니다. 아래 리스트들을 확인하시고 어떤 역할을 하는 지 파악해봅시다.

 

  • TodoTemplate : 레이아웃을 설정하는 컴포넌트입니다. 페이지의 중앙에 그림자가 적용된 흰색 박스를 보여줍니다.
  • TodoHead : 이 컴포넌트는 오늘의 날짜와 요일을 보여주고, 앞으로 해야 할 일이 몇개 남았는지 보여줍니다.
  • TodoList : 정보가 들어있는 todos 배열을 내장함수 map 을 사용하여 여러개의 TodoItem 컴포넌트를 렌더링해줍니다.
  • TodoItem : 각 할 일에 대한 정보를 렌더링해주는 컴포넌트입니다. 
  • TodoCreate : 새로운 할 일을 등록할 수 있게 해주는 컴포넌트입니다. 

 

1. createGlobalStyle

 일단 Body 태그의 색상부터 변경하도록 하겠습니다.  createGlobalStyle 을 통해 Body 태그 안에 css를 변경하실 수 있습니다.

 

 다음은 createGlobalStyle을 사용하여  Body태그의 css를 변경한 App.js 코드입니다.

 

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

const GlobalStyle = createGlobalStyle`
  body {
    background: #e9ecef;
  }
`;

function App() {
    return (
        <>
            <GlobalStyle />
            <h2>안녕하세요</h2>
        </>
    );
}

export default App;

 

2. TodoTemplate 만들기

 TodoTemplate 컴포넌트를 만들어서 중앙에 정렬된 흰색 박스를 보여줘봅시다. src 디렉터리에 components 디렉터리를 만들고, 그 안에 TodoTemplate.js 를 만드세요. 앞으로 만들 컴포넌트들은 모두 components 디렉터리에 만들도록 하겠습니다.

 

 다음은 TodoTemplate.js 파일을 생성한 후 만든 코드입니다.

 

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

const TodoTemplateBox = styled.div`
    width: 512px;
    height: 728px;
    margin: 0 auto;
    margin-top: 150px;
    background-color: white;
    border-radius: 16px;
    box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.04);

    display: flex;
    flex-direction: column;
`;

function TodoTemplate({ children, ...rest }) {
    return (
        <>
            <TodoTemplateBox {...rest}>{children}</TodoTemplateBox>
        </>
    );
}

export default TodoTemplate;

 

코드가 작성이 완료되었으면 App.js 컴포넌트도 변경해주도록 합시다.

 

 다음은 App.js 코드입니다.

 

import React from "react";
import styled, { createGlobalStyle } from "styled-components";
import TodoTemplate from "./components/TodoTemplate";

const GlobalStyle = createGlobalStyle`
  body {
    background: #e9ecef;
  }
`;

function App() {
    return (
        <>
            <GlobalStyle />
            <TodoTemplate>안녕하세요</TodoTemplate>
        </>
    );
}

export default App;

 

3. TodoHead 만들기

이 컴포넌트에서는 오늘의 날짜, 요일, 그리고 남은 할 일 개수를 보여줍니다.

 

 다음은 TodoHead.js 파일을 생성한 후 만든 코드입니다.

 

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

const TodoHeadBox = styled.div`
    display: flex;
    justify-content: flex-start;
    flex-direction: column;
    padding: 30px;
    border-bottom: 1px solid #868e96;

    h1 {
        margin: 0;
        font-size: 36px;
        color: #343a40;
    }

    .day {
        margin-top: 4px;
        color: #868e96;
        font-size: 21px;
    }

    .tasks-left {
        color: #20c997;
        font-size: 18px;
        margin-top: 40px;
        font-weight: bold;
    }
`;

function TodoHead({ children, ...rest }) {
    return (
        <>
            <TodoHeadBox {...rest}>
                <h1>2019년 7월 10일</h1>
                <div className="day">수요일</div>
                <div className="tasks-left">할 일 2개 남음</div>
            </TodoHeadBox>
        </>
    );
}

export default TodoHead;

 

코드가 작성이 완료되었으면 App.js 컴포넌트도 변경해주도록 합시다.

 

 다음은 App.js 코드입니다.

 

import React from "react";
import styled, { createGlobalStyle } from "styled-components";
import TodoHead from "./components/TodoHead";
import TodoTemplate from "./components/TodoTemplate";

const GlobalStyle = createGlobalStyle`
  body {
    background: #e9ecef;
  }
`;

function App() {
    return (
        <>
            <GlobalStyle />
            <TodoTemplate>
                <TodoHead></TodoHead>
            </TodoTemplate>
        </>
    );
}

export default App;

 

4. TodoList 만들기

이번에는 여러개의 할 일 항목을 보여주게 될 TodoList 를 만들어보겠습니다.

 

 다음은 TodoList.js 파일을 생성한 후 만든 코드입니다.

 

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

const TodoListBox = styled.div`
    width: 100%;
    height: 100%;

    padding: 20px;
    box-sizing: border-box;
    background-color: aliceblue;
`;

function TodoList({ children, ...rest }) {
    return (
        <>
            <TodoListBox {...rest}>{children}</TodoListBox>
        </>
    );
}

export default TodoList;

 

코드가 작성이 완료되었으면 App.js 컴포넌트도 변경해주도록 합시다.

 

 다음은 App.js 코드입니다.

 

import React from "react";
import styled, { createGlobalStyle } from "styled-components";
import TodoHead from "./components/TodoHead";
import TodoList from "./components/TodoList";
import TodoTemplate from "./components/TodoTemplate";

const GlobalStyle = createGlobalStyle`
  body {
    background: #e9ecef;
  }
`;

function App() {
    return (
        <>
            <GlobalStyle />
            <TodoTemplate>
                <TodoHead></TodoHead>
                <TodoList>TodoList</TodoList>
            </TodoTemplate>
        </>
    );
}

export default App;

 

5. TodoItem 만들기

이번 컴포넌트에서는 각 할 일 항목들을 보여주는 TodoItem 컴포넌트를 만들어보겠습니다. 이 컴포넌트에서는 react-icons 에서 MdDone 과 MdDelete 아이콘을 사용합니다.

 

 다음은 TodoItem.js 파일을 생성한 후 만든 코드입니다.

 

import React from "react";
import styled, { css } from "styled-components";
import { MdDone, MdDelete } from "react-icons/md";

const Remove = styled.div`
    display: flex;
    align-items: center;
    justify-content: center;
    color: #dee2e6;
    font-size: 24px;
    cursor: pointer;
    &:hover {
        color: #ff6b6b;
    }
    display: none;
`;

const TodoItemBox = styled.div`
    display: flex;
    align-items: center;
    padding-top: 12px;
    padding-bottom: 12px;
    &:hover {
        ${Remove} {
            display: block;
        }
    }
`;

const CircleItem = styled.div`
    width: 30px;
    height: 30px;
    border-radius: 50%;
    border: 1px solid tomato;
    display: flex;
    align-items: center;
    margin-right: 20px;
    ${(props) => {
        if (props.done)
            return css`
                color: darkgray;
                border-color: darkgray;
            `;
    }}
`;

const TextItem = styled.div`
    width: 100%;
    font-size: 21px;
    ${(props) => {
        if (props.done)
            return css`
                color: #ced4da;
            `;
    }}

    margin-right: 20px;
`;

function TodoItem({ done, text }) {
    return (
        <>
            <TodoItemBox>
                <CircleItem done={done}>{done && <MdDone className="md" />}</CircleItem>
                <TextItem done={done}>{text}</TextItem>
                <Remove>
                    <MdDelete></MdDelete>
                </Remove>
            </TodoItemBox>
        </>
    );
}

export default TodoItem;

 

코드가 작성이 완료되었으면 TodoList.js 컴포넌트도 변경해주도록 합시다.

 

 다음은 TodoList.js 코드입니다.

 

import React from "react";
import styled, { css } from "styled-components";
import TodoItem from "./TodoItem";

const TodoListBox = styled.div`
    width: 100%;
    height: 100%;

    padding: 20px;
    box-sizing: border-box;
    background-color: aliceblue;

    padding-bottom: 110px;
`;

function TodoList({ children, ...rest }) {
    return (
        <>
            <TodoListBox {...rest}>
                <TodoItem text={"프로젝트 생성하기"} done={true}></TodoItem>
                <TodoItem text={"프로젝트 생성하기"} done={true}></TodoItem>
                <TodoItem text={"프로젝트 생성하기"} done={false}></TodoItem>
            </TodoListBox>
        </>
    );
}

export default TodoList;

 

 

# Context API 를 활용한 상태 관리


 먼저 Context API를 생성하기 위해 src밑에 TodoContext.js 파일을 생성하고 다음의 코드를 사용하여 Provider 기능을 하위 컴포넌트에게 제공할 것입니다. 

 

 다음은 TodoContext.js의 코드입니다.

 

import React, { useContext, useReducer, useRef } from "react";

const initialTodos = [
    {
        id: 1,
        text: "프로젝트 생성하기",
        done: true,
    },
    {
        id: 2,
        text: "컴포넌트 스타일링하기",
        done: true,
    },
    {
        id: 3,
        text: "Context 만들기",
        done: false,
    },
    {
        id: 4,
        text: "기능 구현하기",
        done: false,
    },
];

const TodoStateContext = React.createContext();
const TodoDispatchContext = React.createContext();
const TodoNextId = React.createContext();

function reducer(state, action) {
    switch (action.type) {
        case "CREATE":
            return state.concat(action.todo);
        case "REMOVE":
            return state.filter((todo) => todo.id !== action.id);
        default:
    }
}

function TodoProvider({ children }) {
    const [state, action] = useReducer(reducer, initialTodos);
    const nextId = useRef(5);

    return (
        <TodoStateContext.Provider value={state}>
            <TodoDispatchContext.Provider value={action}>
                <TodoNextId.Provider value={nextId}>
                    <>{children}</>
                </TodoNextId.Provider>
            </TodoDispatchContext.Provider>
        </TodoStateContext.Provider>
    );
}

export function useTodoContext() {
    return useContext(TodoStateContext);
}

export function useTodoDispatch() {
    return useContext(TodoDispatchContext);
}

export function useTodoNextId() {
    return useContext(TodoNextId);
}

export default TodoProvider;

 

만든 프로젝트를 하위 컴포넌트에 넣도록 하겠습니다.

 

 다음은 App.js 코드입니다.

 

import React from "react";
import styled, { createGlobalStyle } from "styled-components";
import TodoCreate from "./components/TodoCreate";
import TodoHead from "./components/TodoHead";
import TodoList from "./components/TodoList";
import TodoTemplate from "./components/TodoTemplate";
import TodoProvider from "./TodoContext";

const GlobalStyle = createGlobalStyle`
  body {
    background: #e9ecef;
  }
`;

function App() {
    return (
        <>
            <GlobalStyle />
            <TodoProvider>
                <TodoTemplate>
                    <TodoHead></TodoHead>
                    <TodoList></TodoList>
                    <TodoCreate></TodoCreate>
                </TodoTemplate>
            </TodoProvider>
        </>
    );
}

export default App;

 

 

# 기능 구현하기


Context 를 만들어주었으니, 이제 Context 와 연동을 하여 기능을 구현해봅시다. Context 에 있는 state 를 받아와서 렌더링을 하고, 필요한 상황에는 특정 액션을 dispatch 하면 됩니다. 먼저 TodoHead 에 있는 코드를 완성해보도록 하겠습니다.

 

1. TodoHead 완성하기

이 컴포넌트에서는 오늘의 날짜, 요일, 그리고 남은 할 일 개수를 보여줍니다.

 

 다음은 TodoHeader.js의 코드입니다.

 

import React from "react";
import styled, { css } from "styled-components";
import { useTodoContext } from "../TodoContext";

const TodoHeadBox = styled.div`
    display: flex;
    justify-content: flex-start;
    flex-direction: column;
    padding: 30px;
    border-bottom: 1px solid #868e96;

    h1 {
        margin: 0;
        font-size: 36px;
        color: #343a40;
    }

    .day {
        margin-top: 4px;
        color: #868e96;
        font-size: 21px;
    }

    .tasks-left {
        color: #20c997;
        font-size: 18px;
        margin-top: 40px;
        font-weight: bold;
    }
`;

function TodoHead({ children, ...rest }) {
    const state = useTodoContext();
    const todoState = state.filter((todo) => {
        return todo.done !== false;
    });
    return (
        <>
            <TodoHeadBox {...rest}>
                <h1>2019년 7월 10일</h1>
                <div className="day">수요일</div>
                <div className="tasks-left">할 일 {todoState.length}개 남음</div>
            </TodoHeadBox>
        </>
    );
}

export default TodoHead;

 

 

만든 프로젝트를 하위 컴포넌트에 넣도록 하겠습니다.

 

 다음은 App.js 코드입니다.

 

import React from "react";
import styled, { createGlobalStyle } from "styled-components";
import TodoCreate from "./components/TodoCreate";
import TodoHead from "./components/TodoHead";
import TodoList from "./components/TodoList";
import TodoTemplate from "./components/TodoTemplate";
import TodoProvider from "./TodoContext";

const GlobalStyle = createGlobalStyle`
  body {
    background: #e9ecef;
  }
`;

function App() {
    return (
        <>
            <GlobalStyle />
            <TodoProvider>
                <TodoTemplate>
                    <TodoHead></TodoHead>
                    <TodoList></TodoList>
                    <TodoCreate></TodoCreate>
                </TodoTemplate>
            </TodoProvider>
        </>
    );
}

export default App;

 

2. TodoList 완성하기

 

 다음은 TodoList의 코드입니다.

 

import React from "react";
import styled, { css } from "styled-components";
import { useTodoContext } from "../TodoContext";
import TodoItem from "./TodoItem";

const TodoListBox = styled.div`
    width: 100%;
    height: 100%;

    padding: 20px;
    box-sizing: border-box;
    background-color: aliceblue;

    padding-bottom: 110px;
`;

function TodoList({ children, ...rest }) {
    const state = useTodoContext();

    return (
        <>
            <TodoListBox {...rest}>
                {state.map((todo) => {
                    return <TodoItem id={todo.id} text={todo.text} done={todo.done}></TodoItem>;
                })}
            </TodoListBox>
        </>
    );
}

export default TodoList;

 

3. TodoItem 완성하기

 

 다음은 TodoList의 코드입니다.

 

import React, { useCallback } from "react";
import styled, { css } from "styled-components";
import { MdDone, MdDelete } from "react-icons/md";
import { useTodoDispatch } from "../TodoContext";

const Remove = styled.div`
    display: flex;
    align-items: center;
    justify-content: center;
    color: #dee2e6;
    font-size: 24px;
    cursor: pointer;
    &:hover {
        color: #ff6b6b;
    }
    display: none;
`;

const TodoItemBox = styled.div`
    display: flex;
    align-items: center;
    padding-top: 12px;
    padding-bottom: 12px;
    &:hover {
        ${Remove} {
            display: block;
        }
    }
`;

const CircleItem = styled.div`
    width: 30px;
    height: 30px;
    border-radius: 50%;
    border: 1px solid tomato;
    display: flex;
    align-items: center;
    margin-right: 20px;
    ${(props) => {
        if (props.done)
            return css`
                color: darkgray;
                border-color: darkgray;
            `;
    }}
`;

const TextItem = styled.div`
    width: 100%;
    font-size: 21px;
    ${(props) => {
        if (props.done)
            return css`
                color: #ced4da;
            `;
    }}

    margin-right: 20px;
`;

function TodoItem({ id, done, text }) {
    const dispatch = useTodoDispatch();
    const onToggle = useCallback(() => {
        dispatch({
            type: "TOGGLE",
            id,
        });
    });
    const onRemove = () => dispatch({ type: "REMOVE", id });

    return (
        <>
            <TodoItemBox>
                <CircleItem id={id} done={done} onClick={onToggle}>
                    {done && <MdDone className="md" />}
                </CircleItem>
                <TextItem done={done}>{text}</TextItem>
                <Remove>
                    <MdDelete onClick={onRemove}></MdDelete>
                </Remove>
            </TodoItemBox>
        </>
    );
}

export default TodoItem;

 

4. TodoCreate 완성하기

 

 다음은 TodoCreate의 코드입니다.

 

import React, { useCallback, useState } from "react";
import styled, { css, keyframes } from "styled-components";
import { MdAdd } from "react-icons/md";
import { useTodoDispatch, useTodoNextId } from "../TodoContext";

const Rotate = keyframes`
  from {
    transform: rotate( 0deg );
  }
  to {
    transform: rotate( 720deg );
  }
`;

const InsertFormPosition = styled.div`
    height: 100px;
    width: 100%;

    position: absolute;
    bottom: 0;
    left: 0;
    display: flex;
    justify-content: center;
    align-items: center;

    ${(props) => {
        if (props.visible)
            return css`
                background-color: gray;
            `;
    }}
`;

const InsertForm = styled.form`
    width: 100%;
    padding: 5px 30px;
    box-sizing: border-box;

    ${(props) => {
        if (props.visible) {
            return css`
                display: block;
            `;
        } else {
            return css`
                display: none;
            `;
        }
    }}
`;

const Input = styled.input`
    padding: 12px;
    border-radius: 4px;
    border: 1px solid #dee2e6;
    width: 100%;
    outline: none;
    font-size: 18px;
    box-sizing: border-box;
`;

const IconPlace = styled.div`
    position: absolute;
    bottom: 0;
    width: 50px;
    height: 50px;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: aliceblue;

    margin-bottom: -25px;
    font-size: 50px;
    border-radius: 50%;
    cursor: pointer;

    &:active {
        animation-duration: 0.25s;
        animation-timing-function: ease-out;
        animation-name: ${Rotate};
        animation-fill-mode: forwards;
    }

    ${(props) => {
        if (props.visible) {
            return css`
                background-color: red;
                &:hover {
                    background-color: tomato;
                    &:active {
                        background-color: brown;
                    }
                }
            `;
        } else {
            return css`
                background-color: aqua;
                &:hover {
                    background-color: antiquewhite;
                    &:active {
                        background-color: aquamarine;
                    }
                }
            `;
        }
    }}
`;

function TodoCreate({ done, text }) {
    const defaultInitial = false;
    const [state, action] = useState(defaultInitial);
    const defaultinitValue = "";
    const [value, setValue] = useState(defaultinitValue);
    const dispatch = useTodoDispatch();
    const nextId = useTodoNextId();

    const callbackActions = useCallback(() => {
        console.log(state);
        return action(!state);
    }, [state]);

    const onChange = (e) => {
        e.preventDefault();
        var text = e.target.value;
        return setValue(text);
    };
    const onSubmit = (e) => {
        e.preventDefault();
        dispatch({
            type: "CREATE",
            todo: {
                id: nextId.current++,
                text: value,
                done: false,
            },
        });
        setValue("");
        action(false);
    };

    return (
        <>
            <InsertFormPosition visible={state}>
                <InsertForm visible={state} onSubmit={onSubmit}>
                    <Input placeholder="할 일을 입력한 후 Enter를 누르세요." onChange={onChange} value={value} />
                </InsertForm>
                <IconPlace visible={state}>
                    <MdAdd onClick={callbackActions} />
                </IconPlace>
            </InsertFormPosition>
        </>
    );
}

export default TodoCreate;