티스토리 뷰

💥 로그인/회원가입/비밀번호 초기화 API

1) 로그인   =>   /login

2) 회원가입    =>   /signup

3) 비밀번호 초기화    =>   /reset

💥 도서/장바구니/주문 API

1) 도서 목록    =>   /books

2) 도서 상세    =>   /books/{id}

3) 장바구니    =>   /cart

4) 주문서 작성    =>   /order

5) 주문 목록    =>   /orderlist

 

이번엔 React Router 라이브러리를 사용해서 라우트를 작성해보려고 한다.

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

 

먼저 npm install을 해준 후, App.tsx 상단에 import를 꼭 해줘야한다.

path는 "/" , "/books"로 나눠놨고,

Layout 안에 Home을 배치한 이유는, 원래 RouterProvider는 Layout의 하위에 있었다.

하지만, 원하는 Header에 넣은 링크는 Layout 하위에 있지 않고 RouterProvider 밖에 존재한다.

그렇기 때문에, Layout도 RouterProvider 안에 있어야하기 때문에 Layout 안에 Home을 배치했다.

 

지금 /login 은 작성해두지 않았기 때문에 오류가 난다.

그래서 오류페이지를 따로 만들어서 errorElement를 따로 지정해주었다.

이렇게 해두면 오류가 발생했을 때, 어떤 오류인 지 또는 왜 오류가 났는 지 모를수도 있기 때문에

어떤 오류인 지에 대해 사용자 및 개발자들이 확인을 할 수 있어 굉장히 중요한 부분이라고 생각한다.

이렇게 "전체" 카테고리를 눌렀을 때,

/books 가 생기면서 home body 라고 적혔던 부분이 "도서 목록" 이라는 텍스트가 나오는 걸 볼 수 있다.

 

💥 모델 정의하기

프로젝트에서 사용할 주요 모델은 아래와 같다.

1) User

2) Book

3) Category

4) Cart

5) Order

 

지난 번에 user.model.ts 만든 것처럼 만들어주면 된다.

 

1) user.model.ts

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

 

 

2) book.model.ts

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

book 모델 같은 경우에는 전체 도서조회 및 도서 상세조회가 나뉘어 있었다.

그래서 extends 를 이용해 BookDetail에만 categoryName과 liked를 추가해주었다.

 

 

2-1) pagination.model.ts

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

페이지 밑에 페이지네이션 부분도 있었기 때문에 pagination 모델도 따로 정의해놓았다.

 

 

3) category.model.ts

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

 

 

4) cart.model.ts

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

 

 

5) order.model.ts

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

 

💥 API 통신 모듈 구성

 - API 요청 플로우

 

View 에서 Hooks에 데이터를 요청하고 중간에 React Query와 같은 Query Library가 존재할 수도 있고,

Fetcher 안에서도 category.api.ts 처럼 특정 모델을 목적으로 하는 Fetcher 함수가 있을 수도 있고 

그 함수를 감싸는 http 클라이언트가 존재한다.

여기서 API server와 통신하고 역으로 데이터를 넘겨주면 View에 렌더하게 된다.

 

이렇게 레이어를 고려해서 설계하면 렌더 영역을 깔끔하게 유지할 수 있는 장점이 있다.

 

Header라는 컴포넌트에서 어떤 데이터가 필요할 때, 데이터를 직접 fetch해서 넣는 것은 렌더 영역을 깔끔하게 유지하기가 힘들다.

그래서 hooks이나 별도로 분리된 http 클라이언트를 사용할 수 있다.

그리고 hooks를 이용해서 중복 코드를 줄이고, hooks 안에서 데이터를 가공하는 로직을 제공할 수 있다.

Fetcher 역시 분리해서 api마다 달라질 수 있는 설정이나 변경사항 등을 대응할 수 있는 장점이 있다.

 

category fetch를 예시로 진행하겠다.

 

먼저 http 공통 모듈 클라이언트를 만들었다.

npm i axios --save 

