2024.02.23(금)
src/models/user.model.ts
export interface User {
id: number;
email: string;
password?: string;
}
src/models/book.model.ts
export interface Book {
id: number;
title: string;
imageId: number;
summary: string;
author: string;
price: number;
likes: number;
liked: boolean;
publishedDate: string;
}
export interface BookDetail extends Book {
categoryName: string;
form: string;
isbn: string;
pages: number;
detail: string;
tableOfContents: string;
}
src/models/pagination.model.ts
export interface Pagination {
currentPage: number;
totalCount: number;
}
src/models/category.model.ts
export interface Category {
id: number | null;
name: string;
}
src/models/cart.model.ts
export interface Cart {
itemId: number;
bookId: number;
title: string;
summary: string;
quantity: number;
price: number;
}
src/models/order.model.ts
export interface Order {
orderId: number;
orderedAt: string;
address: string;
recipient: string;
contact: string;
bookTitle: string;
totalQuantity: number;
totalPrice: number;
}
node.js와 브라우저를 위한 Promise 기반 HTTP 클라이언트
XMLHttpRequests
생성http
request 생성application/json
)multipart/form-data
)application/x-www-form-urlencoded
)const {data} = await axios.post('/user', document.querySelector('#my-form'), {
headers: {
'Content-Type': 'application/json'
}
})
npm install axios --save
config
객체
속성 | 설명 | 특징 |
---|---|---|
url | 요청을 보낼 서버의 URL | 상대적인 경로인 경우 baseURL과 함께 사용됨 |
method | 요청 메서드 | default 'get' |
baseURL | URL 앞에 붙일 baseURL | 상대 경로를 사용할 때 유용 |
transformRequest | 요청 데이터를 서버로 보내기 전에 변경할 수 있는 함수 배열 | 'PUT', 'POST', 'PATCH', 'DELETE' 메서드에서만 적용 가능 |
transformResponse | 응답 데이터가 then/catch로 전달되기 전에 변경할 수 있는 함수 배열 | |
headers | 요청 시 보낼 사용자 지정 헤더(custom header) | |
params | 요청과 함께 보낼 URL parameters | 반드시 일반 객체나 URLSearchParams 객체여야 함 null이나 undefined는 URL에 렌더링되지 않음 |
paramsSerializer | params의 직렬화를 담당하는 optional 함수 | |
data | request body로 보낼 데이터 | 'PUT', 'POST', 'PATCH', 'DELETE' 메서드에서만 적용 가능 |
timeout | 요청이 타임아웃되기 전에 대기할 시간 [ms] | default 0 (no timeout) 요청이 timeout보다 오래 걸리면 중단됨 |
withCredentials | cross-site Access-Control 요청에 자격 증명을 사용할지 여부 | default false |
adapter | 요청 처리를 커스터마이징하는 데 사용되는 함수 | lib/adapters/README.md 참고 |
auth | HTTP Basic 인증을 사용하고 해당 자격 증명을 제공 | headers를 사용하여 설정한 기존의 Authorization 사용자 지정 헤더를 덮어씀 Bearer 토큰 등의 경우 Authorization 사용자 지정 헤더를 대신 사용 |
responseType | 서버의 응답으로 받을 데이터 유형 | 옵션: 'arraybuffer', 'document', 'json', 'text', 'stream' 브라우저 전용: 'blob' |
responseEncoding | 응답 디코딩에 사용할 인코딩 | Node.js 전용 client-side 요청 또는 responseType이 'stream'이면 무시됨 |
xsrfCookieName | XSRF 토큰 값으로 사용할 쿠키의 이름 | default 'XSRF-TOKEN' |
xsrfHeaderName | XSRF 토큰 값을 운반하는 HTTP 헤더의 이름 | default 'X-XSRF-TOKEN' |
onUploadProgress | 업로드 진행 상황을 처리하는 함수 | 브라우저 전용 |
onDownloadProgress | 다운로드 진행 상황을 처리하는 함수 | 브라우저 전용 |
maxContentLength | HTTP 응답 본문의 최대 크기 [byte] | Node.js 전용 |
maxBodyLength | HTTP 요청 본문의 최대 크기 [byte] | Node.js 전용 |
validateStatus | HTTP 응답 상태 코드의 유효성을 확인하는 함수 | default Promise가 2XX HTTP 상태코드일 때 resolve, 그 외는 reject |
maxRedirects | 리디렉션 최대값 | default 5 |
socketPath | node.js에서 사용될 UNIX 소켓을 정의 | default null socketPath 또는 proxy만 지정 가능 (둘 다 지정될 경우 socketPath가 사용됨) |
httpAgent | HTTP 요청에 사용되는 사용자 지정 에이전트(custom agent) | Node.js 전용 기본적으로 활성화되지 않은 keepAlive와 같은 옵션 추가 가능 |
httpsAgent | HTTPS 요청에 사용되는 사용자 지정 에이전트(custom agent) | Node.js 전용 기본적으로 활성화되지 않은 keepAlive와 같은 옵션 추가 가능 |
proxy | proxy server의 hostname, port, protocol 정의 | |
cancelToken | 요청을 취소하는 데 사용되는 토큰 | |
decompress | response body의 자동 압축 해제 여부 | default true Node.js 전용 |
validateStatus
를 변경해야 할 것 같다.)response
객체
속성 | 설명 | 특징 |
---|---|---|
data | server에서 제공된 응답 데이터 | |
status | 서버 응답의 HTTP 상태 코드 | |
statusText | 서버 응답의 HTTP 상태 메시지 | |
headers | 서버 응답의 HTTP 헤더 | 모든 헤더 이름은 소문자로 표시 Content-Type 헤더는 response.headers['content-type']으로 접근 가능 |
config | Axios로 요청을 보낼 때 제공된 config 객체 | |
request | 해당 응답을 생성한 요청 정보 | node.js: 마지막 ClientRequest 인스턴스 브라우저: 마지막 XMLHttpRequest 인스턴스 |
Access to XMLHttpRequest at 'http://localhost:7777/api/category' from origin 'http://localhost:3000' has been blocked by CORS policy
protocol
, host
, port
동일npm install cors
// app.js
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:3000',
credentials: true
}));
공통 axios instance 설정
src/api/http.ts
import axios, { AxiosRequestConfig } from 'axios';
const BASE_URL = "http://localhost:7777/api";
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 => response,
error => Promise.reject(error)
);
return axiosInstance;
};
export const httpClient = createClient();
category
src/api/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;
};
src/hooks/useCategory.ts
컴포넌트 간에 stateful logic을 공유할 수 있음! (state 자체를 공유하려면 props 사용) ⇒ 반복되는 stateful logic을 줄일 수 있음
import { useEffect, useState } from "react";
import { Category } from "../models/category.model";
import { fetchCategory } from "../api/category.api";
export const useCategory = () => {
const [category, setCategory] = useState<Category[]>([]);
useEffect(() => {
fetchCategory()
.then(category => {
if (!category) return;
const categoryWithAll = [
{
id: null,
name: "전체"
},
...category
];
setCategory(categoryWithAll);
});
}, []);
return { category };
};
src/components/common/Header.tsx
기존에 category 배열 대신 api에 요청을 보내 category 배열을 받아서 사용
useCategory
라는 Custom Hook을 사용해서 category 배열을 받아옴
import styled from "styled-components";
import logo from "../../assets/images/logo.png"
import { FaSignInAlt, FaRegUser } from "react-icons/fa"
import ThemeSwitcher from "../header/ThemeSwitcher";
import { Link } from "react-router-dom";
import { useCategory } from "../../hooks/useCategory";
function Header() {
const { category } = useCategory();
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>
</nav>
<nav className="auth">
<ul>
<li>
<ThemeSwitcher />
</li>
<li>
<Link to="/login">
<FaSignInAlt />로그인
</Link>
</li>
<li>
<Link to="/signup">
<FaRegUser />회원가입
</Link>
</li>
</ul>
</nav>
</HeaderStyle>
);
}
const HeaderStyle = styled.header`
width: 100%;
margin: 0 auto;
max-width: ${({ theme }) => theme.layout.width.large};
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid ${({ theme }) => theme.color.background};
.logo {
img {
width: 200px;
}
}
.category {
ul {
display: flex;
gap: 32px;
li {
a {
font-size: 1.5rem;
font-weight: 600;
text-decoration: none;
color: ${({ theme }) => theme.color.text};
&:hover {
color: ${({ theme }) => theme.color.primary};
}
}
}
}
}
.auth {
ul {
display: flex;
gap: 16px;
li {
a {
font-size: 1rem;
font-weight: 600;
text-decoration: none;
display: flex;
align-items: center;
line-height: 1;
svg {
margin-right: 6px;
}
}
}
}
}
`;
export default Header;
테스트 메일: react.baby@gmail.com
/ reactbaby
공통 컴포넌트에서 html tag의 속성을 사용할 수 있도록 수정
src/components/common/InputText.tsx
기존 Props
interface가 React.InputHTMLAttributes<HTMLInputElement>
를 extend하도록 수정 → input tag의 속성을 사용할 수 있게 됨!
import React, { ForwardedRef } from "react";
import styled from "styled-components";
interface Props extends React.InputHTMLAttributes<HTMLInputElement> {
placeholder?: string;
inputType?: "text" | "email" | "password" | "number";
}
const InputText = React.forwardRef(({ placeholder, inputType, onChange, ...props }: Props, ref: ForwardedRef<HTMLInputElement>) => {
return (
<InputTextStyle placeholder={placeholder} ref={ref} type={inputType} onChange={onChange} {...props} />
);
});
const InputTextStyle = styled.input`
padding: 0.25rem 0.75rem;
border: 1px solid ${({ theme }) => theme.color.border};
border-radius: ${({ theme }) => theme.borderRadius.default};
font-size: 1rem;
line-height: 1.5;
color: ${({ theme }) => theme.color.text};
`;
export default InputText;
src/components/common/Button.tsx
기존 Props
interface가 React.ButtonHTMLAttributes<HTMLButtonElement>
를 extend하도록 수정 → button tag의 속성을 사용할 수 있게 됨!
import styled from "styled-components";
import { ButtonScheme, ButtonSize } from "../../style/theme";
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
size: ButtonSize;
scheme: ButtonScheme;
disabled?: boolean;
isLoading?: boolean;
}
function Button({ children, size, scheme, disabled, isLoading }: Props) {
return (
<ButtonStyle size={size} scheme={scheme} disabled={disabled} isLoading={isLoading}>
{children}
</ButtonStyle>
);
}
const ButtonStyle = styled.button<Omit<Props, "children">>`
font-size: ${({ theme, size }) => theme.button[size].fontSize};
padding: ${({ theme, size }) => theme.button[size].padding};
color: ${({ theme, scheme }) => theme.buttonScheme[scheme].color};
background-color: ${({ theme, scheme }) => theme.buttonScheme[scheme].backgroundColor};
border: 0;
border-radius: ${({ theme }) => theme.borderRadius.default};
opacity: ${({ disabled }) => disabled ? 0.5 : 1};
pointer-events: ${({ disabled }) => disabled ? "none" : "auto"};
cursor: ${({ disabled }) => disabled ? "none" : "pointer"};
`;
export default Button;
signup
src/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/signup", userData);
return response.data;
};
src/hooks/useAlert.ts
리렌더링 사이에 함수 정의를 캐시할 수 있게 해주는 React Hook
useCallback(fn, dependencies)
fn
: 캐시하려는 함수 값dependencies
가 변경되지 않았다면 동일한 함수를 다시 제공import { useCallback } from "react";
export const useAlert = () => {
const showAlert = useCallback((message: string) => {
window.alert(message);
}, []);
return showAlert;
};
src/pages/Signup.tsx
React Hook Form: npm install react-hook-form --save
const { register, handleSubmit, formState } = useForm();
register
: (name: string, RegisterOptions?) => ({ onChange, onBlur, name, ref })React Hook Form에 input element를 등록하고 validation rule을 적용하기 위한 메서드
name
: component를 Hook에 등록하기 위한 unique keyRegisterOptions
: validation rule(required, min, max 등)을 적용할 수 있음
handleSubmit
: ((data: Object, e?: Event) => Promise, (errors: Object, e?: Event) => void) => Promiseform validation이 성공하면 form data를 받음
formState
: Object전체 form state에 대한 정보를 가지고 있는 객체
useForm
Hook과 useAlert
custom Hook 사용
useNavigate
Hook을 사용해서 회원가입 성공 시 로그인 페이지로 이동
<form>
요소에서 연관된 요소들을 하나의 그룹으로 묶을 때 사용
import { useForm } from "react-hook-form";
import styled from "styled-components";
import Title from "../components/common/Title";
import InputText from "../components/common/InputText";
import Button from "../components/common/Button";
import { Link, useNavigate } from "react-router-dom";
import { useState } from "react";
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 [email, setEmail] = useState("");
// const [password, setPassword] = useState("");
// const handleSubmit = ((event: React.FormEvent<HTMLFormElement>) => {
// event.preventDefault(); // action에 의한 페이지 이동(새로고침) 방지
// });
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>
</>
);
}
export 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;
src/App.tsx
router에 Signup
컴포넌트 추가
import Signup from "./pages/Signup";
const router = createBrowserRouter([
…,
{
path: "/signup",
element: <Layout><Signup /></Layout>
},
]);
src/api/auth.api.ts
resetRequest
와 resetPassword
추가
import { SignupProps } from "../pages/Signup";
import { httpClient } from "./http";
export const signup = async (userData: SignupProps) => {
const response = await httpClient.post("/users/signup", userData);
return response.data;
};
export const resetRequest = async (data: SignupProps) => {
const response = await httpClient.post("/users/reset-password", data);
return response.data;
};
export const resetPassword = async (data: SignupProps) => {
const response = await httpClient.put("/users/reset-password", data);
return response.data;
};
src/pages/ResetPassword.tsx
signup의 style을 그대로 사용
import { useForm } from "react-hook-form";
import Title from "../components/common/Title";
import InputText from "../components/common/InputText";
import Button from "../components/common/Button";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { resetPassword, resetRequest } from "../api/auth.api";
import { useAlert } from "../hooks/useAlert";
import { SignupStyle } from "./Signup";
export interface SignupProps {
email: string;
password: string;
}
function ResetPassword() {
const navigate = useNavigate();
const showAlert = useAlert();
const [resetRequested, setResetRequested] = useState(false);
const {
register,
handleSubmit,
formState: { errors }
} = useForm<SignupProps>();
const onSubmit = (data: SignupProps) => {
if (resetRequested) {
resetPassword(data).then(() => {
showAlert("비밀번호 초기화 완료");
navigate("/login");
});
} else {
resetRequest(data).then(() => {
setResetRequested(true);
});
}
};
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>
{
resetRequested && (
<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">
{resetRequested ? "비밀번호 초기화" : "초기화 요청"}
</Button>
</fieldset>
</form>
</SignupStyle>
</>
);
}
export default ResetPassword;
src/App.tsx
router에 ResetPassword
컴포넌트 추가
import ResetPassword from "./pages/ResetPassword";
const router = createBrowserRouter([
…,
{
path: "/reset",
element: <Layout><ResetPassword /></Layout>
},
]);
npm install zustand --save
src/store/authStore.ts
localStorage 활용
import { create } from "zustand";
interface StoreState {
isLoggedIn: boolean;
storeLogin: (token: string) => void;
storeLogout: () => void;
}
export const getToken = () => {
const token = localStorage.getItem("token");
return token;
};
const setToken = (token: string) => {
localStorage.setItem("token", token);
};
export const removeToken = () => {
localStorage.removeItem("token");
};
export const useAuthStore = create<StoreState>(set => ({
isLoggedIn: Boolean(getToken()),
storeLogin: (token: string) => {
set({isLoggedIn: true});
setToken(token);
},
storeLogout: () => {
set({isLoggedIn: false});
removeToken();
}
}));
src/api/auth.api.ts
LoginResponse
interface와 login
추가
import { SignupProps } from "../pages/Signup";
import { httpClient } from "./http";
⁝
interface LoginResponse {
accessToken: string;
}
export const login = async (userData: SignupProps) => {
const response = await httpClient.post<LoginResponse>("/users/login", userData);
return response.data;
};
src/pages/Login.tsx
signup의 style을 그대로 사용
Zustand로 구성한 store 사용
import { useForm } from "react-hook-form";
import Title from "../components/common/Title";
import InputText from "../components/common/InputText";
import Button from "../components/common/Button";
import { Link, useNavigate } from "react-router-dom";
import { login } from "../api/auth.api";
import { useAlert } from "../hooks/useAlert";
import { SignupStyle } from "./Signup";
import { useAuthStore } from "../store/authStore";
export interface SignupProps {
email: string;
password: string;
}
function Login() {
const navigate = useNavigate();
const showAlert = useAlert();
const { storeLogin } = useAuthStore();
const {
register,
handleSubmit,
formState: { errors }
} = useForm<SignupProps>();
const onSubmit = (data: SignupProps) => {
login(data)
.then((res) => {
storeLogin(res.accessToken);
showAlert("로그인 완료");
navigate("/");
})
.catch((error) => {
showAlert("로그인 실패");
});
};
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>
</>
);
}
export default Login;
src/App.tsx
router에 Login
컴포넌트 추가
import Login from "./pages/Login";
const router = createBrowserRouter([
…,
{
path: "/login",
element: <Layout><Login /></Layout>
},
]);
src/components/common/Header.tsx
Zustand store의 전역 값(isLoggedIn
)에 따라 보여지는 화면이 달라지도록 코드 추가
import styled from "styled-components";
import logo from "../../assets/images/logo.png"
import { FaSignInAlt, FaRegUser, FaShoppingCart, FaTruck, FaSignOutAlt } from "react-icons/fa"
import ThemeSwitcher from "../header/ThemeSwitcher";
import { Link } from "react-router-dom";
import { useCategory } from "../../hooks/useCategory";
import { useAuthStore } from "../../store/authStore";
function Header() {
const { category } = useCategory();
const { isLoggedIn, storeLogout } = useAuthStore();
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>
</nav>
<nav className="auth">
{
isLoggedIn && (
<ul>
<li>
<ThemeSwitcher />
</li>
<li>
<Link to="/cart">
<FaShoppingCart />장바구니
</Link>
</li>
<li>
<Link to="/orderlist">
<FaTruck />주문내역
</Link>
</li>
<li>
<button onClick={storeLogout}>
<FaSignOutAlt />로그아웃
</button>
</li>
</ul>
)
}
{
!isLoggedIn && (
<ul>
<li>
<ThemeSwitcher />
</li>
<li>
<Link to="/login">
<FaSignInAlt />로그인
</Link>
</li>
<li>
<Link to="/signup">
<FaRegUser />회원가입
</Link>
</li>
</ul>
)
}
</nav>
</HeaderStyle>
);
}
const HeaderStyle = styled.header`
width: 100%;
margin: 0 auto;
max-width: ${({ theme }) => theme.layout.width.large};
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid ${({ theme }) => theme.color.background};
.logo {
img {
width: 200px;
}
}
.category {
ul {
display: flex;
gap: 32px;
li {
a {
font-size: 1.5rem;
font-weight: 600;
text-decoration: none;
color: ${({ theme }) => theme.color.text};
&:hover {
color: ${({ theme }) => theme.color.primary};
}
}
}
}
}
.auth {
ul {
display: flex;
gap: 16px;
li {
a, button {
font-size: 1rem;
font-weight: 600;
text-decoration: none;
display: flex;
align-items: center;
line-height: 1;
background: none;
border: 0;
cursor: pointer;
svg {
margin-right: 6px;
}
}
}
}
}
`;
export default Header;
src/api/http.ts
Authorization header를 추가하고 401 error 발생 시 login 화면으로 보내기
import axios, { AxiosRequestConfig } from 'axios';
import { getToken, removeToken } from '../store/authStore';
const BASE_URL = "http://localhost:7777/api";
const DEFAULT_TIMEOUT = 30000;
export const createClient = (config?: AxiosRequestConfig) => {
const axiosInstance = axios.create({
baseURL: BASE_URL,
timeout: DEFAULT_TIMEOUT,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getToken() ?? ""}`
},
withCredentials: true,
...config
});
axiosInstance.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
removeToken();
window.location.href = "/login";
return;
}
Promise.reject(error);
}
);
return axiosInstance;
};
export const httpClient = createClient();
나는 백엔드 스프린트 때 개인적으로 accessToken & refreshToken 방식으로 구현했고, 현재 서버에서 refreshToken은 cookie에 accessToken은 response body에 담아서 보내도록 해두었다. 그래서 원래는 클라이언트에서 인증이 필요한 요청을 보냈을 때 에러(401)가 발생하면
/refresh
api로 요청을 보내 다시 refreshToken을 발급받아 원래 요청을 다시 보내고, 만약/refresh
api 요청에서도 에러(400)가 발생하면 로그인 페이지로 redirect하도록 하는 방식을 생각하고 있었다. 강의에서 짜준 코드처럼 status code만으로는 이런 상황을 구분하기 어려울 것 같고 message까지 확인해서 그에 맞는 로직을 짜야할 것 같다. 서버쪽 Error class 부분을 정리하고 어떻게 구현할지 생각해봐야겠다.