📘 2025-06-25 TIL

📌 오늘 배운 핵심 요약

  • react-router-dom을 사용한 라우팅 구성
  • API 요청을 위한 Axios 인스턴스 구성
  • 도메인별 모델 타입 정의
  • react-hook-form을 통한 회원가입 폼 구현
  • react-hot-toast로 사용자 피드백 처리
  • 관심사 분리를 위한 커스텀 훅 설계

🧠 상세 학습 내용

📍 주제 1: 라우트 작성

SPA 구조에서 페이지 전환을 구현하기 위해 react-router-dom을 도입했다. createBrowserRouter를 활용해 라우트들을 정의하고, RouterProvider를 통해 앱 전체에 적용했다.


🔧 1. 라우팅 라이브러리 설치

npm install react-router-dom @types/react-router-dom

🧭 2. 주요 경로 구성

이번 프로젝트에서는 다음과 같은 페이지를 구성했다.

  • / : 홈
  • /books : 도서 목록
  • /login : 로그인 페이지
  • /signup : 회원가입 페이지

이런 페이지들은 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 컴포넌트를 활용하면 새로고침 없이 페이지 전환이 가능하다.


📍 주제 2: 모델 정의 및 API 통신 구조

✅ 도메인별로 타입을 정의한 모델 파일을 생성

- 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;
}
  • 백엔드에서 내려주는 응답 필드(book_id, summary, price)를 그대로 반영해 타입 정의하는 것이 핵심

🧩 API 요청은 http.ts에서 생성한 Axios 인스턴스를 기반

// 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를 분리하면 유지보수가 훨씬 수월하다.


📍 주제 3: API 통신과 데이터 레이어

프론트엔드에서 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

🧠 구조 분리의 장점

  • 관심사 분리 (Separation of Concerns)
    → UI, 데이터 처리, API 요청을 명확히 분리하여 유지보수 용이
  • 테스트 및 재사용성 강화
    → fetcher와 hook은 단위 테스트 및 재사용이 쉬움
  • 선택적 Query Library 사용
    → React Query, SWR은 비동기 상태 관리에 유용하지만 상황에 따라 생략 가능

📍 주제 4: useCategory 훅을 통한 관심사 분리

카테고리 데이터를 관리하는 커스텀 훅을 만들고, UI는 해당 훅만 사용하도록 했다.
헤더 메뉴 구성 시에도 useCategory 훅만 호출하면 된다.

✅ 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 };
};
  • API에서 받아온 카테고리 배열에 category_id: 0, category_name: '전체' 항목을 수동으로 추가하여, 사용자에게 ‘전체 보기’ 옵션을 제공하도록 구성했다. 백엔드에서는 전체 카테고리를 따로 보내주지 않는 게 일반적이고, 이는 필터 UI나 네비게이션 등에서 자주 사용되는 UX 패턴이다.

✅ 카테고리 데이터를 활용한 네비게이션 렌더링

앞서 정의한 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>
  );
};

📍 주제 5: 회원가입 폼 구현 (react-hook-form + react-hot-toast)

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),
  };
};

signup

💭 회고

• 새롭게 알게 된 점
react-hook-form이 기본적인 유효성 검사와 상태 관리를 굉장히 간단하게 처리해준다는 점이 인상적이었다. 특히 토스트와 함께 사용할 때 사용자 경험도 좋아졌다.

• 어렵게 느껴졌던 부분
타입 충돌 및 API 호출 중 토스트 메시지를 깔끔하게 처리하는 구조를 만드는 데 시행착오가 있었다.

다음에 학습할 주제

  • 비밀번호 초기화 구현
  • 로그인과 전역상태
  • 도서 목록 페이지