๐ก Axios ๋ฉ์๋๋ค์ ํ์ ์ค์ ๊ณผ ์ธ์คํด์ค ๊ธฐ๋ณธ ์ค์ ๊ทธ๋ฆฌ๊ณ ํ ํฐ ๋ง๋ฃ ์ ์ฌ๋ฐ๊ธ ๋ก์ง์ ๋ํด ์์ฑํ์์ต๋๋ค.
Axios Type Setting
interface CustomInstance extends AxiosInstance {
get<T>(...params: Parameters<AxiosInstance['get']>): Promise<T>;
delete<T>(...params: Parameters<AxiosInstance['delete']>): Promise<T>;
post<T>(...params: Parameters<AxiosInstance['post']>): Promise<T>;
put<T>(...params: Parameters<AxiosInstance['put']>): Promise<T>;
patch<T>(...params: Parameters<AxiosInstance['patch']>): Promise<T>;
}
Create Axios
์๋ก์ด Axios ์ธ์คํด์ค๋ฅผ client ์ด๋ฆ์ผ๋ก ์์ฑํ๊ณ ํธ์ถ๋๋ URL ์์ ๊ณ ์ ์ ์ผ๋ก ๋ถ๋ BaseURL ์ค์ ํด์ค๋ค. ๋ํ ๋ชจ๋ ์์ฒญ์ ๋ํด ์๊ฒฉ ์ฆ๋ช ์ ํฌํจํ๋ withCredentials ์ต์ ์ ์ผ์ฃผ์ด ์ฟ ํค, HTTP ์ธ์ฆ ํค๋ ์ฆ๋ช ์์ฒญ์ ๋ณด๋ผ ์ ์๊ฒ ๋๋ค.
const BASEURL = ${BASEURL};
const client: CustomInstance = axios.create();
client.defaults.baseURL = BASEURL;
client.defaults.withCredentials = true;
Axios Interceptors
Request Interceptors
์ฟ ํค์ accessToken์ ๊ฐ์ง๊ณ ์๋ค๋ฉด Request Header์ ์ก์ธ์ค ํ ํฐ์ ๋ฃ์ด์ ๋ณด๋ด์ค๋ค. ์๋ค๋ฉด ๋ฐฑ๋จ์์ 401 unauthorized ์๋ฌ๋ฅผ ์๋ตํ ๊ฒ์ด๋ค.
Response Interceptors
Axios Response์ ๋ํด ๋ก๊ทธ๋ฅผ ์ฐ๋๋ค. ์๋ฌ ์ฒ๋ฆฌ๋ ๋ชจ๋ reject ์ํค๊ณ 401 ์๋ฌ๋ ๋ก๊ทธ์์์ ์งํ์ํจ๋ค.
// Request
client.interceptors.request.use(
function (config: InternalAxiosRequestConfig) {
const accessToken = getCookie('accessToken');
if (accessToken) {
config.headers['Authorization'] = 'Bearer ' + accessToken;
}
return config;
},
function (error) {
return Promise.reject(error);
},
);
client.interceptors.response.use(
function (response) {
const url = response.config.url;
console.log(
`%cURL Info : ${url}-------------------------------------`,
'background: #000; color: #bada55',
);
if (response.config.method !== 'get') {
console.log(response.config.data);
}
console.log(response.data);
console.log(
'%c------------------------------------------------------------',
'background: #000; color: #bada55',
);
if (response.data === null) {
return response;
}
return response.data;
},
async error => {
errorLog(error);
if (error.response.status !== 401 || error.config.sent) {
return Promise.reject(error);
}
error.config.sent = true;
return await unAuthProcess(error).catch(() => logout());
},
);
export { client };
Example
client.post("board/update")
.then(res => console.log(res))
.catch(err => console.log(err));
Login Axios with JWT Token
๋ก๊ทธ์ธ ์ ์ฌ์ฉํ Axios ์ธ์คํด์ค๋ฅผ ์๋ก ์์ฑํ์๋ค. ID, PWD๋ฅผ ๋๊ธฐ๋ฉด AccessToken, RefreshToken ์ ๋ฌ์ ๋ฐ๊ณ ์ฟ ํค์ ์ ์ฅ ํ ํ์ํ ๋ ๋ง๋ค ๊บผ๋ด ์ด๋ค.
loginAxiospost('user/login', { userId, password })
loginAxios.interceptors.response.use(
function (response) {
setCookie('accessToken', response.data.accessToken);
setCookie('refreshToken', response.data.refreshToken);
if (response.config.url === '/logout') {
return '';
}
return response.data;
},
error => {
errorLog(error);
return Promise.reject(error);
},
);
export { loginAxios };
Refresh Token ํ ํฐ ์ฌ๋ฐ๊ธ
Access Token ๋ง๋ฃ๊ฐ ๋๋ฉด ๊ธฐ์กด API ์์ฒญ์ ์ ์ฅํด๋๊ณ Refresh Token์ผ๋ก ํ ํฐ ์ฌ๋ฐ๊ธ์ ์งํํ๋ค. ์งํ์ค ๋ค์ด์ค๋ API ์์ฒญ์ ๋ํด์๋ ๊ตฌ๋ ์์คํ ์ ์ด์ฉํด ๋ฐฐ์ด์ ๋ด์๋์๋ค๊ฐ ํ ํฐ ์ฌ๋ฐ๊ธ์ด ์ฑ๊ณตํ๋ฉด ๊ทธ๋ ๋ชจ๋ API ์์ฒญ์ ์คํํ๋ค.
// ํ ํฐ ์ฌ๋ฐ๊ธ ์ค ๋ค์ด์ค๋ API ์์ฒญ์ ๊ตฌ๋
ํ ๋ณ์
let subscribers: Array<() => void> = [];
// ํ ํฐ ์ฌ๋ฐ๊ธ ์ค ๋ค๋ฅธ API๊ฐ ๋ค์ด์ค๋์ง ์๋์ง ํ๋จํ๋ ๋ณ์
let lock = false;
const unAuthProcess = async ({config}: {config: InternalAxiosRequestConfig;})
: Promise<void | AxiosResponse> => {
//ํ ํฐ ๋ง๋ฃ๋ก ์๋ต ๋ชป๋ฐ์ API ์์ฒญ ์ ์ฅ
const originalRequest = <InternalAxiosRequestConfig>config;
// ํ ํฐ ์ฌ๋ฐ๊ธ ์ค ๋ค๋ฅธ API๊ฐ ๋จผ์ ๋ค์ด์๋ค๋ฉด ๋ค์์ ๋ค์ด์จ API ์์ฒญ์ ๊ตฌ๋
if (lock) {
return onSubscribe(() => client(originalRequest));
}
lock = true;
await reissueAccessToken();
// ํ ํฐ ๋ง๋ฃ๋ก ์๋ต ๋ชป๋ฐ์ API ์ฌ์คํ
return client(config);
};
// ์ก์ธ์ค ํ ํฐ ๋ฃ์๋ ์๋ฆฌ์ ๋ฆฌํ๋ ์ฌ ํ ํฐ ๋ฃ์ด์ ์์ฒญ
tokenAxios.interceptors.request.use(
function (config) {
config.headers['Authorization'] = 'Bearer ' + getCookie('refreshToken');
return config;
},
error => {
return Promise.reject(error);
},
);
// ๊ตฌ๋
๋ณ์์ ํ ํฐ ๋ง๋ฃ๋ก ์๋ต ๋ชป๋ฐ์ API ์์ฒญ ๊ตฌ๋
function onSubscribe(cb: () => void) {
subscribers.push(cb);
}
// ํ ํฐ ๋ง๋ฃ๋ก ์๋ต ๋ชป๋ฐ์ API ์์ฒญ ์คํ
function onPublish() {
subscribers.forEach(cb => cb());
}
const reissueAccessToken = async (): Promise<string | void> => {
try {
//๋ฆฌํ๋ ์ฌ ํ ํฐ์ผ๋ก ์ก์ธ์ค ํ ํฐ ์ฌ๋ฐ๊ธ API ์์ฒญ
const { data } = await tokenAxios.post<TokenType>('user/reissue');
lock = false;
onPublish();
subscribers = [];
setCookie('accessToken', data.accessToken);
return data.accessToken;
} catch (e) {
subscribers = [];
// ์คํจ ์ ๋ก๊ทธ์์ ์งํ
logout();
}
};
function logout() {
window.location.href = '/login';
lock = false;
removeCookie('accessToken');
removeCookie('refreshToken');
}
์๋ณธ ์ฝ๋
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { getCookie, removeCookie, setCookie } from './Cookie';
interface CustomInstance extends AxiosInstance {
get<T>(...params: Parameters<AxiosInstance['get']>): Promise<T>;
delete<T>(...params: Parameters<AxiosInstance['delete']>): Promise<T>;
post<T>(...params: Parameters<AxiosInstance['post']>): Promise<T>;
put<T>(...params: Parameters<AxiosInstance['put']>): Promise<T>;
patch<T>(...params: Parameters<AxiosInstance['patch']>): Promise<T>;
}
export interface GetResponse {
responseMessage?: string;
statusCode?: number;
}
const BASEURL = ${BASEURL};
const client: CustomInstance = axios.create();
client.defaults.baseURL = BASEURL;
client.defaults.withCredentials = true;
client.interceptors.request.use(
function (config: InternalAxiosRequestConfig) {
const accessToken = getCookie('accessToken');
if (accessToken) {
config.headers['Authorization'] = 'Bearer ' + accessToken;
}
return config;
},
function (error) {
return Promise.reject(error);
},
);
client.interceptors.response.use(
function (response) {
const url = response.config.url;
console.log(response);
console.log(
`%cURL Info : ${url}-------------------------------------`,
'background: #000; color: #bada55',
);
if (response.config.method !== 'get') {
console.log(response.config.data);
}
console.log(response.data);
console.log(
'%c------------------------------------------------------------',
'background: #000; color: #bada55',
);
if (response.data === null) {
return response;
}
return response.data;
},
async error => {
errorLog(error);
if (error.response.status !== 401 || error.config.sent) {
return Promise.reject(error);
}
error.config.sent = true;
return await unAuthProcess(error).catch(() => logout());
},
);
export { client };
const loginAxios: CustomInstance = axios.create();
loginAxios.defaults.baseURL = BASEURL;
loginAxios.defaults.withCredentials = true;
loginAxios.interceptors.response.use(
function (response) {
setCookie('accessToken', response.data.accessToken);
setCookie('refreshToken', response.data.refreshToken);
if (response.config.url === '/logout') {
return '';
}
return response.data;
},
error => {
errorLog(error);
return Promise.reject(error);
},
);
export { loginAxios };
const errorLog = (error: AxiosError | Error) => {
if (axios.isAxiosError(error)) {
const { message, config, response } = error;
console.log(message);
console.log(config?.url);
console.log(response);
}
if (axios.isAxiosError(error)) {
console.log(
`%cURL Info : ${error?.config?.url}-------------------------------------`,
'background: #000; color: #2CD4A8',
);
if (error.response) {
console.log(error.response);
console.log(
`%cError Code : ${error?.response.status} ${error.response.statusText}\nError Msg : ${error.response.data.responseMessage}`,
'background: #000; color: #2CD4A8',
);
} else if (error.request) {
console.log(error.request.statusText);
console.log(error);
console.log(
`%cError Code : ${error.request.status} ${
error.request.statusText || error.message
}\nError Msg :`,
'background: #000; color: #2CD4A8',
);
}
} else {
console.log(error);
}
};
const tokenAxios: CustomInstance = axios.create();
tokenAxios.defaults.baseURL = BASEURL;
tokenAxios.defaults.withCredentials = true;
let subscribers: Array<() => void> = [];
let lock = false;
tokenAxios.interceptors.request.use(
function (config) {
config.headers['Authorization'] = 'Bearer ' + getCookie('refreshToken');
return config;
},
error => {
return Promise.reject(error);
},
);
function onSubscribe(cb: () => void) {
subscribers.push(cb);
}
function onPublish() {
subscribers.forEach(cb => cb());
}
const unAuthProcess = async ({
config,
}: {
config: InternalAxiosRequestConfig;
}): Promise<void | AxiosResponse> => {
const originalRequest = <InternalAxiosRequestConfig>config;
if (lock) {
return onSubscribe(() => client(originalRequest));
}
lock = true;
await reissueAccessToken();
return client(config);
};
interface TokenType extends GetResponse {
data: { accessToken: string; refreshToken?: string };
}
const reissueAccessToken = async (): Promise<string | void> => {
try {
const { data } = await tokenAxios.post<TokenType>('user/reissue');
lock = false;
onPublish();
subscribers = [];
setCookie('accessToken', data.accessToken);
return data.accessToken;
} catch (e) {
subscribers = [];
logout();
}
};
function logout() {
window.location.href = '/login';
lock = false;
removeCookie('accessToken');
removeCookie('refreshToken');
}
'React > TypeScript' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
React Typescript Interface Tuple Generic (0) | 2024.03.25 |
---|---|
React RichTextEditor Image Upload - reactQuill (0) | 2024.03.18 |
React TypeScript useInfiniteQuery Infinite Scroll (0) | 2024.03.15 |
React TypeScript Zustand Infinite Scroll with IntersectionObserver (0) | 2024.03.14 |
React TypeSciprt - Community Project (0) | 2024.03.13 |