Next.js - Todolist 만들기

carrot·2021년 10월 25일
0

Nextjs

목록 보기
13/15
post-thumbnail

1. header 만들기

header는 모든 페이지에서 공통적으로 사용하게 됩니다. header 컴포넌트를 만든 후 App 컴포넌트에서 import 하여 모든 컴포넌트에서 공통적으로 사용되도록 하겠습니다.

> components/Header.tsx

import React from "react";
import styled from "styled-components";
import palette from "../styles/palette";

const Container = styled.div`
  display: flex;
  align-items: center;
  width: 100%;
  height: 52px;
  padding: 0 12px;
  border-bottom: 1px solid ${palette.gray};
  h1 {
    font-size: 21px;
  }
`;

const Header: React.FC = () => {
  return (
    <Container>
      <h1>devCarrot's TodoList</h1>
    </Container>
  );
};

export default Header;

todolist에서 사용할 색상들을 미리 palette라는 파일에 정의하여 사용하도록 하겠습니다. 색상을 정리하여 사용함으로써 동일한 색상을 사용하게 되어 앱의 통일감을 줄 수 있으며, 색상 값을 필요할 때마다 찾지 않아도 되어 개발에 도움이 됩니다.

> styles/palette.ts

export default {
  red: "#FFAFB0",
  orange: "#FFC282",
  yellow: "#FCFFB0",
  green: "#E2FFAF",
  blue: "#AEE4FF",
  navy: "#B4C7ED",
  gray: "#E5E5E5",
  deep_red: "#F35456",
  deep_green: "#47E774",
};

이제 Header를 공통으로 사용하기 위해 App 컴포넌트에서 import 하도록 하겠습니다.

> _app.tsx

import App, { AppContext, AppProps, AppInitialProps } from "next/app";
import GlobalStyle from "../styles/GlobalStyle";
import Header from "../components/Header";

const app = ({ Component, pageProps }: AppProps) => {
  return (
    <>
      <GlobalStyle />
      <Header />
      <Component {...pageProps} />
    </>
  );
};

export default app;

2. todolist 스타일링 하기

components 폴더에 TodoList.tsx를 만들어 스타일링을 적용하고 pages에서 import 하여 사용하도록 하겠습니다.

> components/TodoList.tsx

import React from "react";
import styled from "styled-components";
import palette from "../styles/palette";
import { TodoType } from "../types/todo";

const Container = styled.div`
  width: 100%;

  .todo-list-header {
    padding: 12px;
    border-bottom: 1px solid ${palette.gray};

    .todo-list-last-todo {
      font-size: 14px;
      span {
        margin-left: 8px;
      }
    }
  }
`;

interface IProps {
  todos: TodoType[];
}

// TodoList는 리액트 함수형 컴포넌트 타입이고 props로 IProps 타입의 프로퍼티를 전달 받습니다.
const TodoList: React.FC<IProps> = ({ todos }) => {
  return (
    <Container>
      <div className="todo-list-header">
        <p className="todo-list-last-todo">
          남은 TODO<span>{todos.length}</span>
        </p>
      </div>
    </Container>
  );
};

export default TodoList;

TodoList를 스타일링 하기에 앞서 임시 데이터를 만들도록 하겠습니다. todo item은 id, text, color, checked값을 가지게 되며 이를 type으로 만들어 사용하도록 하겠습니다.
type은 types 폴더를 만들어서 관련된 타입끼리 모아서 관리하고, todo.d.ts라는 파일에 작성하도록 하겠습니다. d.ts는 타입스크립트 코드 추론을 돕는 파일입니다.

> types/todo.d.ts

export type TodoType = {
  id: number;
  text: string;
  color: "red" | "orange" | "yellow" | "green" | "blue" | "navy";
  checked: boolean;
};
  • color: string;으로 해도 되지만 위와 같이 작성하면 코드 작성시 자동완성 기능을 사용할 수 있으며 이외의 값을 입력시 에러가 발생하여 코드 작성의 효율성을 높여줍니다.

type을 활용하여 todos 데이터를 만듭니다.
> pages/index.tsx

import React from "react";
import { NextPage } from "next";
import TodoList from "../components/TodoList";
import { TodoType } from "../types/todo";

