[BookShop-FE] Route 틀 짜고, 백엔드와 연결해보자.

 

Route

1. React Router 사용

2.

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

 

Endpoint

1. 로그인 /login

2. 회원가입 / signup

3. 비밀번호 초기화 /reset

4. 도서 목록 /books

5. 도서 상세 /books/{id}

6. 장바구니 /cart

7. 주문서 작성 /order

8. 주문 목록 /orderlist

 


Route 지정해주기

react-router-dom의 createBroserRouter, RouterProvider 을 사용해 route를 지정해줄 수 있습니다. 

 

  • createBrowserRouter: 라우트 설정을 자바스크립트 객체 배열 형태로 생성, 어떤 경로(path)에서 어떤 컴포넌트(element)를 보여줄지 정의하는 리스트
  • RouterProvider: 위에서 생성한 라우터 객체를 실제 React 앱에 주입하는 역할, 앱의 최상단에서 한 번만 사용합니다.

App.tsx

 

import Layout from "./components/layout/Layout";
import Home from "./pages/Home";
import ThemeSwitcher from "./components/header/ThemeSwitcher";
import { useContext } from "react";
import { BookStoreThemeProvider, ThemeContext } from "./context/themeContext";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
  },
  {
    path: "./books",
    element: <div>도서 목록</div>
  }
])

function App() {
  const {themeName, toggleTheme} = useContext(ThemeContext);  
  return (
    <>
      <BookStoreThemeProvider>
        <Layout>
            <RouterProvider router={router} />  
        </Layout>
      </BookStoreThemeProvider>
    </>
  );
}

export default App;

현재에는 예상하지 못한 url 로 들어오면 에러페이지가 뜨는 상태. 이 공통적인 에러 페이지를 만들어보겠습니다. 

📄 Error.tsx

import { useRouteError } from "react-router-dom";

interface RouteError {
    statusText?: string;
    message?: String;
}
function Error() {
    const error = useRouteError() as RouteError;
    return (
        <div>
            <h1>오류가 발생했습니다.</h1>
            <p>다음과 같은 오류가 발생했습니다. </p>
            <p>{error.statusText || error.message}</p>
        </div>
    )
}

📄 app.ts

path: "/"의 errorElement 추가합니다. 

const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
    errorElement: <Error />
  },
  {
    path: "/books",
    element: <div>도서 목록</div>
  }
])

📄 Header.tsx

                    {
                        CATEGORY.map((item) => (
                            <li key={item.id}>
                                <a href={item.id === null ? `/books` : `/books?category_id=${item.id}`}>
                                {item.name}</a>
                            </li>
                        ))
                    }

 

<a> 태그는 '새로고침'을 발생시키고, <Link>는 '컴포넌트 교체'만 발생시키기 때문에, Link 로 수정해야지 화면깜빡임 없이 화면이동하는 걸로 보여집니다.

구분 <a> 태그 (Standard HTML) <Link> 컴포넌트 (React Router)
동작 방식 브라우저가 서버에 새로운 HTML을 요청함 브라우저 주소창의 URL만 변경하고 JS로 화면을 그림
상태 유지 앱 전체가 새로고침되어 모든 상태(State)가 초기화 기존 상태(State)가 유지됨 (예: 입력 중인 텍스트 등)
속도/사용성 화면 깜빡임이 발생하고 속도가 상대적으로 느림 필요한 부분만 갈아끼우므로 부드럽고 빠름 (SPA 방식)
                    {
                        CATEGORY.map((item) => (
                            <li key={item.id}>
                                <Link to={item.id === null ? `/books` : `/books?category_id=${item.id}`}>
                                {item.name}</Link>
                            </li>
                        ))
                    }

 


function App() {
  const {themeName, toggleTheme} = useContext(ThemeContext);  
  return (
    <>
      <BookStoreThemeProvider>
      	<Layout>
        <RouterProvider router={router} />
        </Layout>
      </BookStoreThemeProvider>
    </>
  );
}

export default App;


그러나 여기서 Router를 관리하는 주체(RouterProvider)가 Layout의 하위에 있기 때문에 오류가 발생합니다 .

