간단한 React To Do 앱을 만들어보자
FE/React2024. 9. 3. 22:57처음 React를 배울때 만들어봤던 To Do 앱을 다시 한번 만들어보고 싶은 마음이 생겼습니다. 본 글에서는 React로 만든 To Do 앱에 대한 내용을 설명합니다.
기술 스택 정의
이전에는 CRA(Create React App)을 사용해서 프로젝트를 생성했으나, 이번에는 TypeScript
와 Vite
기반의 프로젝트를 생성하여 진행했습니다.
- 언어: TypeScript
- 번들러: Vite
- 트랜스파일러: SWC
- 기타 사용 라이브러리
- tailwindcss
- zustand
- react-hook-form
- yup
- classnames
- dayjs
- prettier
- husky
vite 프로젝트 생성하는 방법에 대해 궁금하신 분은 아래 글을 참고해주세요
프로젝트 구조
사실 간단한 프로젝트라서 components로만 구성해도 될거 같았으나, 혹시라도 추가적인 기능을 개발할 경우를 대비래서 features
라는 디렉토리도 추가했습니다.
src
├── App.tsx
├── components # 전역에서 사용하는 공통 컴포넌트
├── constants # 전역에서 사용하는 상수
├── features
│ └── todo
│ ├── components # todo 관련 컴포넌트
│ ├── constants # todo 관련 상수
│ ├── hooks # todo 관련 hooks
│ ├── index.ts
│ ├── store # todo 전역 store
│ └── types # todo 관련 타입
├── hooks # 전역에서 사용하는 hooks
├── types # 전역에서 사용하는 types
├── index.css
├── main.tsx
└── vite-env.d.ts
프로젝트 구조에는 정답은 없다고 생각하지만, 프로젝트를 진행하면서 위 방식이 저한테는 제일 관리하기 편했던거 같습니다.
기능 정리
이번에 만드려는 todo 앱은 아래 3가지 특징을 고려해서 개발하려고 합니다.
- todo item 을 생성 / 수정 / 삭제 / 완료할 수 있어야 함
- 날짜별로 todo item을 볼 수 있어야 함
- 브라우저를 닫고 다시 접속해도 데이터가 유지되어야 함
- 서버를 따로 둘 생각은 없기 때문에, localStorage와 zustand를 사용할 예정
코드 설명
날짜 변경
날짜를 쉽게 조작할 수 있게 도와주는 dayjs 라이브러리를 추가하였습니다.
bun add dayjs
props drillings를 피하기 위해, 현재 날짜(curDate
)를 전역 상태로 관리하였고 전역상태 관리를 위해 zustand
를 추가했습니다
bun add zustand
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import dayjs, { Dayjs } from 'dayjs';
interface TTodoState {
curDate: Dayjs;
setCurDate: (curDate: Dayjs) => void;
}
const useTodoStore = create<TTodoState>()(
devtools((set) => ({
curDate: dayjs(),
setCurDate: (curDate) => set(() => ({ curDate })),
})),
);
export const useTodoDateStore = () =>
useTodoStore(({ curDate, setCurDate }) => ({ curDate, setCurDate }));
이렇게 만든 store hook을 사용하여 버튼 클릭시 날짜가 하루씩 변경되도록 했습니다.
export default function TodoCardHeader() {
const { curDate, setCurDate } = useTodoDateStore();
const onClickPrevBtn = () => setCurDate(curDate.subtract(1, 'd'));
const onClickNextBtn = () => setCurDate(curDate.add(1, 'd'));
return (
<div className="flex items-center gap-2 sm:gap-6 border-b h-[80px] px-4 sm:px-6">
<ArrowButton onClick={onClickPrevBtn} icon={<Icons.ChevronLeft />} />
<div className="h-full flex-1 text-indigo-500 flex gap-4 items-center justify-center">
<span className="text-lg sm:text-2xl font-bold uppercase">{curDate.format('dddd')}</span>
<span className="sm:text-xl">{curDate.format('MMMM DD')}th</span>
</div>
<ArrowButton onClick={onClickNextBtn} icon={<Icons.ChevronRight />} />
</div>
);
}
todo 항목 추가
todo 항목을 추가하기 위해, react-hook-form
과 yup
을 사용했습니다. react-hook-form은 form control을 쉽게 하기 위함이고, yup은 유효성 검사를 위해 추가했습니다. (사실 유효성 검사할 것도 없어서 useState()로 해도 충분할 것 같긴 합니다)
bun add react-hook-form yup
todo 목록을 날짜별로 확인 할 수 있어야 하므로, key-value 형식으로 데이터를 구성했고, key는 날짜(YYYY-MM-DD) value에는 todo 목록을 넣도록 했습니다. 우선 타입부터 생성해봅시다.
import { TodoState } from '@/features/todo';
export type TTodo = {
id: number;
state: TodoState;
todo: string;
created: string;
updated: string;
};
export type TTodoInfo = { [key: string]: TTodo[] };
저는 타입이나 인터페이스를 정의할때 항상 앞에 T를 붙이는 컨벤션을 사용하고 있습니다. 이 부분은 취향에 맞게 개발하시면 될거 같습니다
todo 목록도 store로 관리하였습니다. 단, localStorage
에 데이터를 저장하기 위해 persist
미들웨어를 활용했습니다.
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { TTodoInfo } from '@/features/todo';
interface TPersistTodoState {
todoInfo: TTodoInfo;
setTodoInfo: (todoInfo: TTodoInfo) => void;
}
const usePersistTodoStore = create<TPersistTodoState>()(
devtools(
persist(
(set) => ({
todoInfo: {},
setTodoInfo: (todoInfo) => set(() => ({ todoInfo })),
}),
{ name: 'todoStore' },
),
),
);
export const useTodoStore = () =>
usePersistTodoStore(({ todoInfo, setTodoInfo }) => ({ todoInfo, setTodoInfo }));
이렇게 만든 useTodoStore 훅을 사용해서, 항목을 추가하였습니다.
import classNames from 'classnames';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
import dayjs from 'dayjs';
import { yupResolver } from '@hookform/resolvers/yup';
import { TodoState, TTodo, useTodoDateStore, useTodoStore } from '@/features/todo';
import { DATE_FORMAT } from '@/constants';
type TFormParams = {
todo: string;
};
const schema = yup
.object({
todo: yup.string().required(),
})
.required();
export default function TodoAddForm() {
const { curDate } = useTodoDateStore();
const { todoInfo, setTodoInfo } = useTodoStore();
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<TFormParams>({ resolver: yupResolver(schema) });
const onSubmit = async (formParams: TFormParams) => {
const todo = formParams.todo.trim();
const todoItem: TTodo = {
todo,
id: dayjs().valueOf(),
state: TodoState.normal,
created: dayjs().format(DATE_FORMAT.FULL_DATE),
updated: dayjs().format(DATE_FORMAT.FULL_DATE),
};
const targetDate = curDate.format(DATE_FORMAT.DATE);
const nextTodoInfo = {
...todoInfo,
};
if (!nextTodoInfo?.[targetDate]) {
nextTodoInfo[targetDate] = [todoItem];
} else {
nextTodoInfo[targetDate] = [...nextTodoInfo[targetDate], todoItem];
}
setTodoInfo(nextTodoInfo);
reset();
};
return (
<div className="flex flex-col gap-1">
<form
className="flex h-[60px] bg-gray-100 rounded-full overflow-hidden group"
onSubmit={handleSubmit(onSubmit)}
>
<input
{...register('todo', { required: true })}
placeholder="내용을 입력해주세요"
className="py-2 px-6 flex-1 bg-transparent outline-none"
/>
<button
className={classNames(
'rounded-full w-[120px] h-[60px] text-white transition duration-100 ease-in-out',
'cursor-pointer bg-indigo-500 hover:bg-indigo-500 active:bg-indigo-600',
)}
>
ADD
</button>
</form>
{errors.todo && (
<div className="px-6 text-red-600 text-sm line-clamp-1">{errors.todo?.message}</div>
)}
</div>
);
}
localStorage 에도 값이 잘 들어가고 있습니다.
결과
만든 앱은 vercel
통해서 배포해두었습니다. 아래 사이트에서 확인 할 수 있습니다.
github repo: https://github.com/bluemiv/react-todo-app
처음 리액트를 배울때는 useState와 props로만 개발을 했었는데, 이번에는 전역 상태관리, react-hook-form, yup 등 다른 라이브러리도 같이 활용해봤습니다.
'FE > React' 카테고리의 다른 글
Styled Components에서 Reset CSS 적용하기 (0) | 2024.09.25 |
---|---|
useMemo를 사용하면 정말로 성능이 좋아질까? (0) | 2024.08.28 |
모바일과 데스크탑을 구분하는 React 커스텀 Hook (0) | 2024.08.27 |
Vite기반 React 프로젝트에서 Path Aliasing 설정하기 (0) | 2024.08.26 |
Vite 기반 React 프로젝트에 Tailwind CSS 적용하기 (0) | 2024.08.25 |
IT 기술에 대한 글을 주로 작성하고, 일상 내용, 맛집/숙박/제품 리뷰 등 여러가지 주제를작성하는 블로그입니다. 티스토리 커스텀 스킨도 개발하고 있으니 관심있으신분은 Berry Skin을 검색바랍니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!