const todos: TodoType[] = [
  { id: 1, text: "리액트 공부하기", color: "red", checked: true },
  { id: 2, text: "노드 공부하기", color: "orange", checked: true },
  { id: 3, text: "넥스트 공부하기", color: "yellow", checked: true },
  { id: 4, text: "타입스크립트 공부하기", color: "green", checked: true },
  { id: 5, text: "개인 프로젝트 환경설정", color: "navy", checked: true },
];

const index: NextPage = () => {
  return <TodoList todos={todos} />;
};

export default index;

2-1. 색상별 투두리스트 개수 구하기

todolist에서 지정된 color값에 따른 개수를 구하는 함수를 만들겠습니다.

const getTodoColorNums = () => {
    let red = 0;
    let orange = 0;
    let yellow = 0;
    let green = 0;
    let blue = 0;
    let navy = 0;
    todos.forEach((todo) => {
      switch (todo.color) {
        case "red":
          red += 1;
          break;
        case "orange":
          orange += 1;
          break;
        case "yellow":
          yellow += 1;
          break;
        case "green":
          green += 1;
          break;
        case "blue":
          blue += 1;
          break;
        case "navy":
          navy += 1;
          break;
        default:
          break;
      }
    });

    return {
      red,
      orange,
      yellow,
      green,
      blue,
      navy,
    };
  };

getTodoColorNums 함수는 TodoList.tsx 컴포넌트가 리렌더링 될 때마다 재계산이 됩니다. 재계산을 방지함으로써 성능 개선을 얻을 수 있는 useMemouseCallback hooks를 사용해 이를 개선해 보도록 하겠습니다.

> components/TodoList.tsx

import React, {useMemo, useCallback} from 'react';

const getTodoColorNums = useCallback(() => {
  ...
  return { red, orange, yellow, green, blue, navy };
}, [todos])

const todoColorNums = useMemo(getTodoColorNums, [todos]);
  • [todos]는 종속성을 나타냅니다. todos가 변경될 때만 함수와 변수를 재연산하게 되는 것을 의미합니다.
  • useMemouseCallback 또한 값의 변화를 비교하게 되며, 배열을 생성하여 사용하는 만큼 메모리를 사용하게 됩니다. 이러한 비용이 재연산하는 비용보다 클 수도 있습니다. 이를 염두에 두고 활용하면 되겠습니다.

getTodoColorNums함수를 개선하여 타입으로 정해지지 않은 색상의 숫자까지 얻을 수 있도록 코드를 변경하겠습니다.

type ObjectIndexType = {
    [key: string]: number | undefined;
  };

  const todoColorNums2 = useMemo(() => {
    const colors: ObjectIndexType = {};
    todos.forEach((todo) => {
      const value = colors[todo.color];
      if (!value) {
        colors[`${todo.color}`] = 1;
      } else {
        colors[`${todo.color}`] = value + 1;
      }
    });

    return colors;
  }, [todos]);

svg 컴포넌트 사용하기

svg 아이콘들을 사용하기 위한 설정이 필요합니다. 넥스트에서 제공하는 svg-components 예제를 참고하여 설정하도록 하겠습니다.

svg를 리액트 안에 컴포넌트로 사용하기 위해서 바벨 플러그인을 설치합니다.

$ yarn add babel-plugin-inline-react-svg -D

바벨 플러그인 설정을 추가합니다.
> .babelrc

{
  "presets": ["next/babel"],
  "plugins": [["styled-components", { "ssr": true }], "inline-react-svg"]
}

svg 모듈 타입을 지정하여 .svg 모듈을 찾지 못하는 에러를 사전에 방지합니다.
> types/image.d.ts

declare module "*.svg";

세부적인 디자인을 완성하여 적용합니다.
> components/TodoList.tsx

import React, { useMemo, useCallback } from "react";
import styled from "styled-components";
import palette from "../styles/palette";
import { TodoType } from "../types/todo";
import TrashCanIcon from "../public/statics/trash_can.svg";
import CheckMarkIcon from "../public/statics/check_mark.svg";

