프로젝트/BookStore 사이트

1126 라우트, 회원가입

thinktank911 2025. 11. 26. 23:44

라우트 작성

1. 로그인 /login
2. 회원가입 /signup
3. 비밀번호 초기화 /reset
4. 도서목록 /books
5. 도서상세 /books/{id}
6. 장바구니 /cart
7. 주문서 작성 /order
8. 주문 목록 /orderlist

 

리액트 라우터 설치

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

App.tsx

  • createBrowserRouter 모듈 호출
    • 라우터 생성 : 라우터의 세부 내용, 경로 지정
      import { createBrowserRouter } from 'react-router-dom';
      
      const router = createBrowserRouter([
        {
          path: '/',
          element: <Home />,
        },
        {
            path: '/books',
            element: <div>도서 목록</div>,
        },
      ]);
  • RouterProvider : 라우터를 화면 렌더링
<Layout>
	<RouterProvider router={router} />
</Layout>

 

  • 공통 에러 페이지 작성
    • 에러가 나면 라우터 정보 안에 errorElement를 띄워준다. 
{
    path: '/',
    element: <Home />,
    errorElement: <div>페이지를 찾을 수 없습니다.</div>,
},
    • 공통의 에러 컴포넌트를 만들어 배치시켜주려 한다.

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

export default Error
  • errorElement에 컴포넌트를 배치시켜준다.
      element: <Home />,
      // errorElement: <div>페이지를 찾을 수 없습니다.</div>,
      errorElement: <Error />,
    },

<a>태그 <Link>로 변경

페이지 이동 시 화면 깜박임이 보인다면 아직 제대로 라우팅이 안 이루어지고 있다는 증거

  • Header.tsx에 <a> 태그를 react-router-dom에서 제공하는 <Link>로 변경

[오류발생]

Cannot destructure property 'basename' of 
'react__WEBPACK_IMPORTED_MODULE_0__.useContext(...)' as it is null.
TypeError: Cannot destructure property 'basename' of 
'react__WEBPACK_IMPORTED_MODULE_0__.useContext(...)' as it is null.
  • 원인 : 라우터 적용이 layout의 children 요소에 적용되어 있는데 Header는 Layout에 있기 때문
  • 해결 : 라우터가 Layout을 포함하도록 추가
// 변경 전
<BookStoreThemeProvider>
  <Layout>
    <RouterProvider router={router} />
  </Layout>
</BookStoreThemeProvider>


// 변경 후
const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout>
                <Home />
             </Layout>,
    // errorElement: <div>페이지를 찾을 수 없습니다.</div>,
    errorElement: <Error />,
  },
  {
      path: '/books',
      element: <Layout>
                  <div>도서 목록</div>
               </Layout>,
  },
  {
      path: '/signup',
      element: <Layout>
                  <Signup />
               </Layout>,
  },
]);

<BookStoreThemeProvider>
    <RouterProvider router={router} />
</BookStoreThemeProvider>

 

프로젝트 모델 타입 정의

주요모델

- User
- Book
- Category
- Cart
- Order

 

book 모델 안에서 bookDetail 확장하기

export interface Book {
    id: number;
    title: string;
    img: number;
    category_id: number;
    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;
}

 

API 통신과 데이터 레이어

데이터 흐름



http 공통 모듈 클라이언트 작성

  • axios 설치 : npm i axios --save

http.ts

import axios, { AxiosRequestConfig } from "axios";

// 모든 요청에 Base로 들어가는 url
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();

1️⃣ Axios 라이브러리

HTTP 통신을 간편하게 하기 위해 사용하는 라이브러리이다.
기본 fetch API보다 사용성이 좋으며, 요청/응답 인터셉터, 기본 설정, 오류 처리 등을 보다 체계적으로 관리할 수 있다는 장점이 있다.

 

2️⃣ AxiosRequestConfig 타입

Axios에서 전달 가능한 설정들을 타입으로 관리하는 객체이다.
TypeScript 환경에서는 요청 설정에 대한 자동완성과 타입 안정성을 제공하므로 유지보수에 유리하다.

 

3️⃣ axios.create()

Axios 인스턴스를 생성하는 API이다.
공통으로 사용하는 baseURL, timeout, headers 등을 한곳에 설정할 수 있어, 여러 API 요청 코드에서 중복 작성을 방지하고 관리 효율이 올라간다.
프로젝트 규모가 커질수록 axios 인스턴스 구조가 필수적이 된다.

 

4️⃣ BaseURL

