ํ ํฐ์ด ๋ง๋ฃ๋์์ ๊ฒฝ์ฐ, ํ ํฐ์ ์ฌ๋ฐ๊ธํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐ ์ค ์๋์ ๊ฐ์ด reissue api๋ฅผ ์ฐ์์ ์ผ๋ก ํธ์ถํ๋ ์ค๋ฅ ์ํฉ์ ๋ง์ฃผํ์๋ค.
๋ด๊ฐ ์์ฑํ ๊ธฐ์กด ๋ก์ง์ ๋ค์๊ณผ ๊ฐ๋ค.
1. axios์ interceptors๋ฅผ ํตํ์ฌ ํ ํฐ ๋ง๋ฃ ์ค๋ฅ๋ฅผ ๊ฐ์งํ๋ค.
2. accessToken์ ํค๋์, refreshToken์ body์ ๋ฃ์ด reissue api๋ฅผ ํธ์ถํ๋ค.
a. ์์ฒญ์ด ์ฑ๊ณต์ ์ผ๋ก ์ฒ๋ฆฌ๋ ๊ฒฝ์ฐ, ์ฌ๋ฐ๊ธ๋ ํ ํฐ์ localStorage์ ์ ์ฅํ๋ค.
b. ๊ทธ๋ ์ง ์์ ๊ฒฝ์ฐ, ๋ค์ ๋ก๊ทธ์ธํ๋ผ๋ ๋ชจ๋ฌ์ ํ์ํ๋ฉฐ, ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํ๊ฒ ํ๋ค.
๋ฌธ์ ์์ธ ํ์
๋คํธ์ํฌ ํญ์ ๋ค์ด๊ฐ ์์ฒญ์ ์์ธํ ์ดํด๋ดค๋ค.
๊ฐ์ฅ ๋จผ์ ๋ฉ์ธ ํ์ด์ง์์ ํ ํฐ์ด ํ์ํ API์ธ user/ticket ์์ฒญ์์ ์๋ฌ๊ฐ ๋ฐ์ํ์๋ค.
๋ณด๋ค์ํผ ํ ํฐ ๋ง๋ฃ์ ๋ฐ๋ฅธ ์ค๋ฅ์์ ์ ์ ์๋ค.
์์์ ํ ํฐ ๋ง๋ฃ ์๋ฌ๊ฐ ๋ฐ์ํ์ผ๋ฏ๋ก user/reissue api๊ฐ ํธ์ถ๋๋ ๊ฒ๊น์ง๋ ์ ์์ ์ด๋ค.
reissue ์์ฒญ์ ํ์ธํด๋ณด๋ Authorization๋ ์ ๋ฃ์๊ณ , payload์๋ ๋ฌธ์ ์๋๋ฐ 401 Unauthorized ์ํ ์ฝ๋๊ฐ ๋ฌ๋ค.
401 ์ค๋ฅ๋ผ๋ฉด ํ ํฐ ๊ด๋ จ ์ค๋ฅ์ธ๋ฐ ์ด๋ฏธ ๋ง๋ฃ๋ acesssToken์ ํค๋์ ๋ฃ์ผ๋ฉด ์ ๋๋ ๊ฒ์ธ๊ฐ?
๋ฐฑ์๋ ํ์์๊ฒ ํ ํฐ ๋ง๋ฃ ์๊ฐ์ 60์ด๋ก ์ค์ ํด๋ฌ๋ผ๊ณ ๋ถํํ ํ ํ ์คํธํด๋ณด๋ ๋ฐ๋ก ๋ฐ๊ธ๋ access ํ ํฐ์ ํค๋์ ๋ด์ ์์ฒญ์ ๋ณด๋ด๋ฉด ์๋ง๊ฒ ์๋ต์ด ์ค๊ณ , ๋ง๋ฃ๋ ํ ํฐ์ ๋ด์ ๊ฒฝ์ฐ์๋ 401 ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
์ด๋ก์จ 401 ํ ํฐ ๋ง๋ฃ ์ค๋ฅ๋ก ํ ํฐ ์ฌ๋ฐ๊ธ api๋ฅผ ํธ์ถํ๋๋ฐ, ํ ํฐ ์ฌ๋ฐ๊ธ ์์ฒญ์ ๋ํ์ฌ ๋ค์ 401 ์ค๋ฅ๊ฐ ๋ฐ์ํ์ฌ ์์ฒญ์ด ๋ฌดํ ๋ฃจํ๋ก ์ด๋ฃจ์ก์์ ์ ์ ์๋ค.
ํ์ธ ์ฐจ ๋ฐฑ์๋ ํ์์๊ฒ ๋ค์ ๋ฌผ์ด๋ณด๋, ํ ํฐ์ด ๋ง๋ฃ๋๊ณ ๋์ ํ ํฐ์ ์ฌ๋ฐ๊ธํ๋ ๊ฒ์ด ์๋๋ผ, ํ ํฐ์ด ๋ง๋ฃ๋๊ธฐ ์ ์ ๋จผ์ ํ ํฐ์ ์ฌ๋ฐ๊ธํด์ผ ํ๋ ๊ฒ์ด์๋ค. (ํ ํฐ ์ฌ๋ฐ๊ธ ๊ณผ์ ์ ๊ดํ ์ํต์ ์ค๋ฅ๊ฐ ์์๋ ๊ฒ์ด๋ค.)
๊ตฌํ
๊ทธ๋ ๋ค๋ฉด !
ํ ํฐ ๋ฐ๊ธ ์๊ฐ์ ์ธก์ ํ ํ, ๋ง๋ฃ ์๊ฐ ์ ์ ์๋์ผ๋ก ์ฌ๋ฐ๊ธํ ์ ์๋๋ก ํ๋ ๋ก์ง์ ์์ฑํด์ผ ํ๋ค.
๊ฐ๋จํ๊ฒ ๋ก์ง์ ์์ฑํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
1. ๋ก๊ทธ์ธ ์setTimeout()์ ํตํ์ฌ ํ ํฐ ๋ง๋ฃ ํ์ด๋จธ๋ฅผ ์๋ํ๋ค.
2. ์ฃผ์ด์ง ์๊ฐ์ด ์ง๋๋ฉด reissue api๋ฅผ ํธ์ถํ์ฌ ํ ํฐ์ ์ฌ๋ฐ๊ธํ๋ค.
๊ทธ๋ ๋ค๋ฉด ์๋ก๊ณ ์นจ์ ํ์ ๊ฒฝ์ฐ์๋ ์ด๋ป๊ฒ ํด์ผ ํ ๊น?
์๋ก๊ณ ์นจ์ ํ๋ฉด ํ์ด๋จธ๊ฐ ์ด๊ธฐํ๋๋ค.
๊ธฐ์กด ํ์ด๋จธ์ ์์ฌ ์๊ฐ(ms)์ ํ ํฐ ๋ฐ๊ธ ์ผ์๋ก๋ถํฐ ๊ฒฝ๊ณผ๋ ์๊ฐ (=ํ ํฐ ๋ฐ๊ธ ์ผ์์ ํ์ฌ์ ์ฐจ์ด, ms)์ ํ ํฐ ๋ง๋ฃ ์๊ฐ์์ ๋บ ๊ฐ์ด๋ค.
ํ ํฐ ๋ฐ๊ธ ์ผ์๋ localStorage์ ์ ์ฅํด์ผ ํ๋ค.REMAIN_TIME(ms) = (TOKEN_EXPIRED_TIME- (TOKEN_ISSUED_DATE-CURRENT_DATE))
1. ๋ก๊ทธ์ธ ์ ํ ํฐ ๋ฐ๊ธ ์๊ฐ์ธtokenDate๋ฅผ localStorage์ ์ ์ฅํ๋ค.
2. ๊ธฐ์กด ํ์ด๋จธ์ ์์ฌ ์๊ฐ(REMAIN_TIME)์ ๊ตฌํ ํ, setTimeout ์๊ฐ์ผ๋ก ์ค์ ํ๋ค.
setTimeout์ ์ธ์ , ์ด๋์ ์คํํด์ผ ํ ๊น?
์ด ํ๋ก์ ํธ์ ๊ฒฝ์ฐ,setTimeout์ ์๊ฐ์ด ์ข ๋ฃ๋ ํ ์ํ๋๋ ํ ํฐ ๋ง๋ฃ ๋ก์ง์์ useNavigate hook์ด ์กด์ฌํ์ฌ Router ์์ ์๋ ์ปดํฌ๋ํธ์ ๋ฐฐ์นํด์ผ ํ๋ค.
๊ทธ๋์ Router ๋ด ์ต์์ ๋ ์ด์์์ธ <Layout> ์ปดํฌ๋ํธ์setTimeout์ ๋ฐฐ์นํ์๋ค.
๊ทธ๋ฆฌ๊ณ ๋ก๊ทธ์ธ/๋ก๊ทธ์์/ํ ํฐ ์ฌ๋ฐ๊ธ์ ํ ๊ฒฝ์ฐ, ํ์ด๋จธ์ ํ ํฐ ๋ฐ๊ธ ์๊ฐ ๊ฐ์ ๋ณ๊ฒฝ์ด ํ์ํ๋ฏ๋ก, ํ ํฐ ๋ฐ๊ธ ์ฌ๋ถ๋ฅผisTokenIssued์ ์ญ ์ํ๋ก ๊ด๋ฆฌํ์ฌ, Layout ์ปดํฌ๋ํธ์์useEffect๋ฅผ ํตํ์ฌisTokenIssued์ ์ํ๋ฅผ ๊ฐ์งํ์ฌ ํ์ด๋จธ์ ํ ํฐ ๋ฐ๊ธ ์๊ฐ์ ๊ฐฑ์ ํ๊ธฐ๋ก ํ์๋ค.
์ฝ๋
(data fetch๋ react-query๋ฅผ ํตํ์ฌ ์ด๋ฃจ์ด์ง๋ค.)
1. ๋ก๊ทธ์ธ ์ฑ๊ณต ํ ๋ก์ง : ํ ํฐ ๋ฐ๊ธ ์ผ์์ ์ฌ๋ถ ํ ๋น
onSuccess: ({ accessToken, refreshToken }) => {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
localStorage.setItem('tokenDate', JSON.stringify(new Date()));
setIsLoggedIn(true);
setIsTokenIssued(true);
},
2. Layout์์ ์ ์ญ ์ํ isTokenIssued ๋ณํ ๊ฐ์ง
getTimeDifference(tokenDate): ํ ํฐ ๋ฐ๊ธ ์ผ์์ธ tokenDate์ ํ์ฌ ์๊ฐ๊ณผ์ ์ฐจ์ด๋ฅผ ๊ตฌํ์ฌ ํ ํฐ ๋ฐ๊ธ์ผ๋ก๋ถํฐ ๋ช ms๊ฐ ์ง๋ฌ๋์ง ๊ตฌํ๋ค.
remainTime: ํ ํฐ ๋ง๋ฃ ์๊ฐ(ms)์ธ TOKEN_EXPIRED_TIME๊ณผ getTimeDifference์ ์ฐจ์ด๋ฅผ ๊ตฌํ์ฌ ํ์ด๋จธ ์๊ฐ์ ์ค์ ํ๋ค. (์๋ก๊ณ ์นจํด๋ ๊ธฐ์กด ์์ฌ ์๊ฐ ์ ์ง๋๋๋ก ํ๋ค.)
isTokenIssued๊ฐ true๋ผ๋ฉด(ํ ํฐ์ด ๋ฐ๊ธ๋์๋ค๋ฉด) ๋ง๋ฃ ์๊ฐ์ด ๊ฒฝ๊ณผํ ํ isTokenIssued๋ฅผ false๋ก ๋ณ๊ฒฝํ๋ค. (๊ทธ๋์ผ ํ ํฐ์ ์ฌ๋ฐ๊ธ ๋ฐ์ isTokenIssued ๊ฐ์ true๋ก ์
๋ฐ์ดํธํ๊ณ , ๋ณํ๋ ๊ฐ์ useEffect๊ฐ ๊ฐ์งํ๋ค.) ๊ทธ๋ฆฌ๊ณ ํ ํฐ ์ฌ๋ฐ๊ธ api๋ฅผ ํธ์ถํ๋ค.
useEffect(() => {
const tokenDate = localStorage.getItem('tokenDate');
if (tokenDate) {
const remainTime = TOKEN_EXPIRED_TIME - getTimeDifference(tokenDate);
isTokenIssued &&
setTimeout(() => {
setIsTokenIssued(false);
reissueToken();
}, remainTime);
}
}, [isTokenIssued]);
3. reissueToken ๋ก์ง
์์ฒญ ์ฑ๊ณต์ ๋ก์ง์ ๋ก๊ทธ์ธ๊ณผ ๊ฑฐ์ ๋์ผํ๋ค.
์๋ฌ ๋ฐ์์, refreshToken ๋ํ ๋ง๋ฃ๋์์์ ๋ปํ๋ฏ๋ก ๋ก๊ทธ์์ ๋ก์ง์ ์คํํ๋ค.
onSuccess: ({ accessToken, refreshToken }) => {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
localStorage.setItem('tokenDate', JSON.stringify(new Date()));
setIsTokenIssued(true);
},
onError: () => {
pathname !== '/login' &&
open({
title: '๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์',
option: {
type: 'CONFIRM',
confirmEvent: () => {
logout();
},
},
});
},
๊ตฌํ ๊ฒฐ๊ณผ
ํ ํฐ ๋ง๋ฃ ์๊ฐ์ ์งง๊ฒ ํ์ฌ ํ ์คํธํด๋ณธ ๊ฒฐ๊ณผ ์๋์ ๊ฐ์ด ์๋์ผ๋ก ํ ํฐ ์ฌ๋ฐ๊ธ API๊ฐ ํธ์ถ๋๋ค.
๋ง๋ฌด๋ฆฌ
์ฒ์ ํ ํฐ ์ฌ๋ฐ๊ธ ๋ก์ง์ ๊ตฌํํ ๋, ๋ฐฑ์๋ ํ๊ณผ์ ์ํต ๋ถ์กฑ์ผ๋ก ์ธํด ํ ํฐ ์ฌ๋ฐ๊ธ ์์ ์ ์ ํํ ํ์ ํ์ง ๋ชปํ๋ค. ์์ผ๋ก ๋ฐฑ์๋ ๊ธฐ๋ฅ ์์ฑ์ ์ฌ๋ฌ ์๋๋ฆฌ์ค์ ๊ดํ์ฌ ํจ๊ป ์ถฉ๋ถํ ๋ ผ์ํด๋ด์ผ๊ฒ ๋ค.
๊ทธ๋ฆฌ๊ณ ํ ํฐ์ด ๋ง๋ฃ๋๊ธฐ ์ ์ ๋ฏธ๋ฆฌ ์ฌ๋ฐ๊ธํ๋ ๋ก์ง์ ๊ตฌํํ๋ฉด์ ์๊ฐ ๊ธฐ๋ฐ ์ด๋ฒคํธ ๊ด๋ฆฌ๋ฅผ ์ฒ์ ๊ฒฝํํ ์ ์์ด ์ข์๋ค. ๋ ํ ํฐ ๋ฐ๊ธ ์ฌ๋ถ๋ฅผ ์ ์ญ ์ํ๋ก ๊ด๋ฆฌํ๋ฉด์, ์๊ฐ๊ณผ ์ํ ๋ณํ์ ๋ฐ๋ฅธ ์ฌ๋ฌ ๊ณผ์ ์ ๋ก์ง์ ์๋์ผ๋ก ์ํํ ์ ์๊ฒ ํ๋ ๊ฒ์ด ์ฌ๋ฐ์๋ค.
๋ค์ ํ๋ก์ ํธ์์๋ localStorage๊ฐ ์๋ cookie๋ก ํ ํฐ์ ๊ด๋ฆฌํด๋ณผ ์์ ์ด๋ค.