앞선 포스트의 next.js + redux + typescript 세팅을 해보면서 redux를 좀 더 자연스럽게 익히고 연습하기 위해 간단한 todolist 프로젝트로 CRUD 기능을 구현해보았다. redux를 연습하는 것이 주 목적이었으므로 style은 tailwind css로 최소한만 적용했다.
Next.js + Redux + TypeScript 세팅
components
/InputForm.tsx
/TodoList.tsx
/TodoItem.tsx
pages
/_app.tsx
/index.tsx
store
/action
- todos.ts
/reducer
- main.ts
- todoReducer.ts
// pages/index.tsx
import Head from 'next/head'
import Image from 'next/image'
import { Inter } from 'next/font/google'
import styles from '@/styles/Home.module.css'
import InputForm from '@/components/InputForm'
import TodoList from '@/components/TodoList'
export default function Home() {
return (
<>
<div className="flex flex-col items-center justify-center min-h-screen bg-purple-500">
<h1 className="text-3xl bg-slate-200 w-2/5 text-center pt-4">My Todo</h1>
<InputForm />
<TodoList />
</div>
</>
)
}
create read update delete
import { ActionsType, ADD_TODO, DELETE_TODO, ISCOMPLETE_TODO, UPDATE_TODO } from "../actions/todos";
interface todoType {
title: string,
isComplete: boolean,
}
interface InitialStateType {
todos: todoType[]
}
const initialState: InitialStateType = {
todos: [],
}
export default function TodoReducer(state = initialState, action: ActionsType) {
switch (action.type) {
case ADD_TODO:
return
case DELETE_TODO:
return
case ISCOMPLETE_TODO:
return
case UPDATE_TODO:
return
default:
return state
}
}
import { useSelector } from "react-redux"
import TodoItem from "./TodoItem"
const TodoList = () => {
const todos = useSelector(state => state.TodoReducer.todos)
return (
<div className="w-2/5 px-5 pb-4 gap-4 bg-slate-200 flex flex-col justify-around ">{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}</div>
)
}
export default TodoList
export const ADD_TODO = "ADD_TODO"
// 새로운 todo가 생성 될 때마다 주어지는 임시 id 값
let id = 1
// 브라우저로부터 받아올 객체 형태의 todo 타입 정의
interface AddTodoType {
title: string,
isComplete: boolean
}
export const addTodo = (todo: AddTodoType) => {
return {
type: ADD_TODO,
payload: {
todo: {
id: id++,
title: todo.title,
isComplete: todo.isComplete,
}
}
}
}
export default function TodoReducer(state = initialState, action: ActionsType) {
switch (action.type) {
// 1. 생성
case ADD_TODO:
return {
todos: [...state.todos, action.payload.todo]
}
default:
return state
}
}
// components/InputForm.tsx
import { addTodo } from "@/store/actions/todos"
import { useState } from "react"
import { useDispatch } from "react-redux"
const InputForm = () => {
const dispatch = useDispatch()
const [text, setText] = useState('')
const handleChange = (e) => {
setText(e.target.value)
}
const handleSubmit = (e) => {
e.preventDefault()
const todo = {
title: text,
isComplete: false,
}
dispatch(addTodo(todo))
console.log(todo);
setText('')
}
return (
<div className="w-2/5">
<form onSubmit={handleSubmit} className="flex flex-row gap-4 bg-slate-200 p-5">
<input type="text" value={text} onChange={handleChange} placeholder="Enter here..." className=" pl-4 p-3 border-2 border-black flex-1" />
<button className="border-2 border-black px-3 py-2 bg-slate-500 rounded-lg text-white flex-0">추가</button>
</form>
</div>
)
}
export default InputForm
export const DELETE_TODO = "DELETE_TODO"
export const deleteTodo = (id: number) => {
return {
type: DELETE_TODO,
payload: {
id,
}
}
}
export default function TodoReducer(state = initialState, action: ActionsType) {
switch (action.type) {
case ADD_TODO:
return {
todos: [...state.todos, action.payload.todo]
}
// 2. 삭제
case DELETE_TODO:
return {
todos: [...state.todos.filter((todo) => todo.id !== action.payload.id)]
}
default:
return state
}
}
// components/TodoItem.tsx
import { deleteTodo } from "@/store/actions/todos"
import { useDispatch } from "react-redux"
import { MdOutlineCheckBoxOutlineBlank, MdCheckBox } from "react-icons/md"
const TodoItem = ({ todo }) => {
const dispatch = useDispatch()
const handleDelete = () => {
// 액션을 dispatch하여 리듀서 삭제 함수를 호출함
dispatch(deleteTodo(todo.id))
}
return (
<div className = "flex flex-row justify-between items-center">
<span className="font-semibold ml-3">{todo.title}</span>
<button onClick={handleDelete} className="border-2 border-black px-3 py-1 ml-1 bg-slate-500 text-white rounded-lg">삭제</button>
</div>
)
}
export default TodoItem
export const ISCOMPLETE_TODO = "ISCOMPLETE_TODO"
export const UPDATE_TODO = "UPDATE_TODO"
interface EditTodoType {
id: number,
title: string,
}
export const isCompleteTodo = (id: number) => {
return {
type: ISCOMPLETE_TODO,
payload: {
id, // id 값으로 todo를 조회한 후 isComplete 값을 변경할 예정
}
}
}
export const updateTodo = (todo: EditTodoType) => {
return {
type: UPDATE_TODO,
payload: {
todo: {
id: todo.id, // 수정할 todo를 찾기위해 id값 필요
title: todo.editTitle // 수정할 title 값을 받아옴
}
}
}
}
export type ActionsType =
ReturnType<typeof addTodo> |
ReturnType<typeof deleteTodo> |
ReturnType<typeof isCompleteTodo> |
ReturnType<typeof updateTodo>
export default function TodoReducer(state = initialState, action: ActionsType) {
switch (action.type) {
case ADD_TODO:
return {
todos: [...state.todos, action.payload.todo]
}
case DELETE_TODO:
return {
todos: [...state.todos.filter((todo) => todo.id !== action.payload.id)]
}
// 수정 - isComplete 완료여부
case ISCOMPLETE_TODO:
return {
todos: [...state.todos.map((todo) => todo.id === action.payload.id ? { ...todo, isComplete: !todo.isComplete } : todo)]
}
// 수정 - title 내용
case UPDATE_TODO:
return {
todos: [...state.todos.map((todo) => todo.id === action.payload.todo.id ? {...todo, title: action.payload.todo.title} : todo)]
}
default:
return state
}
}
// components/TodoItem.tsx
import { deleteTodo, isCompleteTodo, updateTodo } from "@/store/actions/todos"
import { useDispatch } from "react-redux"
import { MdOutlineCheckBoxOutlineBlank, MdCheckBox } from "react-icons/md"
import { useEffect, useRef, useState } from "react"
const TodoItem = ({ todo }) => {
const dispatch = useDispatch()
const [edited, setEdited] = useState(false)
const [newText, setNewText] = useState(todo.title)
// useRef()에 타입 지정 없이 null을 넣게 되면 리액트 라이프 사이클 특성상 error가 발생함
// useEffect를 사용하여 ref 값을 사용
const editInputRef = useRef<any>(null)
useEffect(() => {
// focus 값을 찾을 수 없다고 나오는 경우
// if(editInputRef.current){} 로 current가 있는지 확인 후 focus 사용 가능
if (edited && editInputRef.current) {
editInputRef.current.focus();
}
}, [edited]);
const handleDelete = () => {
dispatch(deleteTodo(todo.id))
}
const handleCheck = () => {
dispatch(isCompleteTodo(todo.id))
}
const handleEdit = () => {
setEdited(true)
}
const onChangeEditInput = (e) => {
setNewText(e.target.value);
};
const onClickSubmitButton = () => {
const nextTodoList = {
id: todo.id,
editTitle: newText
}
dispatch(updateTodo(nextTodoList))
setEdited(false)
}
return (
<div className = "flex flex-row justify-between items-center" >
<span className="flex flex-row items-center">
<span>{todo.isComplete ? <MdCheckBox onClick={handleCheck} size="30" /> : <MdOutlineCheckBoxOutlineBlank onClick={handleCheck} size="30" />}</span>
{edited ? <input type="text" value={newText} onChange={onChangeEditInput} ref={editInputRef} /> : <span className="font-semibold ml-3">{todo.title}</span>}
</span>
<span>
{edited ?
<button onClick={onClickSubmitButton} className="border-2 border-black px-3 py-1 bg-slate-500 text-white rounded-lg">확인</button>
:
<button onClick={handleEdit} className="border-2 border-black px-3 py-1 bg-slate-500 text-white rounded-lg">수정</button>
}
<button onClick={handleDelete} className="border-2 border-black px-3 py-1 ml-1 bg-slate-500 text-white rounded-lg">삭제</button>
</span>
</div >
)
}
export default TodoItem