모든 요청 URL 앞에 공통으로 붙는 기본 주소이다.
서버 주소가 바뀌어도 한 곳에서만 수정하면 전체 API가 대응된다는 점에서 유지보수가 매우 편리해진다.

 

5️⃣ Default Timeout

요청이 일정 시간 안에 완료되지 않을 경우 자동으로 취소시키는 기능이다.
네트워크 장애나 무한 대기 방지를 위해 필수적이다.

 

6️⃣ withCredentials

쿠키 기반 세션 로그인이 필요한 서비스에서 사용하는 옵션이다.
CORS 환경에서 프론트엔드 → 백엔드로 요청할 때 인증 쿠키를 포함하려면 반드시 true로 설정해야 한다.

 

7️⃣ Response Interceptor ( 응답 인터셉터)

서버로부터 응답을 받은 직후, then/catch로 넘어가기 전에 실행되는 처리 로직이다.
즉, 서버 응답이 프론트엔드 코드로 전달되기 전에 가공하거나 공통으로 처리할 내용이 있다면 여기에 작성한다.

 

8️⃣ Error Handling (Promise.reject)

인터셉터에서 오류를 처리할 때 Promise.reject(error)를 사용하면 이후 catch 문에서 에러를 일관성 있게 처리할 수 있다.
이 패턴은 전역 에러 처리, 알림 팝업, 에러 코드별 대응 등과 연결된다.

 

만든 httpClient 사용법 - 조회

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에 적용

  • useState() 훅으로 카테고리 데이터 가져와서 CATEGORY 상수 대체
    const [category, setcategory] = useState<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.id}>
    						<Link to={item.id === null ? `/books` : 
    							`/books?category_id=${item.id}`}>
    							{item.name}
    						</Link>
    					</li>
    				))}
    			</ul>

 

  • 커스텀 훅 useCategory() 만들어서 적용 : 재사용성 증가
  • 전체 카테고리 null로 추가
export const useCategory = () =>{
    const [category, setCategory] = useState<Category[]>([]);
    
    // 첫 렌더링 시 카테고리 데이터 가져오기
    useEffect(() => {
        fetchCategory().then((category) => {

            if(!category) return;
            const categoryWithAll = [
                { category_id: null, category_name: "전체" },
                ...category,
            ];

            setCategory(categoryWithAll);
        });
    }, []);

    return { category };
}

 

[이슈]

  • 문제발생 : 카테고리 데이터 조회 시 Network Error 발생