function App() {
  const {themeName, toggleTheme} = useContext(ThemeContext);  
  return (
    <>
      <BookStoreThemeProvider>
        <RouterProvider router={router} />  
      </BookStoreThemeProvider>
    </>
  );
}

export default App;
import { useRouteError } from "react-router-dom";
import Layout from "../layout/Layout";

interface RouteError {
    statusText?: string;
    message?: String;
}
function Error() {
    const error = useRouteError() as RouteError;
    return (
        <Layout>
            <h1>오류가 발생했습니다.</h1>
            <p>다음과 같은 오류가 발생했습니다. </p>
            <p>{error.statusText || error.message}</p>
        </Layout>
    )
}

export default Error;

이렇게 수정해주어야합니다. 


프로젝트 모델 정의

book.model.ts

export interface Book {
    id: number;
    title: string,
    img: 7,
    category_id: 0,
    form: string;
    isbn: string;
    summary: string;
    detail: string;
    author: string;
    pages: number;
    contents: string;
    price: number;
    likes: number;
    pubDate: string;
}

export interface BookDetail extends Book {
    categoryName: string;
    liked: boolean;
}

cart.model.ts

export interface Cart {
    id: number;
    bookId: number;
    title: string;
    sumamry: string;
    quantity: number;
    price: number;
}

category.model.ts

export interface Category {
    id: number | null;
    name: string;
}

order.model.ts

export interface Order {
    id: number;
    createdAt: string;
    address: string;
    receiver: string;
    contact: string;
    bookTitle: string;
    totalQuantity: number;
    totalPrice: number;
}

pagination.model.ts

export interface Pagination {
    currentPage: number;
    totalNumber: number;
}

user.model.ts

export interface User {
    id: number;
    email: string;
    password?: string;
}

 

데이터 흐름

View(Header.tsx) -> Hooks(useCategory) -> Query Library(optional) -> Fetcher (category.api.ts(http.ts)) -> API server

처음으로 백엔드 서버와 연결을 해보겠습니다. 제 인생 처음이에요 ㅎㅎ 

Docker, MySQL 포함 백엔드 서버를 실행을 시켜주고 실행을 해주어야하고, 

CORS 관련 문제도 해결해야 연결이 됐습니다 .

https://ilove-ya.tistory.com/215

 

[Axios] Axios Network Error & CORS 오류 해결

문제 상황프론트에서 Axios 요청을 보냈을 때 다음과 같은 에러가 발생했다. AxiosError: Network Error개발자 도구 콘솔에서는 추가로 아래 에러가 확인되었다.Access to XMLHttpRequest at 'http://localhost:9999/cate

ilove-ya.tistory.com

연결 중에 발생했던 오류도 첨부합니다.. 

 

📄 http.ts

import axios, {Axios, AxiosRequestConfig} from 'axios';

const BASE_URL = "http://localhost:9999";
const DEFAULT_TIMEOUT = 30000;

export const createClient = (config?: AxiosRequestConfig) => {
    const axiosInstance = axios.create({
        baseURL: BASE_URL,
        timeout: DEFAULT_TIMEOUT,
        headers: {
            "content-type": "application/json",
        },
        withCredentials: true,
        ...config
    })

    axiosInstance.interceptors.response.use(
        (response) => {
            return response;
        },
        (error) => {
            return Promise.reject(error);
        }
    )
    return axiosInstance;
}

export const httpClient = createClient();

 

카테고리 연결해보자. 

📄  category.api.ts

import { Category } from "../models/category.model";
import { httpClient } from "./http";

export const fetchCategory = async () => {
    const response = await httpClient.get<Category[]>("/category");
    return response.data;
}

 

📄 Header.tsx

function Header() {
    const [category, setCategory] = useState<Category[]>([]);

    useEffect(() => {
        fetchCategory().then((category)=> {
            setCategory(category);
        });
    },[])

    return (
        <HeaderStyle>
            <h1 className="logo">
                <Link to="/"><img src={logo} alt="book store"/></Link>
            </h1> 
            <nav className="category">
                <ul>
                    {
                        category.map((item) => (
                            <li key={item.category_id}>
                                <Link to={item.category_id === null ? `/books` : `/books?category_id=${item.category_id}`}>
                                {item.category_name}</Link>
                            </li>
                        ))
                    }
                </ul>
                
                ....