Project๐Ÿ› ๏ธโš™๏ธ

ํ”„๋ก ํŠธ์—”๋“œ JWT ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ํƒ€์ด๋จธ ๊ตฌํ˜„ (React JS + React Query)

Gaeun Lee 2024. 8. 7. 02:23

ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๊ฒฝ์šฐ, ํ† ํฐ์„ ์žฌ๋ฐœ๊ธ‰ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ ์ค‘ ์•„๋ž˜์™€ ๊ฐ™์ด 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๋กœ ํ† ํฐ์„ ๊ด€๋ฆฌํ•ด๋ณผ ์˜ˆ์ •์ด๋‹ค.