์ด๋ฒ ํฌ์คํธ์์๋ JWT๋ฅผ ์ฌ์ฉํด์ ํ ํฐ ๋ง๋ฃ๋ฅผ ํ๋จํ๊ณ ์ฒ๋ฆฌํ๋ ๊ณผ์
์์ ๊ฒช์ ๊ณ ๋ฏผ๊ณผAxios Interceptor์์ React Hook ๋ฐ Custom Hook์ ์ฌ์ฉ
ํ๊ธฐ ์ํด ๊ณ ๋ฏผํ๋ ๊ณผ์ ์ ๋ด์์ต๋๋ค.
ํด๋น ํ๋ก์ ํธ์์๋ ์ฃผ์ด์ง ๋ฐฑ์๋ api๋ฅผ ์ด์ฉํด์ ์ ์ ๊ธฐ๋ฅ(๋ก๊ทธ์ธ, ํ์๊ฐ์ , ์ ์ ์ ๋ณด์์ , ๋ก๊ทธ์์, ํ์ํํด)์ ๊ตฌํํ์์ต๋๋ค. ์ธ์ฆ์์๋ JWT(Json Web Token)์ ์ฌ์ฉํ๋๋ก ๋์ด ์์์ต๋๋ค.
๋๋ฌธ์ ๋ฐฑ์๋ ๊ฐ๋ฐ์์ ์ํตํ ์ ์๊ณ , ์๋ฒ์ ๋ด๋ถ ๊ตฌํ์ํฉ์ ์ ์ ์๋ ์ํฉ์์ ๋ช๊ฐ์ง ๋ฌธ์ ๊ฐ ๋ฐ์ํ์์ต๋๋ค.
JWT์๋ ๋ง๋ฃ๊ธฐํ์ด ์์ต๋๋ค. ์๋ช
์ด ๋ฌดํํ ํ ํฐ์ ํ์ทจ์ ๋ณด์์ ํฐ ์ํ์ด ๋๊ธฐ๋๋ฌธ์ ๋๋ถ๋ถ์ ํ ํฐ์ ๋ง๋ฃ๊ธฐํ expiration date๋ฅผ payload์ ๋ช
์ํ๊ณ ์๋๋ฐ์. ๋ฌธ์ ๋ ์ด๋ฒ ํ๋ก์ ํธ์์ ์ ๋ฌ๋ฐ์ ํ ํฐ์๋ exp
ํญ๋ชฉ์ด ๋ช
์๋์ด์์ง ์์๋ค๋ ์ ์
๋๋ค.
// ๋์ฝ๋ฉ๋ Jwt์ payload
{
"user": {
"_id": "679252*********8f82",
"email": "l***u@gmail.com"
},
"iat": 17******7 //
}
ํ ํฐ์ด ์ด๋ค ์ ๋ณด๋ฅผ ๋ด๋์ง ๋ณด๊ณ ์ถ๋ค๋ฉด https://jwt.io/ ๋ฅผ ๋ฐฉ๋ฌธํ๊ฑฐ๋
jwt-decode
์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ ๊ฐ๋จํ ๋์ฝ๋ฉํด๋ณผ์ ์์ต๋๋ค.
๋ค์ ๋์์์, ์๋๋ refresh token, access token์ด ์ฃผ์ด์ง๊ฑฐ๋ ํ๋์ ํ ํฐ์ ๋ง๋ฃ๊ธฐ๊ฐ์ด ๋๋ฉด ๋ค์ ์๋ก์ด ํ ํฐ์ ๋ฐ๊ธ๋ฐ์์ผ ํ์ง๋ง ์ฃผ์ด์ง ํ ํฐ์์๋ ๋ง๋ฃ๊ธฐ๊ฐ ์ ๋ณด๋ฅผ ํ์ธํ ์ ์์์ต๋๋ค.
๋ก๊ทธ์ธ ์ดํ์ ํ์ธํ ํ ํฐ์ iat ํค๊ฐ ๊ณผ๊ฑฐ์ธ์ง ๋ฏธ๋์ธ์ง ํ๋จํด์ ๊ณผ๊ฑฐ๋ผ๋ฉด ๋ฐ๊ธ์๊ฐ์ด ๋ง์๊ฑฐ๊ณ , ๋ฏธ๋๋ผ๋ฉด ๋ง๋ฃ์๊ฐ์ผ๋ก ํ์ฉํ๊ณ ์์ ๊ฐ๋ฅ์ฑ์ด์์ต๋๋ค.
๋ฐ๋ผ์ ํ์ธ๊ฒฐ๊ณผ iat๋ ๊ณผ๊ฑฐ์๊ฐ์ผ๋ก ์ฌ๋ฐ๋ฅธ ๋ฐ๊ธ์๊ฐ์ผ๋ก ์ฌ์ฉ๋๊ฒ์ด ๋ง์์ต๋๋ค.
iat๋ unix timestamp ํ์์ผ๋ก ๋์ด ์์ต๋๋ค.
์ด๋ค ์์ฒญ์์ ์ ํจํ ์ธ์ฆ ์๊ฒฉ ์ฆ๋ช
์ด ์์ด ๋ฐ์๋๋ statusCode 401
์๋ต์ค๋ฅ๊ฐ ์ฌ ๊ฒฝ์ฐ ๋๊ฐ์ง ๊ฒฝ์ฐ์ ์๋ก ๋๋์ด์ ํ๋จํ ์ ์์ต๋๋ค.
1๏ธโฃ ์์ฒญ ํค๋์ JWT๊ฐ ์์์๋ ๋ถ๊ตฌํ๊ณ 401์๋ฌ๊ฐ ๋ฐ์ํ๋ค๋ฉด ๋ง๋ฃ๋์๊ฑฐ๋ ์ฌ๋ฐ๋ฅด์ง ์์ ํ ํฐ์ธ ๊ฒฝ์ฐ์ด๋ค.
๋ฐ๋ผ์ ํด๋น ๊ฒฝ์ฐ์๋ ํ๋ก ํธ์์ ๊ด๋ฆฌ์ค์ธ ์ ์ ์ํ๋ฅผ ์ด๊ธฐํํ๊ณ , ์ฟ ํค์ ๋ณด๊ด๋ JWT์ญ์ ํฉ๋๋ค. ์ดํ ํ์ผ๋ก ์ด๋ํ๋ฉฐ ์ ์ ์๊ฒ๋ ๊ฒฝ๊ณ ์ฐฝ ํน์ ์๋์ผ๋ก ์ฌ์ฉ์ ์ ๋ณด๊ฐ ๋ง๋ฃ๋์์์ ์ ์ ํ ์๋ ค์ค์ผ ํฉ๋๋ค.
2๏ธโฃ ์์ฒญ ํค๋์ JWT๊ฐ ์๋ค๋ฉด ๋ก๊ทธ์ธ์ ํ์ง ์๊ณ ์ ์ต์ด์ ๊ทผํ์ ๊ฐ๋ฅ์ฑ์ด ์๋ ๊ฒฝ์ฐ์ ๋๋ค.
ํ์ฌ ํ๋ก์ ํธ์์ JWT๋ฅผ ํค๋์ ํฌํจ์์ผ์ผ ํ๋ ์์ฒญ์ด ์๋ ํ์ด์ง์ ๋ก๊ทธ์์์ํ์์์ ์ ๊ทผ์ ๋ง์๋์ด์ ์ด ๊ฒฝ์ฐ๋ ํฌ๋ฐํ์ง๋ง, ์ผ๋จ ํด๋น ์ํฉ์ด ๋ฐ์ํ๋ฉด ๋ก๊ทธ์ธ์ด ํ์ํ๋ค๋ ์๋ฆผ๊ณผ ํจ๊ป ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋์์ผ์ผ ํฉ๋๋ค.
Axios ์ธํฐ์ ํฐ๋?
Axios๋ JavaScript์์ HTTP ์์ฒญ์ ๋ณด๋ด๊ธฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. Axios์ ์ ์ฉํ ๊ธฐ๋ฅ ์ค ํ๋๋ก ์ธํฐ์ ํฐ๊ฐ ์์ต๋๋ค.
์ธํฐ์ ํฐ๋ฅผ ์ฌ์ฉํ๋ฉด ์์ฒญ์ด๋ ์๋ต์ ๊ฐ๋ก์ฑ์ ์ํ๋ ๋ก์ง์ ์ํํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค๋ฉด, ๋ชจ๋ ์์ฒญ์ ํ ํฐ์ ์ถ๊ฐํ๊ฑฐ๋, ์๋ต ๋ก๊น , ์ค๋ฅ ์ฒ๋ฆฌ ๋ฑ์ ํ ์ ์์ต๋๋ค.
ํด๋น ํ๋ก์ ํธ์์๋ ์ธํฐ์
ํฐ๋ฅผ ํ์ฉํด์ ํค๋์ ํ ํฐ์ ์ฃ๊ฑฐ๋, ์๋ต์๋ฌ ์ํฉ์ ๋ฐ๋ฅธ ์ ์ ํ ์๋ฌ๋ฅผ ํธ๋ค๋งํ๊ณ ์์์ต๋๋ค.
์ด์ 401์๋ฌ๊ฐ ๋ ๊ฒฝ์ฐ๋ฅผ ์ ์๊ตฌ์ฌํญ๋๋ก ์์ฑํ 401์๋ฌ ๋ฐ์์ ํธ์ถ๋๋ ํธ๋ค๋ฌ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๊ทธ๋ฌ๋ ์ด ์ฝ๋๋ ๋์ํ์ง ์์ต๋๋ค.
const handle401Error = originalRequest => {
const dispatch = useDispatch();
const navigate = useNavigate();
// iat๊ฐ ํ ํฐ ๋ฐ๊ธ์๊ฐ์ผ๋ก ์ฌ์ฉ๋๋ ๊ฒ์ด ๋ง๋ค๊ณ ํ์ธ๋จ.
// ๋ง๋ฃ์๊ฐ์ ์๋์ผ๋ก ํ์ธํ๋ค.
// 1. ์๋ฌ๊ฐ ๋ ์์ฒญ์ jwt๊ฐ ์์๋์ง ํ์ธ
const hasToken = Boolean(originalRequest.header['Authorization']);
if (hasToken) {
// ์์์ผ๋ฉด ํ ํฐ ๋ง๋ฃ ์ค๋ฅ๋ก ํ๋จ. -> ์ฟ ํค์ ์ ์ ์ํ๋ฅผ ์ง์ฐ๊ณ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋
dispatch(deleteUser()); // ๋ฆฌ๋์ค์ ์ ์ ์ ๋ณด ์ญ์
removeCookie('jwt'); // ํ ํฐ ์ญ์
alert('์ฌ์ฉ์ ์ ๋ณด๊ฐ ๋ง๋ฃ๋์์ต๋๋ค.๋ก๊ทธ์ธํ์ด์ง๋ก ์ด๋ํฉ๋๋ค.');
navigate(PATH.auth);
} else {
// ์์์ผ๋ฉด ํ ํฐ ์์ด ์์ฒญ๋ณด๋ธ ๊ฒ์ด๋ฏ๋ก -> ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋
alert('์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํฉ๋๋ค.');
navigate(PATH.auth);
}
};
useDispatch, useNavigate๊ณผ ๊ฐ์ ๋ฆฌ์กํธ ํ ์ ํจ์ ์ปดํฌ๋ํธ ํน์ ์ปค์คํ ํ ๋ด๋ถ์์๋ง ํธ์ถํ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
๋ฐ๋ผ์ Axios Interceptor์์ฒด๋ฅผ ์ปค์คํ
ํ
์ผ๋ก ๋ง๋ค์ด ๋ด๋ถ์์ Redux์ํ ๋ณ๊ฒฝ์ ์ํ ํ
์ ์ฌ์ฉํ ์ ์๋๋ก ํ๊ฒ ์ต๋๋ค.
๋ํ ํด๋น ํ๋ก์ ํธ์์๋ ์ ๊ณต๋ ๋ฐ๋ธ์๋ฒ ๋ฟ ์๋๋ผ Open Api, supabase๋ฑ ๋ค์ํ ์๋ฒ ๋๋ฉ์ธ์ ์ฌ์ฉํ๊ณ ์์ต๋๋ค.
๋๋ฌธ์ ์ปค์คํ
ํ
์ผ๋ก ๋ง๋ค์ด ๋๋ฉด ๊ฐ ์ธ์คํด์ค๋ง๋ค API ์์ฒญ๊ณผ ์๋ต์ ๋ฐ๋ณต์ ์ธ ์ฝ๋์์ด ๊ฐํธํ๊ฒ ์ ์ดํ ์ ์์ ๊ฒ์ด๋ผ ์๊ฐํ์ต๋๋ค.
const useAxiosInterceptor = instance => {
const dispatch = useDispatch();
const onRequest = config => {
const jwt = getCookie('jwt');
if (jwt) {
config.headers.Authorization = `bearer ${jwt}`;
}
return config;
};
const onErrorRequest = error => {
return Promise.reject(error);
};
const onError = (status, message, errorDetail) => {
const error = { status, message, errorDetail };
throw error;
};
const handle401Error = async originalRequest => {
// iat๊ฐ ํ ํฐ ๋ฐ๊ธ์๊ฐ์ผ๋ก ์ฌ์ฉ๋๋ ๊ฒ์ด ๋ง๋ค๊ณ ํ์ธ๋จ.
// ๋ง๋ฃ์๊ฐ์ ์๋์ผ๋ก ํ์ธํ๋ค.
// 1. ์๋ฌ๊ฐ ๋ ์์ฒญ์ jwt๊ฐ ์์๋์ง ํ์ธ
const hasToken = originalRequest.headers['Authorization'].split(' ').at(1);
if (hasToken) {
// ์์์ผ๋ฉด ํ ํฐ ๋ง๋ฃ ์ค๋ฅ๋ก ํ๋จ. -> ์ฟ ํค์ ์ ์ ์ํ๋ฅผ ์ง์ฐ๊ณ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋
alert('์ฌ์ฉ์ ์ ๋ณด๊ฐ ๋ง๋ฃ๋์์ต๋๋ค.ํ์ผ๋ก ์ด๋ํฉ๋๋ค.');
await dispatch(deleteUser());
removeCookie('jwt');
window.location.href = PATH.root;
} else {
// ์์์ผ๋ฉด ํ ํฐ ์์ด ์์ฒญ๋ณด๋ธ ๊ฒ์ด๋ฏ๋ก -> ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋
alert('์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํฉ๋๋ค.');
await dispatch(deleteUser());
removeCookie('jwt');
window.location.href = PATH.auth;
}
};
const onResponse = response => response;
const onErrorResponse = error => {
if (axios.isAxiosError(error)) {
const originalRequest = error.config;
const { method, url } = originalRequest;
const { data: message, status: statusCode, statusText } = error.response;
console.log(
`[API] ${method.toUpperCase()} ${url} | ERROR ${statusCode} ${statusText} | ${message}`,
);
switch (statusCode) {
case 400: {
onError(statusCode, '์๋ชป๋ ์์ฒญ์
๋๋ค.', message);
break;
}
case 401: {
handle401Error(originalRequest);
break;
}
case 403: {
onError(statusCode, '๊ถํ์ด ์์ต๋๋ค.', message);
break;
}
case 404: {
onError(statusCode, '์ฐพ์ ์ ์๋ ํ์ด์ง ์
๋๋ค.', message);
break;
}
case 500: {
onError(statusCode, '์๋ฒ ์ค๋ฅ์
๋๋ค.', message);
break;
}
default: {
onError(statusCode, '์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค.', message);
}
}
} else if (error && error.name === 'TimeoutError') {
console.log(`[API] | Timeout ERROR ${error.toString()}`);
onError(0, '์์ฒญ์๊ฐ์ด ์ด๊ณผ๋์์ต๋๋ค.', '');
} else {
console.log(`[API] | ERROR ${error.toString()}`);
onError(0, '์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค.', '');
}
return Promise.reject(error);
};
// interceptor ์ถ๊ฐ
const setupInterceptors = () => {
const requestInterceptor = instance.interceptors.request.use(onRequest, onErrorRequest);
const responseInterceptor = instance.interceptors.response.use(onResponse, onErrorResponse);
return { requestInterceptor, responseInterceptor };
};
// interceptor ์ ๊ฑฐ
const ejectInterceptors = (requestInterceptor, responseInterceptor) => {
instance.interceptors.request.eject(requestInterceptor);
instance.interceptors.response.eject(responseInterceptor);
};
useEffect(() => {
const { requestInterceptor, responseInterceptor } = setupInterceptors();
return () => {
ejectInterceptors(requestInterceptor, responseInterceptor);
};
}, []);
};
export default useAxiosInterceptor;
๐ axios interceptor
instance.interceptors.request.use(์์ฒญ ์ ๋ฌ ์ ์์ , ์์ฒญ ์ค๋ฅ์ ์์ )
instance.interceptors.response.use(200๋ฒ๋ ์ํ์ฝ๋์ ํธ๋ฆฌ๊ฑฐ๋๋ ์์ , ์๋ต ์ค๋ฅ๊ฐ ์์๋ ์์ )
instance.interceptors.request.eject()
: ์ธํฐ์ ํฐ ์ ๊ฑฐ
function App() {
useAxiosInterceptor(devAPI);
return <RouterProvider router={router} />;
}
export default App;
์ฑ์ ์ต์๋จ์์ ์์น์์ผ์ฃผ์์ต๋๋ค. ์ด์จ๋ useAxiosInterceptor๊ฐ 'react-router'์ธ๋ถ์์ ํธ์ถ๋๊ณ ์๊ธฐ๋๋ฌธ์ ๊ธฐ์กด์ useNavigator์ ๊ฐ์ ํ ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
๐๋ฐ๋ผ์ window.location.href๋ก ์ด๋์์ผ์ฃผ์ด์ผ ํฉ๋๋ค. ๋ค๋ง useDispatch๊ฐ redux-persist์ ํจ๊ป ์ฌ์ฉ๋์ด ๋น๋๊ธฐ์ ์ผ๋ก ์ํ๋ฅผ ์ ๋ฐ์ดํธ ํ๊ธฐ ๋๋ฌธ์ async-await ๊ตฌ๋ฌธ์ ์ฌ์ฉํด์ ์ํ๊ฐ ์์ ํ ์ ๋ฐ์ดํธ ๋๊ณ ๋๋ค window.location.href๋ก ๋ฆฌ๋๋ ์ ์ด ์ด๋ฃจ์ด์ ธ์ผ ํฉ๋๋ค.