
간단한 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 프로젝트 생성하는 방법에 대해 궁금하신 분은 아래 글을 참고해주세요
Bun 설치 및 React 프로젝트 생성하기
Bun은 JavaScript 및 TypeScript 프로젝트를 위한 번들러(bundler)입니다. 본 글에서는 Bun을 이용하여 React 프로젝트를 생성하는 방법에 대해 설명합니다. Bun 설치하기먼저, React 프로젝트를 생성하기 전
bluemiv.tistory.com
프로젝트 구조
사실 간단한 프로젝트라서 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
통해서 배포해두었습니다. 아래 사이트에서 확인 할 수 있습니다.
To Do List
blumiv-react-todo-app.vercel.app
github repo: https://github.com/bluemiv/react-todo-app
처음 리액트를 배울때는 useState와 props로만 개발을 했었는데, 이번에는 전역 상태관리, react-hook-form, yup 등 다른 라이브러리도 같이 활용해봤습니다.
'FE > React' 카테고리의 다른 글
Virtual DOM 때문에 React를 사용할까? (0) | 2025.01.08 |
---|---|
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 |
IT 기술에 대한 글을 주로 작성하고, 일상 내용, 맛집/숙박/제품 리뷰 등 여러가지 주제를작성하는 블로그입니다. 티스토리 커스텀 스킨도 개발하고 있으니 관심있으신분은 Berry Skin을 검색바랍니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!