📄 프로젝트 생성 및 라이브러리 설치
$ create react-app todo-app
$ cd todo-app
$ yarn add node-sass classnames react-icons
node-sass
, classnames
, react-icons
📄 prettier 설정
앞에서 배웠던 Prettier를 설정해 코드 스타일을 정리하자. 프로젝트 최상위 디렉토리에 .prettierrc 파일을 다음과 같이 생성한다.
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}
📄 index.css 수정
프로젝트 글로벌 스타일 파일이 들어 있는 index.css를 수정한다.
body {
margin: 0;
padding: 0;
background: white;
}
📄 App 컴포넌트 초기화
import React from "react";
const App = () => {
return <div>Todo App을 만들자!</div>
}
export default App;
컴포넌트
src 디렉토리에 components 디렉토리를 생성하여 다음 4개의 파일을 저장한다.
- TodoTemplate
화면을 가운데로 정렬시켜 주며, 앱 타이틀을 보여준다.- TodoInsert
새로운 항목을 입력하고 추가할 수 있는 컴포넌트. state를 통해 input의 상태를 관리한다.- TodoListItem
각 할 일에 대한 정보를 보여주는 컴포넌트. todo 객체를 props로 받아와 상태에 따라 다른 스타일의 UI를 보여준다.- TodoList
todos 배열을 props로 받고, 이를 map을 사용해 여러 개의 TodoListItem 컴포넌트로 변환하여 보여준다.
🏷️TodoTemplate.js
import React from "react";
import './TodoTemplate.scss'
const TodoTemplate = ({children}) => {
const today = new Date() // js 기본 제공 함수
const time = {
year: today.getFullYear(), //현재 년도
month: today.getMonth() + 1, // 현재 월
date: today.getDate(), // 현재 날짜
day: today.getDay(), // 요일, 숫자로 출력됨
};
const week = ['일','월','화','수','목','금','토'] // 숫자를 요일로 바꿔주기 위함
return (
<div className="TodoTemplate">
<h1>{time.year}년 {time.month}월 {time.date}일</h1>
<div className="day">{week[time.day]}요일</div>
<div className="content">{children}</div>
</div>
)
}
export default TodoTemplate
🏷️TodoTemplate.scss
components 디렉토리에 TodoTemplate.scss 파일을 생성한다.
.TodoTemplate{
padding-top: 48px;
padding-left: 32px;
padding-right: 32px;
padding-bottom: 24px;
border-bottom: 1px solid #e9ecef;
h1 {
margin: 0;
font-size: 36px;
color: #343a40;
}
.day {
margin-top: 4px;
color: #868e96;
font-size: 21px;
}
.content{
background-color: white;
color: #20c997;
font-size: 18px;
margin-top: 40px;
font-weight: bold;
}
}
🏷️App.js
import React from "react";
import TodoTemplate from "./components/TodoTemplate";
const App = () => {
return <TodoTemplate>Todo App을 만들자!</TodoTemplate>
}
export default App;
🏷️TodoInsert.js
import React from 'react'
import {MdOutlineAdd} from 'react-icons/md'
import './TodoInsert.scss'
const TodoInsert = () => {
return (
<form className='TodoInsert'>
<input placeholder='할 일을 입력하세요.'/>
<button type='submit'><MdOutlineAdd/></button>
</form>
)
}
export default TodoInsert
🏷️TodoInsert.scss
components 디렉토리에 TodoTemplate.scss 파일을 생성한다.
.TodoInsert {
display: flex;
background: whitesmoke ;
input {
// 기본 스타일 초기화
background: none;
outline: none;
border: none;
padding: 0.5rem;
font-size: 1.125rem;
line-height: 1.5;
color: black;
&::placeholder{
color: #abb7d0
}
// 버튼을 제외한 영역 모두 차지
flex: 1;
}
button{
// 기본 스타일 초기화
background: none;
outline: none;
border: none;
background: #adb5db;
color: white;
padding-left: 1rem;
padding-right: 1rem;
font-size: 1.5rem;
display: flex;
align-items: center;
cursor: pointer;
transition: 0.1s background ease-in;
// 버튼에 마우스를 대면 색깔이 바뀐다.
&:hover{
background: #d2d8f6;
}
}
}
🏷️App.js
import React from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
const App = () => {
return <TodoTemplate><TodoInsert/></TodoTemplate>
}
export default App;
🏷️TodoListItem.js
import React from "react";
import {MdCheckBoxOutlineBlank, MdCheckBox, MdRemoveCircleOutline} from 'react-icons/md'
import './TodoListItem.scss'
const TodoListItem = () => {
return (
<div>
<div className="checkbox">
<MdCheckBoxOutlineBlank/>
<div className="text">할 일</div>
</div>
<div className="remove"><MdRemoveCircleOutline/></div>
</div>
)
}
export default TodoListItem
🏷️TodoList.js
import React from "react";
import TodoListItem from "./TodoListItem";
import './TodoList.scss'
const TodoList = () => {
return (
<div className="TodoList">
<TodoListItem/>
<TodoListItem/>
<TodoListItem/>
</div>
)
}
export default TodoList
🏷️TodoListItem.scss
components 디렉토리에 TodoListItem.scs 파일을 생성한다.
.TodoListItem {
padding: 1rem;
display: flex;
align-items: center; // 세로 중앙 정렬
&:nth-child(even){
background: #f8f9fa;
}
.checkbox {
cursor: pointer;
flex: 1; // 차지할 수 있는 영역 모두 차지
display: flex;
align-items: center; // 세로 중앙 정렬
svg {
// 아이콘
font-size: 1.5rem;
}
.text {
margin-left: 0.5rem;
flex: 1; // 차지할 수 있는 영역 모두 차지
}
// 체크 시 보여 줄 스타일
&.checked {
svg {
color: #22b8cf
}
.text {
color: #adb5bd;
text-decoration: line-through;
}
}
}
.remove {
display: flex;
align-items: center;
font-size: 1.5rem;
color: #ff6b6b;
cursor: pointer;
&:hover {
color: #ff8787
}
}
// 앨리먼트와 앨리먼트 사이 테두리 넣어줌
& + & {
border-top: 1px solid #dee2e6;
}
}
🏷️TodoList.scss
components 디렉토리에 TodoList.scss 파일을 생성한다.
.TodoList {
min-height: 320px;
max-height: 513px;
overflow-y: auto;
}
🏷️App.js
import React from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
const App = () => {
return (
<TodoTemplate>
<TodoInsert/>
<TodoList/>
</TodoTemplate>
)
}
export default App;
App에서 useState
를 사용해 todos라는 상태를 정의하고, todos를 TodoList의 props로 전달할 것이다. todos 배열 안에는 각항목의 " id, text, 완료 여부 값 "이 포함되어 있다. TodoList에서 이 값을 받아온 후 TodoItem으로 변환하여 렌더링할 것이다.
🏷️App.js
import React, { useState } from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: "리액트 기초 배우기",
checked: true
},
{
id: 2,
text: "방 청소하기",
checked: true
},
{
id: 3,
text: "이상협 때리기",
checked: false
}
])
return (
<TodoTemplate>
<TodoInsert/>
<TodoList todos={todos}/>
</TodoTemplate>
)
}
export default App;
🏷️TodoList.js
props로 받아온 todos 배열을 map을 통해 TodoListItem으로 이루어진 배열로 변환하여 렌더링해주었다. map을 사용해 컴포넌트로 변환할 때는 key props
를 전달해 주어야 한다.
import React from "react";
import TodoListItem from "./TodoListItem";
import './TodoList.scss'
const TodoList = ({todos}) => {
return (
<div className="TodoList">
{todos.map(todo => (
<TodoListItem todo={todo} key={todo.id}/>
))}
</div>
)
}
export default TodoList
🏷️TodoListItem.js
이 코드에서는 조건부 스타일링을 위해 classnames를 사용한다.
아래 코드에서<div className={cn('checkbox', { checked })}>
부분에 classnames가 사용되었다.
checked 값이true
→ className이 "checkbox checked "
checked 값이false
→ checked가 적용이 되지 않아 className이 "checkbox "
import React from "react";
import {MdCheckBoxOutlineBlank, MdCheckBox, MdRemoveCircleOutline} from 'react-icons/md'
import './TodoListItem.scss'
import cn from 'classnames'
const TodoListItem = ({todo}) => {
const {text, checked} = todo
return (
<div className="TodoListItem">
<div className={cn("checkbox",{checked})}>
{checked? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
<div className="text">{text}</div>
</div>
<div className="remove"><MdRemoveCircleOutline/></div>
</div>
)
}
export default TodoListItem
TodoInsert에서는 input 상태를 관리하고, App 컴포넌트에서는 todos 배열에 새로운 객체를 추가하는 함수를 만들어야 한다.
🏷️App.js
onInsert 함수에서 id 값을 useRef
를 사용해 관리한다. useState
가 아닌 useRef
를 사용하는 이유는 id 값은 렌더링 정보가 아닌 단순히 새로운 정보를 만들 때 참조되는 값이기 때문이다.
import React, { useCallback, useState, useRef } from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: "리액트 기초 배우기",
checked: true
},
{
id: 2,
text: "방 청소하기",
checked: true
},
{
id: 3,
text: "이상협 때리기",
checked: false
}
])
const nextId = useRef(4)
const onInsert = useCallback((text) => {
const todo = {
id: nextId.current,
text,
checked: false
}
setTodos([...todos,
todo])
nextId.current += 1
},[todos])
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert}/>
<TodoList todos={todos}/>
</TodoTemplate>
)
}
export default App;
🏷️TodoInsert.js
⭐ button 이벤트에 onClick을 사용해도 되지만 굳이 form과 onSubmit을 이용한 이유는, onSubmit 이벤트의 경우 input에서 엔터를 눌렀을 때도 이벤트가 발생한다.
import React, { useCallback, useState } from 'react'
import {MdOutlineAdd} from 'react-icons/md'
import './TodoInsert.scss'
const TodoInsert = ({onInsert}) => {
const [value, setValue] = useState('')
// 리렌더링 시마다 함수를 새로 만들지 않도록 useCallback 사용
const onChange = useCallback((e) => {
setValue(e.target.value)
},[])
const onSubmit = useCallback(
(e) => {
onInsert(value)
setValue('')
e.preventDefault() // 브라우저 새로고침 방지
}
,[value, onInsert])
return (
<form className='TodoInsert' onSubmit={onSubmit}>
<input placeholder='할 일을 입력하세요.'
value={value}
onChange={onChange}/>
<button type='submit' onSubmit={onSubmit}><MdOutlineAdd/></button>
</form>
)
}
export default TodoInsert
🏷️App.js
App 컴포넌트에서 id를 파라미터로 받아와서 같은 id를 가진 항목을 todos에서 지우는 onRemove 함수를 작성한다.
import React, { useCallback, useState, useRef } from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: "리액트 기초 배우기",
checked: true
},
{
id: 2,
text: "방 청소하기",
checked: true
},
{
id: 3,
text: "이상협 때리기",
checked: false
}
])
const nextId = useRef(4)
const onInsert = useCallback((text) => {
const todo = {
id: nextId.current,
text,
checked: false
}
setTodos([...todos,
todo])
nextId.current += 1
}, [todos])
const onRemove = useCallback((id) => {
setTodos(todos.filter(todo => todo.id !== id))
},[todos])
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert}/>
<TodoList todos={todos} onRemove={onRemove}/>
</TodoTemplate>
)
}
export default App;
🏷️TodoList.js
App 컴포넌트에서 만든 onRemove 함수를 TodoListItem 컴포넌트에서 사용하기 위해선 TodoList 컴포넌트를 거쳐야한다.
import React from "react";
import TodoListItem from "./TodoListItem";
import './TodoList.scss'
const TodoList = ({todos, onRemove}) => {
return (
<div className="TodoList">
{todos.map(todo => (
<TodoListItem todo={todo} key={todo.id} onRemove={onRemove}/>
))}
</div>
)
}
export default TodoList
🏷️TodoListItem.js
import React from "react";
import {MdCheckBoxOutlineBlank, MdCheckBox, MdRemoveCircleOutline} from 'react-icons/md'
import './TodoListItem.scss'
import cn from 'classnames'
const TodoListItem = ({todo, onRemove}) => {
const {id, text, checked} = todo
return (
<div className="TodoListItem">
<div className={cn("checkbox",{checked})}>
{checked? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
<div className="text">{text}</div>
</div>
<div className="remove" onClick={()=>onRemove(id)}><MdRemoveCircleOutline/></div>
</div>
)
}
export default TodoListItem
토글 기능은 위에서 만든 삭제 기능과 꽤 비슷하다. onToggle 함수를 App에서 만들고 해당 함수를 TodoList, TodoListItem에 props로 넘겨주면 된다.
🏷️App.js
import React, { useCallback, useState, useRef } from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: "리액트 기초 배우기",
checked: true
},
{
id: 2,
text: "방 청소하기",
checked: true
},
{
id: 3,
text: "이상협 때리기",
checked: false
}
])
const nextId = useRef(4)
const onInsert = useCallback((text) => {
const todo = {
id: nextId.current,
text,
checked: false
}
setTodos([...todos,
todo])
nextId.current += 1
}, [todos])
const onRemove = useCallback((id) => {
setTodos(todos.filter(todo => todo.id !== id))
},[todos])
const onToggle = useCallback((id)=>{
setTodos(
todos.map(todo =>
todo.id === id ? {...todo, checked: !todo.checked} : todo))
},[todos])
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert}/>
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
</TodoTemplate>
)
}
export default App;
🏷️TodoList.js
import React from "react";
import TodoListItem from "./TodoListItem";
import './TodoList.scss'
const TodoList = ({todos, onRemove, onToggle}) => {
return (
<div className="TodoList">
{todos.map(todo => (
<TodoListItem todo={todo} key={todo.id} onRemove={onRemove}
onToggle={onToggle}/>
))}
</div>
)
}
export default TodoList
🏷️TodoListItem.js
import React from "react";
import {MdCheckBoxOutlineBlank, MdCheckBox, MdRemoveCircleOutline} from 'react-icons/md'
import './TodoListItem.scss'
import cn from 'classnames'
const TodoListItem = ({todo, onRemove, onToggle}) => {
const {id, text, checked} = todo
return (
<div className="TodoListItem">
<div className={cn("checkbox",{checked})} onClick={()=>onToggle(id)}>
{checked? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
<div className="text">{text}</div>
</div>
<div className="remove" onClick={()=>onRemove(id)}><MdRemoveCircleOutline/></div>
</div>
)
}
export default TodoListItem
이번 프로젝트는 소규모이기 때문에 따로 컴포넌트 리렌더링 최적화 작업이 없어도 정상 작동한다. 😀