할 일 관리 앱 만들기
0. 프로젝트 생성하기
todo-Todo 디렉토리를 생성 후 프로젝트를 생성합니다.
npx create-next-app@latest ./
https://ko.react.dev/learn/start-a-new-react-project
1. 컴포넌트 구조 설계하기
2. 컴포넌트 제작
- Header 컴포넌트 만들기
// src/components/TodoHd.jsx
import React from 'react';
const TodoHd = () => {
return (
<div>
<h1>📝 할 일 관리 앱</h1>
<p>2024.03.11 오늘의 할 일을 적어보세요.</p>
</div>
);
};
export default TodoHd;
- TodoEditor 컴포넌트 만들기
// src/components/TodoEditor.jsx
import React from 'react';
const TodoEditor = () => {
return (
<div>
<h2>새로운 Todo 작성하기 ✏ </h2>
<div>
<input placeholder="할 일을 추가로 입력해주세요." />
<button>추가</button>
</div>
</div>
);
};
export default TodoEditor;
- TodoList 컴포넌트 만들기
TodoItem 컴포넌트를 TodoList 컴포넌트에서 렌더링을 연습합니다.
// src/components/TodoList.jsx
import React from 'react';
const TodoList = () => {
return (
<div>
<h2>할 일 목록 📃</h2>
<input placeholder="검색어를 입력하세요" />
<ul>
<TodoItem isDone={true} task="고양이 밥주기" createdDate="2024.03.11" />
<TodoItem isDone={false} task="감자 캐기" createdDate="2024.03.11" />
<TodoItem isDone={false} task="고양이 놀아주기" createdDate="2024.03.11" />
</ul>
</div>
);
};
export default TodoList;
- TodoItem 컴포넌트 만들기
// src/components/TodoItem.jsx
const TodoItem = () => {
return (
<div>
<li>
<input type="checkbox" />
<span>고양이 밥주기</span>
<span>2024.03.11</span>
<button>삭제</button>
</li>
</div>
);
};
export default TodoItem;
- 컴포넌트 구조 설계하기
// src/components/Todo.jsx
import TodoHd from '@/components/TodoHd';
import TodoEditor from '@/components/TodoEditor';
import TodoList from '@/components/TodoList';
const Todo = () => {
return (
<div className="flex flex-col gap-5">
<TodoHd />
<TodoEditor />
<TodoList />
</div>
);
}
export default Todo;
3. 기능 구현
- Todo 컴포넌트 : 할 일 데이터 관리
- Header 컴포넌트 : 오늘 날짜 표시
- TodoEditor 컴포넌트 : 할 일 추가
- TodoList 컴포넌트 : 검색에 따라 필터링된 할 일 목록 표시
- TodoItem 컴포넌트 : 할 일 목록의 수정 및 삭제
데이터를 다루는 4가지 기능 (CRUD)을 구현합니다.
추가(Create), 조회(Read), 수정(Update), 삭제(Delete)
- 오늘 날짜 표시하기
날짜를 표시하는 라이브러리로 date-fns를 사용합니다.
npm install date-fns
yarn add date-fns
data-fns를 사용하여 오늘 날짜를 표시하는 코드를 작성합니다.
문법 : format(new Date(), 'yyyy.MM.dd')
// src/components/TodoHd.jsx
import React from 'react';
import { format } from 'date-fns';
const TodoHd = () => {
return (
<div>
<h1>📝 할 일 관리 앱</h1>
<strong>{format(new Date(), 'yyyy.MM.dd')}</strong>
<p>오늘의 할 일을 적어보세요.</p>
</div>
);
};
export default TodoHd;
- 기초 데이터 및 타입 설정하기
// src/data/todoData.ts
export const mockTodoData = [
{
id: 1,
isDone: false,
task: '고양이 밥주기',
createdDate: new Date().getTime(), // 현재 시간
},
{
id: 2,
isDone: false,
task: '감자 캐기',
createdDate: new Date().getTime(),
},
{
id: 3,
isDone: false,
task: '고양이 놀아주기',
createdDate: new Date().getTime(),
},
];
// src/data/index.ts
export * from './todoData';
Read: 할 일 목록 렌더링하기
- 로직
- Todo 컴포넌트 : 할 일 데이터 관리
- TodoList 컴포넌트 : 할 일 목록 렌더링
컴포넌트의 재사용성을 높이기 위해 데이터를 Todo 컴포넌트에서 관리하고, TodoList 컴포넌트에서 할 일 목록을 렌더링합니다.
- 할 일 목록 렌더링하기
// src/components/Todo.jsx
import { useState } from "react";
import TodoHd from "@/components/TodoHd";
import TodoEditor from "@/components/TodoEditor";
import TodoList from "@/components/TodoList";
import { mockTodoData } from "@/data";
const Todo = () => {
const [todos, setTodos] = useState(mockTodoData);
return (
<div className="flex flex-col gap-5">
<TodoHd />
<TodoEditor />
<TodoList todos={todos} />
</div>
);
};
export default Todo;
// src/components/TodoList.jsx
import React from 'react';
import TodoItem from './TodoItem';
const TodoList = ({ todos }) => {
return (
<div>
<h2>할 일 목록 📃</h2>
<input placeholder="검색어를 입력하세요"/>
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} {...todo} />
))}
</ul>
</div>
);
};
export default TodoList;
// src/components/TodoItem.jsx
import React from 'react';
const TodoItem = ({ id, isDone, task, createdDate }) => {
console.log(`TodoItem ${task}: isDone = ${isDone}`); // 추가
return (
<div>
<li key={id}>
<input
type="checkbox"
checked={isDone}
onChange={() => {}} // 나중에 구현
/>
<span>{task}</span>
<span>{new Date(createdDate).toLocaleDateString()}</span>
<button>삭제</button>
</li>
</div>
);
};
export default TodoItem;
Create: 할 일 추가하기
- 로직
- 사용자 : 할 일 입력, 추가 버튼 클릭
- TodoEditor : 입력한 할 일을 받아와서 할 일 목록을 관리하는 상태인 todo에 추가
- TodoList : 추가된 할 일 목록을 전달하여 화면에 표시
- Todo : 추가된 할 일 목록을 브라우저의 로컬 스토리지에 저장
- TodoEditor
- 빈 입력 방지
- input 초기화 및 포커스
- Enter 키로 할 일 추가
- 소문자 검색
- 할 일 추가하기
// src/components/Todo.jsx
import { useState } from "react";
import TodoHd from "@/components/TodoHd";
import TodoEditor from "@/components/TodoEditor";
import TodoList from "@/components/TodoList";
import { mockTodoData } from "@/data";
import type { TodoType } from "@/types/todo";
const Todo = () => {
const [todos, setTodos] = useState(mockTodoData);
// 할 일을 추가하는 함수를 만듭니다.
const addTodo = (task) => {
const newTodo = {
id: todos.length + 1, // ID는 기존 할 일 개수 + 1
task,
isDone: false, // 완료 상태 기본값은 false
createdDate: new Date().getTime(), // 생성 시간 기록
};
setTodos([newTodo, ...todos]); // 새로운 할 일을 기존 목록 앞에 추가
};
return (
<div className="flex flex-col gap-5">
<TodoHd />
<TodoEditor addTodo={addTodo} />
<TodoList todos={todos} />
</div>
);
};
export default Todo;
- 할 일 추가 함수 호출하기
// src/components/TodoEditor.jsx
import React, { useRef, useState } from 'react';
const TodoEditor = ({ addTodo }) => {
// 할 일을 입력하는 input 상태를 관리합니다.
const [task, setTask] = useState('');
// input에 할 일이 입력되면 입력한 값을 task 상태에 업데이트하는 함수를 만듭니다.
const onChangeTask = e => setTask(e.target.value);
// 추가 버튼을 클릭하면 할 일을 추가하는 함수를 호출합니다.
const onSubmit = () => {
// 할 일을 추가하는 함수를 호출합니다.
addTodo(task);
};
return (
<div>
<h2>새로운 Todo 작성하기 ✏ </h2>
<div>
{/* task 상태값을 value로 설정 */}
{/* onChange 이벤트에 onChangeTask 함수를 연결 */}
<input
value={task}
onChange={onChangeTask}
placeholder="할 일을 추가로 입력해주세요."
/>
{/* 추가 버튼을 클릭하면 onSubmit 함수를 호출 */}
<button onClick={onSubmit}>추가</button>
</div>
</div>
);
};
export default TodoEditor;
- input 초기화 후 포커스 맞추기
// src/components/TodoEditor.jsx
import React, { useRef, useState } from 'react';
const TodoEditor = ({ addTodo }) => {
const [task, setTask] = useState('');
// inputRef 변수가 useRef()로 생성됩니다.
// 연결된 input 요소에 포커스를 맞추기 위해 사용합니다.
const inputRef = useRef();
const onChangeTask = e => setTask(e.target.value);
const onSubmit = () => {
if (!task) return
addTodo(task);
// 할 일을 추가한 후 input을 초기화합니다.
setTask('')
// input에 포커스를 맞춥니다.
inputRef.current.focus();
};
return (
<div>
<h2>새로운 Todo 작성하기 ✏ </h2>
<div>
{/* inputRef 변수를 사용하여 input 요소에 포커스를 맞춥니다. */}
<input
ref={inputRef}
value={task}
onChange={onChangeTask}
placeholder="할 일을 추가로 입력해주세요."
/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
);
};
export default TodoEditor;
- Enter 키로 할 일 추가하기
// src/components/TodoEditor.jsx
import React, { useRef, useState } from 'react';
const TodoEditor = ({ addTodo }) => {
(...)
// input에서 Enter 키를 누르면 할 일을 추가하는 함수를 호출합니다.
const onKeyDown = (e) => {
if (e.key === 'Enter') {
onSubmit()
}
}
return (
<div>
<h2>새로운 Todo 작성하기 ✏ </h2>
<div>
{/* onKeyDown 이벤트에 onKeyDown 함수를 연결합니다. */}
<input
ref={inputRef}
value={task}
onChange={onChangeTask}
onKeyDown={onKeyDown}
placeholder="할 일을 추가로 입력해주세요."
/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
);
};
export default TodoEditor;
Search: 할 일 검색하기
- 할 일 검색 & 소문자 검색
// src/components/TodoList.jsx
import React, { useState } from 'react';
import TodoItem from './TodoItem';
const TodoList = ({ todos }) => {
// state를 이용하여 input에 입력된 검색어를 관리합니다.
const [search, setSearch] = useState('');
// input에 입력된 검색어를 상태로 관리합니다.
const onChangeSearch = e => {
setSearch(e.target.value);
};
// 검색어를 포함하는 할 일 목록을 저장합니다.
const filteredTodo = () => {
// 검색어가 포함된 할 일 목록을 반환합니다.
// todo.filter() 함수는 todo 배열을 순회하면서 검색어가 포함된 할 일 목록을 반환합니다.
// toLowerCase()를 이용하여 검색어와 할 일 목록의 task를 소문자로 변경합니다.
return todos.filter((item) => item.task.toLowerCase().includes(search.toLowerCase()));
};
return (
<div>
<h2>할 일 목록 📃</h2>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요"/>
<ul>
{filteredTodo().map((todo) => (
<TodoItem key={todo.id} {...todo} />
))}
</ul>
</div>
);
};
export default TodoList;
Update: 작업 완료
- 로직
- 사용자 : TodoItem 체크박스 틱(체크표시) 합니다.
- TodoItem : onUpate 함수를 호출하고 해당 체크박스의 id를 전달합니다.
- Todo : id에 해당하는 할 일의 isDone을 변경합니다.
- TodoList : 변경된 할 일 목록을 전달하여 화면에 표시합니다.
- TodoItem : 할 일 목록의 수정 및 삭제 버튼을 추가합니다.
- 완료 표시하기
// src/components/Todo.jsx
(...)
const Todo = () => {
(...)
// 완료 표시를 클릭시 호출되는 함수 onUpdate를 만들고, id에 해당하는 할 일의 isDone을 변경합니다.
const onUpdate = (id) => {
// id에 해당하는 할 일의 isDone을 변경합니다.
setTodos(
// map() 함수를 이용하여 todo 배열을 순회하면서 id에 해당하는 할 일의 isDone을 변경합니다.
todos.map((item) => {
// id에 해당하는 할 일의 isDone을 변경합니다.
if (item.id === id) {
return { ...item, isDone: !item.isDone }
}
// 변경된 할 일을 반환합니다.
return item
})
)
}
// 완료 표시를 클릭시 호출되는 함수 onUpdate를 만들고, id에 해당하는 할 일의 isDone을 변경합니다.
const onUpdate = (id) => {
setTodos(
// map() 함수를 이용하여 todo 배열을 순회하면서
todos.map((todo) =>
// id에 해당하는 할 일의 isDone을 변경합니다.
todo.id === id ? { ...todo, isDone: !todo.isDone } : todo
)
);
};
return (
<div className="flex flex-col gap-5">
<TodoHd />
<TodoEditor addTodo={addTodo} />
{/* onUpdate 함수를 TodoList 컴포넌트에 전달합니다. */}
<TodoList todos={todos} onUpdate={onUpdate} />
</div>
);
};
export default Todo;
// src/components/TodoList.jsx
(...)
const TodoList = ({ todos, onUpdate }) => {
(...)
return (
<div>
<h2>할 일 목록 📃</h2>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요"/>
<ul>
{filteredTodo().map((todo) => (
{/* onUpdate 함수를 TodoItem 컴포넌트에 전달합니다. */}
<TodoItem key={todo.id} onUpdate={onUpdate} {...todo} />
))}
</ul>
</div>
);
};
export default TodoList;
// src/components/TodoItem.jsx
import React from 'react';
// onUpdate 함수를 추가합니다.
const TodoItem = ({ id, isDone, task, createdDate, onUpdate }) => {
console.log(`TodoItem ${task}: isDone = ${isDone}`); // 추가
return (
<div>
<li key={id}>
<input
type="checkbox"
checked={isDone}
onChange={() => onUpdate(id)} // onUpdate 함수를 호출합니다.
/>
<span className={`${isDone ? 'line-through text-gray-400' : 'no-underline text-black'}`}>
{task}
</span>
<span>{new Date(createdDate).toLocaleDateString()}</span>
<button>삭제</button>
</li>
</div>
);
};
export default TodoItem;
Delete : 할 일 삭제하기
- 로직
- 사용자 : TodoItem 삭제 버튼 클릭
- TodoItem : onDelete 함수를 호출하고 해당 삭제 버튼의 id를 전달합니다.
- Todo : id에 해당하는 할 일을 삭제합니다.
- TodoList : 변경된 할 일 목록을 전달하여 화면에 표시합니다.
- 할 일 삭제하기
// src/components/Todo.jsx
(...)
const Todo = () => {
(...)
// 삭제 버튼을 클릭시 호출되는 함수 onDelete를 만들고, id에 해당하는 할 일을 삭제합니다.
const onDelete = (id) => {
// 해당 id 요소를 뺀 나머지 요소들만 반환합니다.
setTodos(todos.filter((todo) => todo.id !== id));
}
return (
<div className="flex flex-col gap-5">
<TodoHd />
<TodoEditor addTodo={addTodo} />
{/* onDelete 함수를 TodoList 컴포넌트에 전달합니다. */}
<TodoList todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
};
export default Todo;
// src/components/TodoList.jsx
(...)
{/* onDelete 함수를 TodoItem 컴포넌트에 전달합니다. */}
const TodoList = ({ todos, onUpdate, onDelete }) => {
(...)
return (
<div>
<h2>할 일 목록 📃</h2>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요"/>
<ul>
{filteredTodo().map((todo) => (
{/* onDelete 함수를 TodoItem 컴포넌트에 전달합니다. */}
<TodoItem key={todo.id} onUpdate={onUpdate} onDelete={onDelete} {...todo} />
))}
</ul>
</div>
);
};
export default TodoList;
// src/components/TodoItem.jsx
import React from 'react';
{/* onDelete 함수를 추가합니다. */}
const TodoItem = ({ id, isDone, task, createdDate, onUpdate, onDelete }) => {
console.log(`TodoItem ${task}: isDone = ${isDone}`); // 추가
return (
<div>
<li key={id}>
(...)
{/* onDelete 함수를 호출합니다. */}
<span>{new Date(createdDate).toLocaleDateString()}</span>
<button onClick={() => onDelete(id)}>삭제</button>
</li>
</div>
);
};
export default TodoItem;
4. useEffect를 활용한 확장
목표: 할 일을 로컬 스토리지에 저장하고, 초기 렌더링 시 저장된 데이터를 불러오도록 변경.
수정 내용:
- useEffect로 로컬 스토리지에 todos를 저장.
- 컴포넌트가 마운트될 때 로컬 스토리지에서 데이터를 불러와 초기 상태로 설정.
// src/components/Todo.jsx
import { useState, useEffect } from "react";
const Todo = () => {
const [todos, setTodos] = useState([]);
// 1. 초기 데이터 로드
useEffect(() => {
// localStorage에서 'todos' 키로 저장된 데이터를 가져옴
// JSON.parse() 함수를 이용하여 문자열을 객체로 변환
const savedTodos = JSON.parse(localStorage.getItem('todos')) || [];
// 가져온 데이터로 상태 업데이트
setTodos(savedTodos);
}, []) // 빈 배열: 컴포넌트가 처음 마운트될 때만 실행
// 2. 데이터 자동 저장
useEffect(() => {
// todos 상태가 변경될 때마다 localStorage에 저장
// JSON.stringify() 함수를 이용하여 객체를 문자열로 변환
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]) // todos가 변경될 때마다 실행
// 기존 addTodo, onUpdate, onDelete 함수 유지
};
- localStorage란?
localStorage는 웹 브라우저에서 제공하는 데이터 저장소로 데이터를 브라우저에 저장할 수 있습니다.
- 브라우저를 닫아도 데이터가 유지됩니다.
- 도메인 별로 데이터를 저장할 수 있습니다.
- 데이터는 문자열 형태로만 저장할 수 있습니다.
- localStorage 사용법
// 데이터 저장
localStorage.setItem('key', 'value');
// 데이터 불러오기
localStorage.getItem('key');
// 데이터 삭제
localStorage.removeItem('key');
- 주의할 점
localStorage에는 문자열만 저장할 수 있습니다
- 객체나 배열을 저장할 때는
JSON.stringify()
로 문자열로 변환 - 불러올 때는
JSON.parse()
로 다시 객체로 변환
- 객체나 배열을 저장할 때는
용량 제한이 있습니다
- 보통 5MB ~ 10MB 정도 (브라우저마다 다름)
보안에 민감한 데이터는 저장하면 안됩니다
- 비밀번호나 개인정보 등은 피해야 함
실제로 확인하는 방법
- 브라우저에서
F12
키를 눌러 개발자 도구 열기 Application
탭 선택- 왼쪽 메뉴에서
Local Storage
선택 - 저장된 데이터 확인 가능
이렇게 localStorage
를 사용하면 새로고침을 하거나 브라우저를 닫았다가 다시 열어도 Todo 목록이 유지되는 것입니다.
5. useReducer로 상태 관리 전환
목표
- 상태 관리 로직을 useReducer로 변경하여 코드의 가독성과 유지보수성을 높이기.
- dispatch를 사용하여 상태 업데이트의 흐름을 명확히 하기.
useReducer란?
- 복잡한 상태 관리 로직을 한 곳에 모으는 데 유용.
- 상태 변경 로직을 Reducer 함수로 분리.
- dispatch로 특정 액션을 호출하여 상태를 변경.
구성 요소
- Reducer 함수: 상태와 액션을 받아 새로운 상태를 반환.
- State 초기값: 상태의 초기값을 정의.
- Dispatch: 상태를 변경하기 위한 액션을 보냄.
변경할 내용
- reducer 함수 정의 (예: ADD_TODO, UPDATE_TODO, DELETE_TODO).
- useReducer로 상태 관리 변경.
// reducers/todoReducer.js
// uuid 패키지를 사용하여 고유한 ID를 생성하고, 초기 상태와 Reducer 함수를 정의합니다.
import {v4 as uuidv4} from "uuid";
// 액션 타입 정의
// 문법 : export const 액션타입 = "액션타입";
export const ADD_TODO = "ADD_TODO";
export const UPDATE_TODO = "UPDATE_TODO";
export const DELETE_TODO = "DELETE_TODO";
// 초기 상태
export const initialState = [];
// Reducer 함수
// 문법 : export const 리듀서함수명 = (state, action) => { switch(action.type) { case 액션타입: return 변경된상태; default: throw new Error(`Unknown action type: ${action.type}`); } };
export const todoReducer = (state, action) => {
switch (action.type) {
case ADD_TODO:
return [
{
id: uuidv4(), // 고유한 UUID 생성
task: action.payload.task,
isDone: false,
createdDate: new Date().getTime(),
},
...state,
];
case UPDATE_TODO:
return state.map((todo) =>
todo.id === action.payload.id ? { ...todo, isDone: !todo.isDone } : todo
);
case DELETE_TODO:
return state.filter((todo) => todo.id !== action.payload.id);
default:
throw new Error(`Unknown action type: ${action.type}`);
}
};
// app/components/Todo.jsx
"use client";
import { useReducer, useEffect } from "react";
import { todoReducer, initialState, ADD_TODO, UPDATE_TODO, DELETE_TODO } from "@/reducers/todoReducer";
import TodoHd from "@/components/TodoHd";
import TodoEditor from "@/components/TodoEditor";
import TodoList from "@/components/TodoList";
// 로컬 스토리지 키 선언
const LOCAL_STORAGE_KEY = "my-todo-app-todos";
const Todo = () => {
const [todos, dispatch] = useReducer(todoReducer, initialState);
// 로컬 스토리지에서 초기 상태 로드
useEffect(() => {
const savedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [];
savedTodos.forEach((todo) => dispatch({ type: ADD_TODO, payload: todo }));
}, []);
// 상태 변경 시 로컬 스토리지에 저장
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos));
}, [todos]);
// 할 일을 추가하는 함수
const addTodo = (task) => {
dispatch({ type: ADD_TODO, payload: { task } });
};
// 완료 상태를 업데이트하는 함수
const onUpdate = (id) => {
dispatch({ type: UPDATE_TODO, payload: { id } });
};
// 할 일을 삭제하는 함수
const onDelete = (id) => {
dispatch({ type: DELETE_TODO, payload: { id } });
};
return (
<div className="flex flex-col gap-5">
<TodoHd />
<TodoEditor addTodo={addTodo} />
<TodoList todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
};
export default Todo;
6. useContext로 전역 상태 관리
목표
- Context API를 사용해 상태와 dispatch를 전역적으로 공유.
- 컴포넌트 간 상태 전달을 단순화.
변경할 내용
- TodoContext를 생성하여 상태와 dispatch를 전역으로 제공.
- 컴포넌트에서 useContext를 사용해 상태와 dispatch를 쉽게 접근.
// src/contexts/TodoContext.js
"use client";
import { createContext, useContext, useReducer, useEffect } from "react";
import { todoReducer, initialState, ADD_TODO, UPDATE_TODO, DELETE_TODO } from "@/reducers/todoReducer";
// Context 생성
// 문법 : const context = createContext(defaultValue);
const TodoContext = createContext();
// Custom Hook
// 문법 : const customHook = () => useContext(context);
export const useTodoContext = () => {
const context = useContext(TodoContext);
if (!context) {
throw new Error("useTodoContext must be used within a TodoProvider");
}
return context;
};
// Provider 컴포넌트
// 문법 : const Provider = ({ children }) => { return <context.Provider value={value}>{children}</context.Provider>; };
export const TodoProvider = ({ children }) => {
const [todos, dispatch] = useReducer(todoReducer, initialState);
const LOCAL_STORAGE_KEY = "my-todo-app-todos";
// 로컬 스토리지에서 초기 상태 로드
useEffect(() => {
const savedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [];
savedTodos.forEach((todo) => dispatch({ type: ADD_TODO, payload: todo }));
}, []);
// 상태 변경 시 로컬 스토리지에 저장
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos));
}, [todos]);
// 할 일 추가
const addTodo = (task) => {
dispatch({ type: ADD_TODO, payload: { task } });
};
// 완료 상태 업데이트
const onUpdate = (id) => {
dispatch({ type: UPDATE_TODO, payload: { id } });
};
// 할 일 삭제
const onDelete = (id) => {
dispatch({ type: DELETE_TODO, payload: { id } });
};
const value = {
todos,
addTodo,
onUpdate,
onDelete,
};
return <TodoContext.Provider value={value}>{children}</TodoContext.Provider>;
};
// src/components/Todo.jsx
"use client";
import { TodoProvider } from "@/contexts/TodoContext";
import TodoHd from "@/components/TodoHd";
import TodoEditor from "@/components/TodoEditor";
import TodoList from "@/components/TodoList";
const Todo = () => {
return (
<TodoProvider>
<div className="flex flex-col gap-5">
<TodoHd />
<TodoEditor />
<TodoList />
</div>
</TodoProvider>
);
};
export default Todo;
// src/components/TodoEditor.jsx
import { useState, useRef } from "react";
import { useTodoContext } from "@/contexts/TodoContext";
const TodoEditor = () => {
const [task, setTask] = useState("");
const inputRef = useRef();
const { addTodo } = useTodoContext();
const onSubmit = () => {
if (!task) return; // 빈 입력 방지
addTodo(task);
setTask(""); // 입력창 초기화
inputRef.current.focus(); // 입력창 포커스
};
const onKeyDown = (e) => {
if (e.key === "Enter") onSubmit();
};
return (
<div>
<input
ref={inputRef}
value={task}
onChange={(e) => setTask(e.target.value)}
onKeyDown={onKeyDown}
placeholder="할 일을 입력하세요"
/>
<button onClick={onSubmit}>추가</button>
</div>
);
};
export default TodoEditor;
// src/components/TodoList.jsx
import { useTodoContext } from "@/contexts/TodoContext";
import TodoItem from "@/components/TodoItem";
const TodoList = () => {
const { todos, onUpdate, onDelete } = useTodoContext();
return (
<ul>
{todos.map((todo) => (
<TodoItem
key={todo.id}
{...todo}
onUpdate={onUpdate}
onDelete={onDelete}
/>
))}
</ul>
);
};
export default TodoList;
7. 최적화
최적화란 불필요한 렌더링을 줄여 성능을 향상시키는 것을 말합니다.
목표:
React.memo
와useMemo
를 사용하여 성능 최적화.React.memo
: 컴포넌트의 props가 변경되지 않으면 리렌더링을 방지. (컴포넌트의 props가 바뀌지 않으면 다시 그리지 않도록 함.)useMemo
: 계산 비용이 높은 함수의 결과를 캐싱하여 성능을 향상시키는 방법은, 복잡한 계산을 반복하지 않고 한 번 계산한 결과를 저장해 두었다가 필요할 때 다시 사용하는 것입니다. 이렇게 하면 동일한 계산을 여러 번 수행할 필요가 없어져서 프로그램의 속도가 빨라집니다.useCallback
: 함수를 캐싱하여 성능을 향상시키는 방법은, 함수를 새로 만들지 않고 재사용하는 것입니다. 이렇게 하면 동일한 함수를 여러 번 만들 필요가 없어져서 프로그램의 속도가 빨라집니다.
TodoItem.js 최적화
React.memo를 사용하여 동일한 props가 전달되면 컴포넌트 리렌더링을 방지.
// src/components/TodoItem.jsx
import React from 'react';
// React.memo를 사용하여 동일한 props가 전달되면 컴포넌트 리렌더링을 방지.
const TodoItem = React.memo(({ id, isDone, task, createdDate, onUpdate, onDelete }) => {
console.log(`TodoItem ${task}: isDone = ${isDone}`); // 추가
return (
<div>
<li key={id}>
<input
type="checkbox"
checked={isDone}
onChange={() => onUpdate(id)}
/>
<span className={`${isDone ? 'line-through text-gray-400' : 'no-underline text-black'}`}>
{task}
</span>
<span>{new Date(createdDate).toLocaleDateString()}</span>
<button onClick={() => onDelete(id)}>삭제</button>
</li>
</div>
);
});
// displayName 추가
TodoItem.displayName = 'TodoItem';
export default TodoItem;
TodoList.js 최적화
React.memo를 사용하여 동일한 props가 전달되면 컴포넌트 리렌더링을 방지.
// src/components/TodoList.jsx
import React, { useCallback, useMemo, useState } from 'react';
import TodoItem from './TodoItem';
import {useTodoContext} from "@/contexts/TodoContext";
const TodoList = () => {
// useTodoContext()를 통해 todos, onUpdate, onDelete 함수를 가져온다.
const { todos, onUpdate, onDelete } = useTodoContext();
const [search, setSearch] = useState('');
// 검색어 변경 핸들러를 useCallback으로 메모이제이션
const onChangeSearch = useCallback(e => {
setSearch(e.target.value);
}, []);
// 검색된 할 일을 useMemo로 캐싱
const filteredTodo = useMemo(() => {
return todos.filter((item) => item.task.toLowerCase().includes(search.toLowerCase()));
}, [todos, search]);
// TodoItem에서 사용할 핸들러를 useCallback으로 메모이제이션
const handleUpdate = useCallback((id) => onUpdate(id), [onUpdate]);
const handleDelete = useCallback((id) => onDelete(id), [onDelete]);
return (
<div>
<h2>할 일 목록 📃</h2>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요"/>
<ul>
{filteredTodo().map((todo) => (
<TodoItem
key={todo.id}
onUpdate={() => handleUpdate(todo.id)} // 메모이제이션된 업데이트 핸들러
onDelete={() => handleDelete(todo.id)} // 메모이제이션된 삭제 핸들러 {...todo}
/>
))}
</ul>
</div>
);
};
export default TodoList;
8. TypeScript로 전환
목표: TypeScript로 타입 안정성을 제공.
수정 내용:
- 상태와 함수의 타입 정의.
- 설치
npm install typescript @types/react @types/react-dom --save-dev
- 초기 설정
npx tsc --init
// tsconfig.json
{
"compilerOptions": {
"target": "ESNext", // 최신 JavaScript 버전 사용
"module": "ESNext", // ES 모듈 사용
"jsx": "preserve", // React JSX 지원
"strict": true, // 엄격한 타입 검사
"moduleResolution": "Node", // Node.js 모듈 방식 사용
"esModuleInterop": true, // ES 모듈 간 상호 운용성 지원
"forceConsistentCasingInFileNames": true, // 대소문자 일관성 검사
"skipLibCheck": true, // 타입 선언 파일 검사 생략
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"noEmit": true,
"incremental": true,
"resolveJsonModule": true,
"isolatedModules": true,
"plugins": [
{
"name": "next"
}
]
},
"include": [
"src",
".next/types/**/*.ts"
], // 컴파일할 파일 경로
"exclude": [
"node_modules",
".next",
] // 컴파일 제외 디렉토리
}
- 파일 확장자 변경
.jsx
→.tsx
.js
→.ts
- 타입 정의
interface Todo {
id: number;
task: string;
isDone: boolean;
createdDate: number;
}
type Action =
| { type: "ADD_TODO"; payload: Todo }
| { type: "UPDATE_TODO"; payload: { id: number } }
| { type: "DELETE_TODO"; payload: { id: number } };
const reducer = (state: Todo[], action: Action): Todo[] => {
switch (action.type) {
// 기존 로직
}
};
- 패키지 추가 설치
npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
npm install prettier eslint-config-prettier eslint-plugin-prettier --save-dev
9. Firebase 연동
목표: Firebase Firestore와 Authentication 연동.
수정 내용:
- Firebase에서 사용자 인증 및 Firestore를 통해 데이터를 동기화.
import { getFirestore, collection, addDoc, getDocs } from "firebase/firestore";
const db = getFirestore();
const fetchTodos = async () => {
const querySnapshot = await getDocs(collection(db, "todos"));
querySnapshot.forEach((doc) => console.log(doc.data()));
};
const addTodo = async (task) => {
await addDoc(collection(db, "todos"), { task, isDone: false, createdDate: new Date().getTime() });
};
10. Node.js 서버 추가
목표: Node.js와 Express로 REST API 서버 개발.
수정 내용:
- Node.js 서버에서 Todo CRUD API 구현.
import express from "express";
const app = express();
app.use(express.json());
const todos = [];
app.post("/todos", (req, res) => {
const todo = req.body;
todos.push(todo);
res.status(201).send(todo);
});
app.listen(3000, () => console.log("Server running on http://localhost:3000"));
11. Axios로 API 호출
목표: Axios를 사용하여 Node.js 서버와 통신.
수정 내용:
- Axios로 서버에 데이터를 전송하고 받아오는 로직 추가.
import axios from "axios";
const addTodo = async (task) => {
const { data } = await axios.post("http://localhost:3000/todos", { task, isDone: false });
setTodos([data, ...todos]);
};
'Front > React' 카테고리의 다른 글
[Hooks] useReducer - React 배우기 (0) | 2024.03.19 |
---|---|
최적화 - React 배우기 (0) | 2024.03.18 |
useEffect, 생명주기 - React 배우기 (0) | 2024.03.11 |
React 카운터 앱 만들기 - component, useState (1) | 2024.03.04 |
React(리액트) 기본 기능 정리 (0) | 2024.02.29 |