반응형
할 일 관리 앱 만들기
1. 컴포넌트 구조 설계하기
- App 컴포넌트 만들기
// src/App.js
import React from 'react'
import TodoHd from './TodoHd'
import TodoEditor from './component/TodoEditor'
import TodoList from './component/TodoList'
export default function App() {
return (
<div>
<TodoHd />
<TodoEditor />
<TodoList />
</div>
)
}
- Header 컴포넌트 만들기
// src/components/TodoHd.js
import React from 'react'
export default function TodoHd() {
return (
<div>
<h1>📝 할 일 관리 앱</h1>
<p>2024.03.11 오늘의 할 일을 적어보세요.</p>
</div>
)
}
- TodoEditor 컴포넌트 만들기
// src/components/TodoEditor.js
import React from 'react'
export default function TodoEditor() {
return (
<div>
<h2>새로운 Todo 작성하기 ✏ </h2>
<div>
<input placeholder="할 일을 추가로 입력해주세요." />
<button>추가</button>
</div>
</div>
)
}
- TodoList 컴포넌트 만들기
// src/components/TodoList.js
import React from 'react'
import TodoItem from './TodoItem'
export default function TodoList() {
return (
<div>
<h2>할 일 목록 📃</h2>
<input placeholder="검색어를 입력하세요" />
<ul>
<li>
<input type="checkbox" />
<span>고양이 밥주기</span>
<span>2024.03.11</span>
<button>삭제</button>
</li>
<li>
<input type="checkbox" />
<span>감자 캐기</span>
<span>2024.03.11</span>
<button>삭제</button>
</li>
<li>
<input type="checkbox" />
<span>고양이 놀아주기</span>
<span>2024.03.11</span>
<button>삭제</button>
</li>
</ul>
</div>
)
}
2. 기능 구현 준비하기
- App 컴포넌트 : 할 일 데이터 관리
- Header 컴포넌트 : 오늘 날짜 표시
- TodoEditor 컴포넌트 : 할 일 추가
- TodoList 컴포넌트 : 검색에 따라 필터링된 할 일 목록 표시
- TodoItem 컴포넌트 : 할 일 목록의 수정 및 삭제
데이터를 다루는 4가지 기능인 추가(Create), 조회(Read), 수정(Update), 삭제(Delete) 기능을 앞글자만 따서 CRUD라고 부릅니다. 이러한 CRUD 기능을 구현하기 위해 다음과 같은 데이터 구조를 사용합니다.
- 오늘 날짜 표시하기
날짜를 표시하는 라이브러리로 date-fns를 사용합니다.
npm install date-fns
yarn add date-fns
// src/components/TodoHd.js
import React from 'react'
import { Text, Box } from '@chakra-ui/react'
import { format } from 'date-fns'
export default function TodoHd() {
return (
<Box>
<Text fontSize={18}>
<Text as="span" fontSize={24} fontWeight={700}>
📝 {format(new Date(), 'yyyy.MM.dd')}
</Text>{' '}
오늘의 할 일을 적어보세요.
</Text>
</Box>
)
}
- 기초 데이터 설정하기
// src/App.js
import React, { useState } from 'react'
import TotoHd from './TodoHd'
import TodoEditor from './component/TodoEditor'
import TodoList from './component/TodoList'
// 할 일 데이터
const mockTodo = [
{
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(),
},
]
export default function App() {
// useState를 이용하여 할 일 데이터를 관리합니다.
const [todo, setTodo] = useState(mockTodo)
return (
<div>
<TotoHd />
<TodoEditor />
<TodoList />
</div>
)
}
Read: 할 일 목록 렌더링하기
- 로직
- App 컴포넌트 : 할 일 데이터 관리
- TodoList 컴포넌트 : 할 일 목록 렌더링
- 할 일 목록 렌더링하기
// src/App.js
import React, { useState } from 'react'
import TodoHd from './TodoHd'
import TodoEditor from './TodoEditor'
import TodoList from './TodoList'
const mockTodo = [
(...)
]
export default function App() {
const [todo, setTodo] = useState(mockTodo)
return (
<div>
<TodoHd />
<TodoEditor />
// 할 일 목록을 TodoList 컴포넌트에 전달합니다.
<TodoList todo={todo} />
</div>
)
}
// src/components/TodoList.js
import React from 'react'
export default function TodoList({ todo }) {
return (
<div>
<h2>할 일 목록 📃</h2>
<input placeholder="검색어를 입력하세요" />
<ul>
{todo.map((item) => (
// item.id를 key로 사용하여 고유한 값을 설정합니다.
// ...item은 item 객체의 모든 속성을 TodoItem 컴포넌트에 전달합니다.
<TodoItem key={item.id} {...item} />
))}
</ul>
</div>
)
}
// src/components/TodoItem.js
const TodoItem = ({ id, isDone, task, createdDate }) => {
return (
<div>
<li key={id}>
<input type="checkbox" checked={isDone} />
<span>{task}</span>
<span>{new Date(createdDate).toLocaleDateString()}</span>
<button>삭제</button>
</li>
</div>
)
}
export default TodoItem
Create: 할 일 추가하기
- 로직
- 사용자 : 할 일 입력, 추가 버튼 클릭
- TodoEditor : 입력한 할 일을 받아와서 할 일 목록을 관리하는 상태인 todo에 추가
- TodoList : 추가된 할 일 목록을 전달하여 화면에 표시
- App : 추가된 할 일 목록을 브라우저의 로컬 스토리지에 저장
- TodoEditor
- 빈 입력 방지
- input 초기화 및 포커스
- Enter 키로 할 일 추가
- 소문자 검색
- 할 일 추가하기
// src/App.js
import React, { useState, useRef } from 'react'
import TodoHd from './TodoHd'
import TodoEditor from './TodoEditor'
import TodoList from './TodoList'
const mockTodo = [
(...)
]
function App() {
const [todo, setTodo] = useState(mockTodo)
// 할 일을 추가하는 함수를 만듭니다.
const addTodo = (task) => {
// 새로운 할 일 객체를 만듭니다.
const newTodo = {
id: todos.length + 1,, // id는 1씩 증가하는 숫자로 설정합니다.
isDone: false,
task,
createdDate: new Date().getTime(),
}
// 기존 할 일 목록에 새로운 할 일을 추가합니다. (새로운 할 일이 위로 올라가도록 합니다.)
setTodo([newTodo, ...todo])
}
return (
<div>
<TodoHd />
<TodoEditor addTodo={addTodo} />
<TodoList todo={todo} />
</div>
)
}
export default App
- 할 일 추가 함수 호출하기
// src/components/TodoEditor.js
import { useState, useRef } 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 className="TodoEditor">
<h2>새로운 Todo 작성하기 ✏ </h2>
<div>
{/* inputRef를 input 요소에 연결합니다. */}
{/* task 상태값을 value로 설정 */}
<input ref={inputRef} value={task} onChange={onChangeTask} placeholder="할 일을 추가로 입력해주세요." />
<button onClick={onSubmit}>추가</button>
</div>
</div>
)
}
export default TodoEditor
- input에 포커스 맞추기
// src/components/TodoEditor.js
import { useState, useRef } from 'react'
const TodoEditor = ({ addTodo }) => {
(...)
// inputRef 변수가 useRef()로 생성됩니다.
// 연결된 input 요소에 포커스를 맞추기 위해 사용합니다.
const inputRef = useRef()
(...)
// 추가 버튼을 클릭하면 할 일을 추가하는 함수를 호출합니다.
const onSubmit = () => {
// task가 빈 문자열이면 함수를 종료합니다.
if (!task) return
// 할 일을 추가하는 함수를 호출합니다.
addTodo(task)
}
return (
<div className="TodoEditor">
<h2>새로운 Todo 작성하기 ✏ </h2>
<div>
{/* inputRef가 존재하면 focus() 실행 */}
<input ref={(inputRef) => inputRef && inputRef.focus()} value={task} onChange={onChangeTask} placeholder="할 일을 추가로 입력해주세요." />
<button onClick={onSubmit}>추가</button>
</div>
</div>
)
}
export default TodoEditor
- 아이템 입력 후 input 초기화하기
// src/components/TodoEditor.js
import { useState, useRef } from 'react'
const TodoEditor = ({ addTodo }) => {
(...)
const onSubmit = () => {
if (!task) return
addTodo(task)
// 할 일을 추가한 후 input을 초기화합니다.
setTask('')
}
return (
<div className="TodoEditor">
<h2>새로운 Todo 작성하기 ✏ </h2>
<div>
<input ref={inputRef} value={task} onChange={onChangeTask} placeholder="할 일을 추가로 입력해주세요." />
<button onClick={onSubmit}>추가</button>
</div>
</div>
)
}
export default TodoEditor
- Enter 키로 할 일 추가하기
// src/components/TodoEditor.js
import { useState, useRef } from 'react'
const TodoEditor = ({ addTodo }) => {
(...)
const onSubmit = () => {
(...)
}
// input에서 Enter 키를 누르면 할 일을 추가하는 함수를 호출합니다.
const onKeyDown = (e) => {
if (e.key === 'Enter') {
onSubmit()
}
}
return (
<div className="TodoEditor">
<h2>새로운 Todo 작성하기 ✏ </h2>
<div>
<input ref={inputRef} value={task} onChange={onChangeTask} onKeyDown={onKeyDown} placeholder="할 일을 추가로 입력해주세요." />
<button onClick={onSubmit}>추가</button>
</div>
</div>
)
}
export default TodoEditor
Search: 할 일 검색하기
- 할 일 검색하기
// src/components/TodoList.js
import React from 'react'
import TodoItem from './TodoItem'
export default function TodoList({ todo }) {
// state를 이용하여 input에 입력된 검색어를 관리합니다.
const [search, setSearch] = useState('')
// input에 입력된 검색어를 상태로 관리합니다.
const onChangeSearch = (e) => {
setSearch(e.target.value)
}
// 검색어를 포함하는 할 일 목록을 저장합니다.
const filteredTodo = () => {
// 검색어가 포함된 할 일 목록을 반환합니다.
// todo.filter() 함수는 todo 배열을 순회하면서 검색어가 포함된 할 일 목록을 반환합니다.
return todo.filter((item) => item.task.includes(search))
}
return (
<div>
<h2>할 일 목록 📃</h2>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력하세요" />
<ul>
{filteredTodo().map((item) => (
<TodoItem key={item.id} {...item} />
))}
</ul>
</div>
)
}
- 대소문자 구분 없이 검색하기
// src/components/TodoList.js
import React from 'react'
import TodoItem from './TodoItem'
export default function TodoList({ todo }) {
(...)
const filteredTodo = () => {
// toLowerCase()를 이용하여 검색어와 할 일 목록의 task를 소문자로 변경합니다.
return todo.filter((item) => item.task.toLowerCase().includes(search.toLowerCase()))
}
return (
<div>
<h2>할 일 목록 📃</h2>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력하세요" />
<ul>
{filteredTodo().map((item) => (
<TodoItem key={item.id} {...item} />
))}
</ul>
</div>
)
}
Update: 작업 완료
- 로직
- 사용자 : TodoItem 체크박스 틱(체크표시) 합니다.
- TodoItem : onUpate 함수를 호출하고 해당 체크박스의 id를 전달합니다.
- App : id에 해당하는 할 일의 isDone을 변경합니다.
- TodoList : 변경된 할 일 목록을 전달하여 화면에 표시합니다.
- TodoItem : 할 일 목록의 수정 및 삭제 버튼을 추가합니다.
- 완료 표시하기
// src/components/App.js
(...)
function App() {
const [todo, setTodo] = useState(mockTodo)
(...)
// 완료 표시를 클릭시 호출되는 함수 onUpdate를 만들고, id에 해당하는 할 일의 isDone을 변경합니다.
const onUpdate = (id) => {
// id에 해당하는 할 일의 isDone을 변경합니다.
setTodo(
// map() 함수를 이용하여 todo 배열을 순회하면서 id에 해당하는 할 일의 isDone을 변경합니다.
todo.map((item) => {
// id에 해당하는 할 일의 isDone을 변경합니다.
if (item.id === id) {
return { ...item, isDone: !item.isDone }
}
// 변경된 할 일을 반환합니다.
return item
})
)
}
return (
<div>
<TodoHd />
<TodoEditor addTodo={addTodo} />
<TodoList todo={todo} onUpdate={onUpdate} />
</div>
)
}
// src/components/TodoList.js
import React from 'react'
import TodoItem from './TodoItem'
export default function TodoList({ todo, onUpdate }) {
(...)
return (
<div>
<h2>할 일 목록 📃</h2>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력하세요" />
<ul>
{filteredTodo().map((item) => (
// onUpdate 함수를 TodoItem 컴포넌트에 전달합니다.
<TodoItem key={item.id} onUpdate={onUpdate} {...item} />
))}
</ul>
</div>
)
}
// src/components/TodoItem.js
const TodoItem = ({ id, isDone, task, createdDate, onUpdate }) => {
return (
<div>
<li key={id}>
{/* 체크박스를 클릭하면 onUpdate 함수를 호출합니다. */}
<input type="checkbox" checked={isDone} onChange={() => onUpdate(id)} />
<span>{task}</span>
<span>{format(new Date(createdDate), 'yyyy.MM.dd')}</span>
<button>삭제</button>
</li>
</div>
)
}
export default TodoItem
- 완료 취소선 표시하기
// src/components/TodoItem.js
const TodoItem = ({ id, isDone, task, createdDate, onUpdate }) => {
return (
<div>
<li key={id}>
<input type="checkbox" checked={isDone} onChange={() => onUpdate(id)} />
{/* 완료된 할 일은 흐리게 표시합니다. */}
<span style={{ textDecoration: isDone ? 'line-through' : 'none' }}>{task}</span>
<span>{new Date(createdDate).toLocaleDateString()}</span>
<button>삭제</button>
</li>
</div>
)
}
export default TodoItem
Delete : 할 일 삭제하기
- 로직
- 사용자 : TodoItem 삭제 버튼 클릭
- TodoItem : onDelete 함수를 호출하고 해당 삭제 버튼의 id를 전달합니다.
- App : id에 해당하는 할 일을 삭제합니다.
- TodoList : 변경된 할 일 목록을 전달하여 화면에 표시합니다.
- 할 일 삭제하기
// src/components/App.js
(...)
function App() {
(...)
// 삭제 버튼을 클릭시 호출되는 함수 onDelete를 만들고, id에 해당하는 할 일을 삭제합니다.
const onDelete = (id) => {
// 해당 id 요소를 뺀 나머지 요소들만 반환합니다.
setTodo(todo.filter((item) => item.id !== id))
}
return (
<div>
<TodoHd />
<TodoEditor addTodo={addTodo} />
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} />
</div>
)
}
// src/components/TodoList.js
import React from 'react'
import TodoItem from './TodoItem'
export default function TodoList({ todo, onUpdate, onDelete }) {
(...)
return (
<div>
<h2>할 일 목록 📃</h2>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력하세요" />
<ul>
{filteredTodo().map((item) => (
{/* onDelete 함수를 TodoItem 컴포넌트에 전달합니다. */}
<TodoItem key={item.id} onUpdate={onUpdate} onDelete={onDelete} {...item} />
))}
</ul>
</div>
)
}
// src/components/TodoItem.js
const TodoItem = ({ id, isDone, task, createdDate, onUpdate, onDelete }) => {
return (
<div>
<li key={id}>
<input type="checkbox" checked={isDone} onChange={() => onUpdate(id)} />
<span>{task}</span>
<span>{new Date(createdDate).toLocaleDateString()}</span>
{/* 삭제 버튼을 클릭하면 onDelete 함수를 호출하고 해당 인수로 해당 아이템의 id를 전달합니다. */}
<button onClick={() => onDelete(id)}>삭제</button>
</li>
</div>
)
}
export default TodoItem
useReducer로 상태 관리하기
- useReducer란
- useState를 대체하여 컴포넌트에서 상태를 관리하는 훅입니다.
- 상태를 변경하는 로직을 컴포넌트 바깥에 선언하여 재사용이 용이합니다.
- 상태를 변경하는 로직이 복잡해져도 코드가 간결해집니다.
- useReducer 사용하기
// src/components/App.js
import React, { useReducer } from 'react'
import TodoHd from './TodoHd'
import TodoEditor from './TodoEditor'
import TodoList from './TodoList'
const mockTodo = [
{
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(),
},
]
// reducer 함수로 상태를 변경합니다.
// state : 현재 상태
// action : 상태를 변경할 때 참조하는 값
function reducer(state, action) {
switch (action.type) {
case 'ADD':
return [action.payload, ...state]
case 'UPDATE':
return state.map((it) => (it.id === action.payload ? { ...it, isDone: !it.isDone } : it))
case 'DELETE':
return state.filter((it) => it.id !== action.payload)
default:
return state
}
}
function App() {
// useReducer를 이용하여 상태를 관리합니다.
// 첫 번째 인수로 reducer 함수를, 두 번째 인수로 초기 상태를 전달합니다.
// 반환값으로 state와 dispatch 함수를 받습니다.
// state : 현재 상태
// dispatch : action을 발생시키는 함수
// dispatch 함수를 이용하여 action을 발생시키면 reducer 함수가 호출됩니다.
// reducer 함수가 호출되면 state가 변경됩니다.
// state가 변경되면 컴포넌트가 리렌더링됩니다.
const [todo, dispatch] = useReducer(reducer, mockTodo)
const addTodo = (task) => {
const newTodo = {
id: Date.now(),
isDone: false,
task,
createdDate: new Date().getTime(),
}
dispatch({ type: 'ADD', payload: newTodo })
}
const onUpdate = (id) => {
dispatch({ type: 'UPDATE', payload: id })
}
const onDelete = (id) => {
dispatch({ type: 'DELETE', payload: id })
}
return (
<div>
<TodoHd />
<TodoEditor addTodo={addTodo} />
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} />
</div>
)
}
export default App
최적화
최적화란 불필요한 연산을 줄여 성능을 향상시키는 것입니다.
최적화 방법
- 코드, 폰트, 이미지 등의 리소스를 압축합니다.
- 메모이제이션을 이용하여 불필요한 연산을 줄입니다.
메모이제이션
- 메모이제이션이란 연산의 결과를 기억했다가 필요할 때 사용함으로써 불필요한 연산을 방지하는 것입니다.
- useMemo : 함수의 불필요한 재실행을 방지합니다.
- useCallback : 함수의 불필요한 재생성을 방지합니다.
- 총 할 일 개수, 완료한 할 일 개수, 남은 할 일 개수를 표시
useMemo를 사용하지 않고
// src/components/TodoList.js
import React, { useState } from 'react'
import TodoItem from './TodoItem'
export default function TodoList({ todo, onUpdate, onDelete }) {
const [search, setSearch] = useState('')
const onChangeSearch = (e) => {
setSearch(e.target.value)
}
// 최적화 테스트
const lookBack = () => {
// lookBack 함수가 빈번하게 호출됩니다.
console.log('lookBack')
const total = todo.length
const done = todo.filter((item) => item.isDone).length
const left = total - done
return { total, done, left }
}
return (
<div>
<h2>할 일 목록 📃</h2>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력하세요" />
<ul>
{todo.map((item) => (
<TodoItem key={item.id} onUpdate={onUpdate} onDelete={onDelete} {...item} />
))}
</ul>
<h3>
총 {lookBack().total}개 중에 {lookBack().done}개 완료, {lookBack().left}개 남음
</h3>
</div>
)
}
- useMemo를 이용하여 할 일 목록 렌더링 최적화하기
문법 : useMemo(() => 연산 결과, [의존성 배열])
- useMemo란 연산 결과를 기억했다가 필요할 때 사용함으로써 불필요한 연산을 방지하는 것입니다.
- 의존성 배열에 포함된 값이 변경될 때만 연산을 수행합니다.
// src/components/TodoList.js
import React, { useState, useMemo } from 'react'
import TodoItem from './TodoItem'
export default function TodoList({ todo, onUpdate, onDelete }) {
const [search, setSearch] = useState('')
const onChangeSearch = (e) => {
setSearch(e.target.value)
}
// 최적화 테스트
const lookBack = useMemo(() => {
console.log('lookBack')
const total = todo.length
const done = todo.filter((item) => item.isDone).length
const left = total - done
return { total, done, left }
}, [todo])
return (
<div>
<h2>할 일 목록 📃</h2>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력하세요" />
<ul>
{todo.map((item) => (
<TodoItem key={item.id} onUpdate={onUpdate} onDelete={onDelete} {...item} />
))}
</ul>
<h3>
총 {lookBack.total}개 중에 {lookBack.done}개 완료, {lookBack.left}개 남음
</h3>
</div>
)
}
- useCallback을 이용하여 함수 재생성 최적화하기
문법 : useCallback(() => 함수, [의존성 배열])
- useCallback이란 함수의 불필요한 재생성을 방지하는 것입니다.
- 의존성 배열에 포함된 값이 변경될 때만 함수를 재생성합니다.
// src/components/App.js
import React, { useReducer, useCallback } from 'react'
(...)
function App() {
const [todo, dispatch] = useReducer(reducer, mockTodo)
const addTodo = useCallback((task) => {
const newTodo = {
id: Date.now(),
isDone: false,
task,
createdDate: new Date().getTime(),
}
dispatch({ type: 'ADD', payload: newTodo })
}, [])
const onUpdate = useCallback((id) => {
dispatch({ type: 'UPDATE', payload: id })
}, [])
const onDelete = useCallback((id) => {
dispatch({ type: 'DELETE', payload: id })
}, [])
return (
<div>
<TodoHd />
<TodoEditor addTodo={addTodo} />
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} />
</div>
)
}
export default App
- React.memo를 이용하여 컴포넌트 최적화하기
문법 : React.memo(컴포넌트)
- React.memo란 컴포넌트의 불필요한 리렌더링을 방지하는 것입니다.
- 컴포넌트의 props가 변경되지 않으면 리렌더링을 방지합니다.
// src/components/TodoItem.js
import React from 'react'
const TodoItem = ({ id, isDone, task, createdDate, onUpdate, onDelete }) => {
return (
<div>
<li key={id}>
<input type="checkbox" checked={isDone} onChange={() => onUpdate(id)} />
<span style={{ textDecoration: isDone ? 'line-through' : 'none' }}>{task}</span>
<span>{new Date(createdDate).toLocaleDateString()}</span>
<button onClick={() => onDelete(id)}>삭제</button>
</li>
</div>
)
}
// React.memo를 이용하여 컴포넌트를 최적화합니다.
export default React.memo(TodoItem)
Context API로 전역 상태 관리하기
- Context API란
- Context API란 컴포넌트 트리 전체에 데이터를 제공하는 것입니다.
- 전역 상태를 관리할 때 사용합니다.
- Context API 사용하기
// src/context/TodoContext.js
import React, { createContext, useReducer, useContext } from 'react'
// 초기 상태
const initialState = [
{
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(),
},
]
// reducer 함수
function reducer(state, action) {
switch (action.type) {
case 'ADD':
return [action.payload, ...state]
case 'UPDATE':
return state.map((it) => (it.id === action.payload ? { ...it, isDone: !it.isDone } : it))
case 'DELETE':
return state.filter((it) => it.id !== action.payload)
default:
return state
}
}
// Context 객체 생성
const TodoStateContext = createContext()
const TodoDispatchContext = createContext()
// 커스텀 훅 생성
export function useTodoState() {
return useContext(TodoStateContext)
}
export function useTodoDispatch() {
return useContext(TodoDispatchContext)
}
// Provider 컴포넌트 생성
export function TodoProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<TodoStateContext.Provider value={state}>
<TodoDispatchContext.Provider value={dispatch}>{children}</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
)
}
// src/components/App.js
import React from 'react'
import TodoHd from './TodoHd'
import TodoEditor from './TodoEditor'
import TodoList from './TodoList'
import { TodoProvider } from '../context/TodoContext'
function App() {
return (
<TodoProvider>
<div>
<TodoHd />
<TodoEditor />
<TodoList />
</div>
</TodoProvider>
)
}
export default App
// src/components/TodoList.js
import React, { useState } from 'react'
import TodoItem from './TodoItem'
import { useTodoState, useTodoDispatch } from '../context/TodoContext'
export default function TodoList() {
const todo = useTodoState()
const dispatch = useTodoDispatch()
const [search, setSearch] = useState('')
const onChangeSearch = (e) => {
setSearch(e.target.value)
}
const filteredTodo = () => {
return todo.filter((item) => item.task.toLowerCase().includes(search.toLowerCase()))
}
return (
<div>
<h2>할 일 목록 📃</h2>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력하세요" />
<ul>
{filteredTodo().map((item) => (
<TodoItem key={item.id} {...item} />
))}
</ul>
</div>
)
}
// src/components/TodoEditor.js
import React, { useState } from 'react'
import { useTodoDispatch } from '../context/TodoContext'
export default function TodoEditor() {
const dispatch = useTodoDispatch()
const [task, setTask] = useState('')
const onChangeTask = (e) => {
setTask(e.target.value)
}
const onSubmit = () => {
if (!task) {
return
}
dispatch({ type: 'ADD', payload: { id: Date.now(), isDone: false, task, createdDate: new Date().getTime() } })
setTask('')
}
const onKeyDown = (e) => {
if (e.key === 'Enter') {
onSubmit()
}
}
return (
<div className="TodoEditor">
<h2>새로운 Todo 작성하기 ✏ </h2>
<div>
<input
value={task}
onChange={onChangeTask}
onKeyDown={onKeyDown}
placeholder="할 일을 추가로 입력해주세요."
/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
)
}
// src/components/TodoItem.js
import React from 'react'
import { useTodoDispatch } from '../context/TodoContext'
export default function TodoItem({ id, isDone, task, createdDate }) {
const dispatch = useTodoDispatch()
const onUpdate = () => {
dispatch({ type: 'UPDATE', payload: id })
}
const onDelete = () => {
dispatch({ type: 'DELETE', payload: id })
}
return (
<div>
<li key={id}>
<input type="checkbox" checked={isDone} onChange={onUpdate} />
<span style={{ textDecoration: isDone ? 'line-through' : 'none' }}>{task}</span>
<span>{new Date(createdDate).toLocaleDateString()}</span>
<button onClick={onDelete}>삭제</button>
</li>
</div>
)
}
반응형
'Front > React' 카테고리의 다른 글
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 |