tags:
- TIL
- React
created: 2025-06-25
📘 2025-06-25 TIL
SPA 구조에서 페이지 전환을 구현하기 위해 react-router-dom을 도입했다. createBrowserRouter를 활용해 라우트들을 정의하고, RouterProvider를 통해 앱 전체에 적용했다.
npm install react-router-dom @types/react-router-dom
이번 프로젝트에서는 다음과 같은 페이지를 구성했다.
이런 페이지들은 createBrowserRouter와 RouterProvider를 이용해 설정한다.
const router = createBrowserRouter([
{
path: '/',
element: <Layout />, // 모든 페이지의 공통 레이아웃 (Header/Footer 포함)
errorElement: <Error />, // 존재하지 않는 경로 접근 시 표시
children: [
{ index: true, element: <Home /> }, // 기본 경로 → 홈
{ path: 'books', element: <Books /> }, // 도서 목록
{ path: 'login', element: <Login /> }, // 로그인
{ path: 'signup', element: <Signup /> }, // 회원가입
],
},
]);
<Link to="/books">
도서 목록</Link>
와 같이 Link 컴포넌트를 활용하면 새로고침 없이 페이지 전환이 가능하다.
- book.model.ts: 책 정보 인터페이스 (Book, BookDetail 등)
- cart.model.ts: 장바구니 아이템 모델 (CartItem, CartRequestBody)
- category.model.ts: 카테고리 이름과 id (Category)
- order.model.ts: 주문 관련 모델 (Order, OrderItem)
- pagination.model.ts: 페이지네이션 처리용 (Pagination, PageMeta)
- user.model.ts: 사용자 정보 (User, LoginResponse, 등)
// models/user.model.ts
export interface SignupForm {
email: string;
password: string;
}
// api/http.ts
import axios from 'axios';
export const httpClient = axios.create({
baseURL: 'http://localhost:9999',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});
// api/auth.api.ts
import { httpClient } from './http';
import type { SignupForm } from '@/models/user.model';
export const signup = async (userData: SignupForm) => {
const response = await httpClient.post('/users/join', userData);
return response.data;
};
이렇게 API 요청 로직과 UI를 분리하면 유지보수가 훨씬 수월하다.
프론트엔드에서 API를 통해 데이터를 가져오는 구조는 아래와 같이 단계별 레이어로 나뉘며, 각 레이어는 역할이 명확하게 구분되어 유지보수성과 확장성을 높여준다.
[View] → [Custom Hook] → [Query Library (Optional)] → [Fetcher] → [API Server]
레이어 | 설명 | 파일 예시 |
---|---|---|
View | 화면 UI 렌더링, 훅 호출로 상태 사용 | Header.tsx, Detail.tsx |
Hooks | 재사용 가능한 비즈니스 로직 정의. 내부에서 fetcher 또는 React Query 호출 | useCategory.ts |
Query Lib | (선택) React Query 등 상태 관리 라이브러리에서 useQuery, useMutation 사용 | useCategory.ts 내부 |
Fetcher | 실제 HTTP 요청 담당. axios나 fetch로 API 호출 | category.api.ts |
API Server | Express 등 백엔드 서버 | GET /categories 등 |
src/
├── pages/
│ └── Header.tsx
├── hooks/
│ └── useCategory.ts
├── api/
│ └── category.api.ts
├── models/
│ └── category.model.ts
카테고리 데이터를 관리하는 커스텀 훅을 만들고, UI는 해당 훅만 사용하도록 했다.
헤더 메뉴 구성 시에도 useCategory 훅만 호출하면 된다.
// hooks/useCategory.ts
import { useEffect, useState } from 'react';
import { fetchCategory } from '@/api/category.api';
import { toast } from 'react-hot-toast';
export const useCategory = () => {
const [categories, setCategories] = useState([]);
useEffect(() => {
fetchCategory()
.then((data) => {
const categoryWithAll = [
{ category_id: 0, category_name: '전체' },
...data,
];
setCategories(categoryWithAll);
})
.catch(() => toast.error('카테고리 정보를 불러올 수 없습니다.'));
}, []);
return { categories };
};
앞서 정의한 useCategory 훅은 아래와 같이 실제 내비게이션 메뉴에서 활용 가능하다. 사용자는 특정 카테고리를 클릭해 해당 도서 목록으로 이동할 수 있으며, "전체"는 /books 전체 목록을 보여준다.
import { useCategory } from '@/hooks/useCategory';
import { Link } from 'react-router-dom';
const Header = () => {
const { categories } = useCategory();
return (
<nav>
<ul className="flex items-center gap-4">
{categories.map((item) => (
<li key={item.category_id}>
<Link
to={
item.category_id === 0
? '/books'
: `/books?category_id=${item.category_id}`
}
className={navItemStyle}
>
{item.category_name}
</Link>
</li>
))}
</ul>
</nav>
);
};
React Hook Form을 이용해 회원가입 폼을 만들고, Toast를 활용해 사용자 피드백을 전달했다.
const {
register,
handleSubmit,
formState: { errors },
} = useForm<SignupForm>();
회원가입 제출 시 로딩 상태를 띄우고, 완료되면 성공 알림과 함께 로그인 페이지로 이동한다:
const onSubmit = async (data: SignupForm) => {
const toastId = loading('회원가입 처리중입니다...');
try {
await signup(data);
dismiss(toastId);
success('회원가입이 완료되었습니다.');
navigate('/login');
} catch (err) {
dismiss(toastId);
error('회원가입에 실패했습니다. 다시 시도해주세요.');
}
};
커스텀 알림 훅도 구현했다:
// hooks/useAlert.ts
import { toast } from 'react-hot-toast';
export const useAlert = () => {
return {
success: (msg: string) => toast.success(msg),
error: (msg: string) => toast.error(msg),
loading: (msg: string) => toast.loading(msg),
dismiss: (id?: string) => toast.dismiss(id),
};
};
• 새롭게 알게 된 점
react-hook-form이 기본적인 유효성 검사와 상태 관리를 굉장히 간단하게 처리해준다는 점이 인상적이었다. 특히 토스트와 함께 사용할 때 사용자 경험도 좋아졌다.
• 어렵게 느껴졌던 부분
타입 충돌 및 API 호출 중 토스트 메시지를 깔끔하게 처리하는 구조를 만드는 데 시행착오가 있었다.
• 다음에 학습할 주제