const Container = styled.div`
  width: 100%;

  .todo-num {
    margin-left: 12px;
  }

  .todo-list-header {
    padding: 12px;
    border-bottom: 1px solid ${palette.gray};

    .todo-list-last-todo {
      font-size: 14px;
      span {
        margin-left: 8px;
      }
    }

    .todo-list-header-colors {
      display: flex;
      .todo-list-header-color-num {
        display: flex;
        margin-right: 8px;
        p {
          font-size: 14px;
          line-height: 16px;
          margin: 0;
          margin-left: 6px;
        }
        .todo-list-header-round-color {
          width: 16px;
          height: 16px;
          border-radius: 50%;
        }
      }
    }
  }
  .bg-blue {
    background-color: ${palette.blue};
  }
  .bg-green {
    background-color: ${palette.green};
  }
  .bg-navy {
    background-color: ${palette.navy};
  }
  .bg-orange {
    background-color: ${palette.orange};
  }
  .bg-red {
    background-color: ${palette.red};
  }
  .bg-yellow {
    background-color: ${palette.yellow};
  }

  .todo-list {
    .todo-items {
      display: flex;
      justify-content: space-between;
      align-items: center;
      width: 100%;
      height: 52px;
      border-bottom: 1px solid ${palette.gray};

      .todo-left-side {
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;

        .todo-color-block {
          width: 12px;
          height: 100%;
        }
        .checked-todo-text {
          color: ${palette.gray};
          text-decoration: line-through;
        }
        .todo-text {
          margin-left: 12px;
          font-size: 16px;
        }
      }
    }
  }

  .todo-right-side {
    display: flex;
    margin-right: 12px;

    svg {
      &:first-child {
        margin-right: 16px;
      }
    }

    .todo-trash-can {
      width: 16px;
      path {
        fill: ${palette.deep_red};
      }
    }
    .todo-check-mark {
      fill: ${palette.deep_green};
    }

    .todo-button {
      width: 20px;
      height: 20px;
      border-radius: 50%;
      border: 1px solid ${palette.gray};
      background-color: transparent;
      outline: none;
    }
  }
`;

interface IProps {
  todos: TodoType[];
}

const TodoList: React.FC<IProps> = ({ todos }) => {
  const getTodoColorNums = useCallback(() => {
    let red = 0;
    let orange = 0;
    let yellow = 0;
    let green = 0;
    let blue = 0;
    let navy = 0;
    todos.forEach((todo) => {
      switch (todo.color) {
        case "red":
          red += 1;
          break;
        case "orange":
          orange += 1;
          break;
        case "yellow":
          yellow += 1;
          break;
        case "green":
          green += 1;
          break;
        case "blue":
          blue += 1;
          break;
        case "navy":
          navy += 1;
          break;
        default:
          break;
      }
    });

    return {
      red,
      orange,
      yellow,
      green,
      blue,
      navy,
    };
  }, [todos]);

  const todoColorNums = useMemo(getTodoColorNums, [todos]);
  console.log(todoColorNums);

  type ObjectIndexType = {
    [key: string]: number | undefined;
  };

  const todoColorNums2 = useMemo(() => {
    const colors: ObjectIndexType = {};
    todos.forEach((todo) => {
      const value = colors[todo.color];
      if (!value) {
        colors[`${todo.color}`] = 1;
      } else {
        colors[`${todo.color}`] = value + 1;
      }
    });

    return colors;
  }, [todos]);
  console.log(todoColorNums2);

  return (
    <Container>
      <div className="todo-list-header">
        <p className="todo-list-last-todo">
          남은 TODO<span>{todos.length}</span>
        </p>
        <div className="todo-list-header-colors">
          {Object.keys(todoColorNums2).map((color, index) => (
            <div className="todo-list-header-color-num" key={index}>
              <div className={`todo-list-header-round-color bg-${color}`} />
              <p>{todoColorNums[color]}</p>
            </div>
          ))}
        </div>
      </div>
      <ul className="todo-list">
        {todos.map((todo) => (
          <li className="todo-items" key={todo.id}>
            <div className="todo-left-side">
              <div className={`todo-color-block bg-${todo.color}`} />
              <p
                className={`todo-text ${
                  todo.checked ? "checked-todo-text" : ""
                }`}
              >
                {todo.text}
              </p>
            </div>
            <div className="todo-right-side">
              {todo.checked && (
                <>
                  <TrashCanIcon className="todo-trash-can" onClick={() => {}} />
                  <CheckMarkIcon
                    className="todo-check-mark"
                    onClick={() => {}}
                  />
                </>
              )}
              {!todo.checked && (
                <>
                  <button
                    type="button"
                    className="todo-button"
                    onClick={() => {}}
                  />
                </>
              )}
            </div>
          </li>
        ))}
      </ul>
    </Container>
  );
};

export default TodoList;
profile
당근같은사람

0개의 댓글