대중적인 axios 라이브러리 사용했고, 나중에 설정을 위해서 AxiosRequestConfig 라는 타입도 미리 불러놓기 위해,

import axios, { AxiosRequestConfig } from 'axios' 를 상단에 적어주었다.

 

import 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()

BASE_URL은 모든 요청에 베이스로 들어가는 URL이다.

그리고 나중에 timeout을 조정할 일이 생길 땐, DEFAULT_TIMEOUT을 통해서 제공하려고 한다.

 

createClient를 통해서 실제로 클라이언트를 작성하고, 이 작성한 클라이언트를 httpClient 이름으로 export 해서 사용할 예정이다. 

createClient를 할 때, Base Url을 변경한다거나 기존의 config를 오버라이드 할 때 사용한다.

 

axios.create의 인자인 파라미터로 baseURL과 timeout과 headers, withCredentials를 지정해준다.

httpClient에서 createClient 함수를 통해서 axiosInstance를 가지고오면 http 공통 모듈에선 같은 axiosInstance를 쓰게된다.

axiosInstance의 에러상황에선 interceptors를 통해서 response, error 부분을 처리했다.

 

이렇게 만든 httpClient를 사용하는 category API 파일도 따로 만들어준다.

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

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

 - category.api.ts

 

import { useEffect, useState } from "react"
import { fetchCategory } from "../api/category.api"
import { Category } from "../models/category.model"

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

 - UseCategory.ts

 

 - Header.tsx

 

이렇게 category.api.ts, useCategory.ts을 만들고 Header.tsx 파일을 수정해주면 된다.

그럼 이렇게 category가 잘 받아와지는 걸 볼 수 있다.

물론... 그 전에 많은 오류가 있었다 😥

수많은 X만 봐도 보이는 저 오류들... 거의 CORS 오류들이었다.

찾아보니까 생각보다 많은 사람들이 짜증나게 느끼는 오류 중 하나였다 ㅠㅠ..

이것저것 찾아보면서 해봤었는데 계속 고쳐지지 않아서 도대체 뭔가 문제야!! 하고 냅다 집어던지고 1시간 뒤에,

다시 구글링하면서 차근차근 고쳐나가봤는데 드디어 고쳐졌다...

 

CORS는 Cross Origin Resource Sharing 라는 뜻이다.

Origin 이나 도메인이 다른 도메인을 가진 리소스에 액세스 할 수 있게 하는 보안 메커니즘이다.

동일 출처 정책? 때문에 생긴거라고 하는데...  쉽게 말해 프로토콜, 호스트명, 포트가 같아야 한다는 것

만약 다르다? 나처럼 오류 뜨면서 CORS 에러가 무자비하게 뜬다 ㅋㅋ...

 

그래서 이것저것 해보다가 고쳐진 게 'cors' 미들웨어를 사용하는 것이었다😀

웬만해서 다른 거 설치 하지않고 고쳐보려고 이 방법은 빼고 해봤었는데 그냥 이것부터 해볼 걸 그랬다...

이렇게 cors 미들웨어를 사용해서 허용할 도메인을 적고, credentials: true를 적어주면 된다.

이렇게 간단한 걸.. 2시간을 고생하면서 이것저것 만졌었네 ㅠㅠ.. 다음부턴 무조건 미들웨어부터 쓴다^^...

 

💥 회원가입 API

import { Link, useNavigate } from "react-router-dom"
import styled from "styled-components"
import Button from "../components/common/Button"
import InputText from "../components/common/InputText"
import Title from "../components/common/Title"
import { useForm } from "react-hook-form"
import { signup } from "../api/auth.api"
import { useAlert } from "../hooks/useAlert"

export interface SignupProps {
    email: string
    password: string
}