Network Error AxiosError:
Network Error at XMLHttpRequest.handleError (http://localhost:3000/static/js/bundle.js:1096:19)
at Axios.request (http://localhost:3000/static/js/bundle.js:1547:41)
at async fetchCategory (http://localhost:3000/static/js/bundle.js:72182:20)
  • 원인파악 : 다른 폴더에서 진행한 백엔드 서버를 안 켜서 그런 것.
  • 해결방안 : 백엔드 폴더에서 서버 실행 후 다른 실행 포트 허용 위해 proxy/cors 체크하기
    • 방법 A — 백엔드에 CORS 허용 : localhost:3000을 허용해주기
const express = require("express");
const cors = require("cors");
const app = express();

app.use(cors({
origin: "http://localhost:3000", // 허용할 프론트 주소
credentials: true // 쿠키/세션 허용 필요시
}));

app.use(express.json());
  • 해결방안 
    • 방법 B — CRA에서 proxy 설정 : 프론트 package.json에 추가 / axios 요청 시 절대 경로 제거
// 프론트 package.json에 추가
"proxy": "http://localhost:9999"

// axios 요청 시 절대 경로 제거
axios.get("/category");

 

[이슈2]

  • 문제발생 : 수동 추가한 전체만 보이고 나머지 데이터가 안 보임
  • 원인파악 : 데이터 구조 mismatch. 백엔드에서 category_id, category_name으로 데이터를 넘겨주고 있었고,
    프론트에서는 id, name으로 받고 있었다.
  • 해결 : 데이터 항목 형식을 맞추니 해결되었다.

회원가입

InputTextStyle props로 inputType과 onChange 넘기기

  • input의 타입을 동적으로 받기 위해
  • useState로 아이디, 비밀번호 값 변화 onChange에 셋팅
  • props 확장하기
    • extends React.InputHTMLAttributes<HTMLInputElement> 사용 이유
    • <input /> 태그 안 여러 속성 자동으로 Props 포함하기 위해(value/onChange/disabled/required 등)
interface Props extends React.InputHTMLAttributes<HTMLInputElement> {
    placeholder?: string;
    inputType?: "text" | "password" | "email" | "number";
}

 

  • submit 함수 작성
function Signup() {

    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault(); // form submit 시 action에 의해 페이지 이동 막음
        console.log(email, password);
    }

  return (
    <>
        <Title size="large">회원가입</Title>
        <SignupStyle>
            <form onSubmit={handleSubmit}>
                <fieldset>
                    <InputText  placeholder="이메일"
                        inputType="email" value={email}
                        onChange={(e) => setEmail(e.target.value)}
                    />
                </fieldset>
                <fieldset>
                    <InputText  placeholder="비밀번호"
                        inputType="password" value={password}
                        onChange={(e) => setPassword(e.target.value)}
                    />
                </fieldset>
                <fieldset>
                    <Button type="submit" size="large" scheme="primary">
                        회원가입
                    </Button>
                </fieldset>
                <div className="info">
                    <Link to="/reset">비밀번호 초기화</Link>
                </div>
            </form>
        </SignupStyle>
    </>
    
  );
}

 


폼 상태관리 라이브러리 사용


react-hook-form

  • 참조 링크 : https://react-hook-form.com/
  • 리액트 폼 상태관리와 validation에 특화된 라이브러리
  • 설치 : npm install react-hook-form


useForm

  • 폼을 관리하는 커스텀 훅
    • useState와 submit 함수를 하나의 훅에서 관리
    • value와 onChange를 하나의 register에서 관리
    • error 메세지 띄워주기


Signup.tsx  

// const [email, setEmail] = useState("");
// const [password, setPassword] = useState("");

// const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
//     e.preventDefault(); // form submit 시 action에 의해 페이지 이동 막음
//     console.log(email, password);
// }

// react-hook-form 이용해 form 관리 
const { register, 
		handleSubmit, 
		formState: { errors}
	  } = useForm<SignupProps>();
	  

<InputText  placeholder="이메일"
	// inputType="email" value={email}
	// onChange={(e) => setEmail(e.target.value)}
	inputType="email" 
	{...register("email", {required: true})}    // "필드명", 필수 여부
/>
{errors.email && <p className="error-text">이메일을 입력해주세요.</p>}

 

설명 링크 참조

2025.11.27 - [프레임워크\라이브러리/React] - [라이브러리] React Hook Form

 

[라이브러리] React Hook Form

// const [email, setEmail] = useState("");// const [password, setPassword] = useState("");// const handleSubmit = (e: React.FormEvent) => {// e.preventDefault(); // form submit 시 action에 의해 페이지 이동 막음// console.log(email, password);//

thinktank911.tistory.com

 

 

회원가입 api 함수 생성

auth.api.ts

import { SignupProps } from "../pages/Signup";
import { httpClient } from "./http";

export const signup = async(userData:SignupProps) => {
    const response = await httpClient.post("/users/join",
        userData);
    return response.data;
}

 

Signup.tsx

const navigate = useNavigate();

const onSubmit = (data: SignupProps) => {
	signup(data).then((res) => {
		// 성공
		window.alert('회원가입이 완료되었습니다.');
		navigate('/login');
	});
}

 

  • 회원가입 시 홈으로 이동하도록 navigate 추가

alert 커스텀하기

useCallback 사용해 커스텀 훅 만들기

※ useCallback
React에서 함수 재생성을 방지하기 위해 사용하는 Hook 이다.
컴포넌트가 리렌더링될 때마다 내부에서 선언된 함수들도 다시 만들어지는데, 불필요하게 함수가 새로 생성되면 성능 저하 + 자식 컴포넌트에 props로 전달될 때 불필요한 리렌더링 발생 등의 문제가 생길 수 있다.
그래서 useCallback 으로 함수를 메모이제이션(memoization) 해서의존성(dependency)이 변하지 않는 한 같은 함수 인스턴스를 재사용하도록 보장한다.

useCallback 사용 목적
컴포넌트가 리렌더링될 때 함수 인스턴스를 재사용해서 성능 최적화하고,
특히 자식 컴포넌트의 불필요한 렌더링을 막기 위해.

 


useAlert.ts

import { useCallback } from "react"

export const useAlert = () => {
    // useCallback : 렌더링이 반복될 때마다 
    // 함수가 새로 생성되는 것을 방지하기 위해 
    // 함수를 '기억(memoization)'하는 데 사용
    const showAlert = useCallback((message: string) => {
        window.alert(message);
    }, []);

    return showAlert;
};

 

Signup.tsx

const showAlert = useAlert();

// 성공
// window.alert('회원가입이 완료되었습니다.');
showAlert('회원가입이 완료되었습니다.');