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;
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;
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
컴포넌트가 리렌더링 될 때마다 재계산이 됩니다. 재계산을 방지함으로써 성능 개선을 얻을 수 있는 useMemo
와 useCallback
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가 변경될 때만 함수와 변수를 재연산하게 되는 것을 의미합니다.useMemo
와 useCallback
또한 값의 변화를 비교하게 되며, 배열을 생성하여 사용하는 만큼 메모리를 사용하게 됩니다. 이러한 비용이 재연산하는 비용보다 클 수도 있습니다. 이를 염두에 두고 활용하면 되겠습니다.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-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;