function Signup() {
    const navigate = useNavigate()
    const showAlert = useAlert()

    const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm<SignupProps>()

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

    return (
        <>
            <Title size="large">회원가입</Title>
            <SignupStyle>
                <form onSubmit={handleSubmit(onSubmit)}>
                    <fieldset>
                        <InputText
                            placeholder="이메일"
                            inputType="email"
                            {...register("email", { required: true })}
                        />
                        {errors.email && <p className="error-text">이메일을 입력해주세요.</p>}
                    </fieldset>
                    <fieldset>
                        <InputText
                            placeholder="비밀번호"
                            inputType="password"
                            {...register("password", { required: true })}
                        />
                        {errors.password && <p className="error-text">비밀번호를 입력해주세요.</p>}
                    </fieldset>
                    <fieldset>
                        <Button type="submit" size="medium" scheme="primary">
                            회원가입
                        </Button>
                    </fieldset>
                    <div className="info">
                        <Link to="/reset">비밀번호 초기화</Link>
                    </div>
                </form>
            </SignupStyle>
        </>
    )
}

const SignupStyle = styled.div`
    max-width: ${({ theme }) => theme.layout.width.small};
    margin: 80px auto;

    fieldset {
        border: 0;
        padding: 0 0 8px 0;
        .error-text {
            color: red;
        }
    }

    input {
        width: 100%;
    }

    button {
        width: 100%;
    }

    .info {
        text-align: center;
        padding: 16px 0 0 0;
    }
`

export default Signup

 - Signup.tsx 

 

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
}

 - auth.api.ts

 

import { useCallback } from "react"

export const useAlert = () => {
    const showAlert = useCallback((message: string) => {
        window.alert(message)
    }, [])

    return showAlert
}

 - useAlert.ts

 

회원가입 API는 다행히 강의를 듣는 내내 어렵다고 느낄 만한 부분이 없었다.

단지, 아까처럼 오류가 그득그득 떴었는데.. 이번엔 CORS 가 아닌 400 BAD REQUEST 였다.

400?? 그럼 주소가 문제가 아니라 데이터베이스에 문제가 있나? 싶어서..

워크밴치를 켜서 확인해봤더니, id 항목의 AI에 체크가 안 되어있어서 문제가 생긴거였다 ㅠㅠ..

데이터베이스는 건들지 않았으니 문제가 없을텐데.. 왜 자꾸 콘솔엔 id가 없다고 그러지? 했는데..

예전에 백엔드 때 뭔갈 잘못 건들였었나보다... 그래도 이번엔 오래 안 가서 다행🤣

 

회원가입 API에서 신기했던 건, input이 아닌 fieldset을 사용한다는 것..

{errors.email && <p>이메일을 입력해주세요.</p>} 이 코드를 사용하여

이메일을 치지않았을 때, 빨간글자로 경고를 주는 것.. 이 부분이 좀 신기했다.

백엔드 때, 미리 유효성 검사 정도는 해놨어서 프론트 때는 안하겠지? 했는데..

확실히 백엔드와 좀 다른 부분이어서 꽤나 신기하고 재밌던 코드였다.

 

그리고 navigate를 사용해서, 회원가입이 완료가 되면 login 페이지로 넘어가게 하는 것도 신기했다.

백엔드 때도 뭔갈 했던 것 같은데.. 어떤 걸로 했었는데 기억이 잘 안 난다 ㅠㅠ 큰일이다😥

 

그래도 백엔드 하고나서 프론트를 해서 그런가.. 서로 간의 존중이 필요할 것 같은 느낌적인 느낌이 들었다 ㅋㅋㅋ...

백엔드가 했겠지 뭐~ or 프론트가 하겠지 뭐~ 이런 생각으로 일을 하다간 아마 사무실이 파워냉방 튼 것 마냥 추워질 것 같다...

만약 신입 개발자로 백엔드or프론트쪽으로 가게된다면 일단 백엔드 측에서 얼마나 했는 지.. 어떤 걸 나한테 맡길건지..

그 정도는 미리 파악하고 해야할 것 같은 기분이다... 신입은 알잘딱깔센이 힘드니 알아서 가르쳐주면 더 좋을지..도🙇‍♀️

회원가입 페이지의 결과물 올려놓고 마무리 해야할 것 같다..

오늘 블로그 참 길다.... 그만큼 공부도 오래했다...... 구글링이 젤 오래걸림😏

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함