๐Ÿ” 05. ์™ธ๋ถ€ ์ธ์ฆ ์„œ๋ฒ„ ์—ฐ๋™๊ณผ ํ† ํฐ ๊ด€๋ฆฌ: "์ƒˆ๋กœ๊ณ ์นจ ํ•˜๋‹ˆ๊นŒ ๋กœ๊ทธ์•„์›ƒ ๋˜๋Š”๋ฐ์š”?"

๐Ÿ“‹ ๊ฐœ์š”

Zustand์™€ ์˜์†์„ฑ(Persist) ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ™œ์šฉํ•œ ํด๋ผ์ด์–ธํŠธ ์ธ์ฆ ์ƒํƒœ ๋ฐ JWT ๋ณด์•ˆ ์ „๋žต

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • Zustand persist ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ†ตํ•ด ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€๋‚˜ ์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€์™€ ์ƒํƒœ๋ฅผ ์ž๋™ ๋™๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • JWT (Access Token & Refresh Token) ์ธ์ฆ ํ”Œ๋กœ์šฐ์—์„œ ์ƒํƒœ ๊ด€๋ฆฌ์™€ ๋ณด์•ˆ์˜ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋ฅผ ์ดํ•ดํ•œ๋‹ค.
  • ์™ธ๋ถ€ ์ธ์ฆ ์„œ๋ฒ„(Next.js BFF/Spring Boot) ์—ฐ๋™ ์‹œ Zustand์— ๋‹ด์•„์•ผ ํ•  ์ •๋ณด์™€ ๋‹ด์ง€ ๋ง์•„์•ผ ํ•  ์ •๋ณด๋ฅผ ์™„๋ฒฝํžˆ ํ†ต์ œํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 15๋ถ„ / ํ•ต์‹ฌ ํŒŒํŠธ: 10๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
์ˆœ์ˆ˜ ์Šคํ† ์–ด์˜ ํœ˜๋ฐœ์„ฑ ๋ฌธ์ œ โ†’ persist ๋ฏธ๋“ค์›จ์–ด ์ ์šฉ โ†’ JWT ํ† ํฐ์˜ ๋ณด์•ˆ ์ €์žฅ์ฒ˜ ๋ถ„๋ฐฐ ์ „๋žต (XSS ๋ฐฉ์–ด)

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

  • ๐Ÿฃ ์˜์ฒ  ( ์‹ ์ž… ): "๋ฆฌ๋“œ ๋‹˜! Zustand๋กœ ์œ ์ € ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์˜ˆ์˜๊ฒŒ ๋งŒ๋“ค์–ด์™”์–ด์š”. ์™„์ „ ๋น ๋ฆ…๋‹ˆ๋‹ค! ๊ทผ๋ฐ... ์‚ฌ์šฉ์ž๊ฐ€ F5(์ƒˆ๋กœ๊ณ ์นจ)๋ฅผ ๋ˆ„๋ฅด๋‹ˆ๊นŒ ๋‹ค ๋‚ ์•„๊ฐ€๋ฉด์„œ ๋‹ค์‹œ ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ํŠ•๊ธฐ๋„ค์š”."
  • ๐Ÿฆ ์˜ํ˜ธ ( ๋ฆฌ๋“œ ): "์˜์ฒ  ๋‹˜, JS ๋ฉ”๋ชจ๋ฆฌ๋Š” ๋ธŒ๋ผ์šฐ์ € ์ˆ˜๋ช… ์ฃผ๊ธฐ์— ์ข…์†๋˜๋‹ˆ๊นŒ ๋‹น์—ฐํžˆ ๋‹ค ๋‚ ์•„๊ฐ€์ฃ . ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ๋ฐฑ์—…์„ ํ•ฉ์‹œ๋‹ค. ์•„, ์ž ๊น ์˜์ˆ˜(PM) ๋‹˜์ด ๋ฐฑ์—”๋“œ ๋ณด์•ˆ ์ชฝ์—์„œ ํ•˜์‹ค ๋ง์”€์ด ์žˆ๋‚˜ ๋ณด๋„ค์š”."
  • ๐Ÿ‘” ์˜์ˆ˜ ( PM / ๋ฐฑ์—”๋“œ ): "์˜์ฒ  ์”จ, ํ† ํฐ ๊ด€๋ฆฌ๋ฅผ ๋ง์”€๋“œ๋ฆฌ๋Š” ๊ฑด๋ฐ์š”. ๋ฐฑ์—”๋“œ์—์„œ ๋ฐœ๊ธ‰ํ•ด๋“œ๋ฆฐ Access Token์ด๋‚˜ Refresh Token์„ ์ ˆ๋Œ€ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ๋ฌดํ„ฑ๋Œ€๊ณ  ์ €์žฅํ•˜์‹œ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค. ํ•ด์ปค๋“คํ•œํ…Œ ๋œฏ๊ธฐ๋ฉด ์ฑ…์ž„์ง€์‹ค ๊ฑด๊ฐ€์š”?"

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

Zustand ๊ฐ™์€ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋ณธ์งˆ์ ์œผ๋กœ RAM ๋จธ์‹ , ์ฆ‰ ๋ธŒ๋ผ์šฐ์ €์˜ JavaScript Memory Heap์— ์ƒ์ฃผํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํŽ˜์ด์ง€๊ฐ€ ๋ฆฌ๋กœ๋“œ๋˜๋ฉด ๋ชจ๋“  ์ƒํƒœ๋Š” ์ดˆ๊ธฐ ํ…… ๋นˆ ๊ฐ’์œผ๋กœ ๋ฆฌ์…‹๋˜๋ฉฐ, ์ด๋Š” '๋กœ๊ทธ์ธ ์œ ์ง€'๋ผ๋Š” ๊ฐ€์žฅ ํ”ํ•œ ์›น ์•ฑ ๊ธฐ๋ฏน์— ์น˜๋ช…์ ์ž…๋‹ˆ๋‹ค.

์ด๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•ด ์›น ์Šคํ† ๋ฆฌ์ง€(LocalStorage / SessionStorage)์— ์ง์ ‘ setItem, getItem์„ ๋•์ง€๋•์ง€ ๋ฐ”๋ฅด๋ฉด ์ฝ”๋“œ๊ฐ€ ์ŠคํŒŒ๊ฒŒํ‹ฐ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. Zustand๋Š” ์ด๋ฅผ ์œ„ํ•œ ๋‚ด์žฅ persist ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ œ๊ณตํ•˜์—ฌ ํด๋ฆญ ํ•œ ๋ฒˆ ์ˆ˜์ค€์œผ๋กœ ์˜์†์„ฑ์„ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์˜์ˆ˜๊ฐ€ ๊ฒฝ๊ณ ํ–ˆ๋“ฏ, ์—ฌ๊ธฐ์„œ JWT (JSON Web Token) ์•„ํ‚คํ…์ฒ˜์˜ ๋ณด์•ˆ ๋ฌธ์ œ๊ฐ€ ์ˆ˜๋ฉด ์œ„๋กœ ๋– ์˜ค๋ฆ…๋‹ˆ๋‹ค. ์–ด๋–ค ๊ฑด ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ๋กœ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•ด๋„ ๋˜๊ณ , ์–ด๋–ค ๊ฑด ์ ˆ๋Œ€๋กœ JS๊ฐ€ ์ ‘๊ทผํ•˜์ง€ ๋ชปํ•˜๋Š” ๊ณณ(HttpOnly)์œผ๋กœ ์ˆจ๊ฒจ์•ผ ํ• ๊นŒ์š”? ์ด๋ฅผ ๋ชจ๋ฅด๋ฉด ํŽธ๋ฆฌํ•จ์„ ์–ป๊ณ  ์•ฑ์˜ ์ฝ”์–ด ๋‹จ์„ ํ†ต์งธ๋กœ ํ„ธ๋ฆฌ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


๐Ÿ—๏ธ 1. ์Šคํ† ์–ด ์˜์†์„ฑ ๋ถ€์—ฌ: persist ๋ฏธ๋“ค์›จ์–ด

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ persist ๋ฌธ๋ฒ•๋ถ€ํ„ฐ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
 
interface AuthState {
  isLoggedIn: boolean;
  username: string;
  login: (name: string) => void;
  logout: () => void;
}
 
// ๐Ÿฆ ์˜ํ˜ธ: create ๊ด„ํ˜ธ ์•ˆ์— persist ํ•จ์ˆ˜๋กœ ์Šคํ† ์–ด ์ •์˜๋ถ€๋ฅผ ํ•œ ๋ฒˆ ๋” ๊ฐ์Œ‰๋‹ˆ๋‹ค.
export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      isLoggedIn: false,
      username: '',
      login: (name) => set({ isLoggedIn: true, username: name }),
      logout: () => set({ isLoggedIn: false, username: '' }),
    }),
    {
      name: 'auth-storage', // ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅ๋  ๋•Œ ์‚ฌ์šฉ๋  Key ๊ณ ์œ  ์‹๋ณ„์ž
      storage: createJSONStorage(() => sessionStorage), // ๊ธฐ๋ณธ๊ฐ’์€ localStorage. ์—ฌ๊ธฐ์„  ๋ธŒ๋ผ์šฐ์ € ์ข…๋ฃŒ ์‹œ ๋‚ ๋ฆฌ๋ ค๊ณ  session ์‚ฌ์šฉ.
      
      // ๐Ÿ’ก ๋ถ€๋ถ„ ์ €์žฅ ๊ธฐ๋Šฅ (์„ ํƒ์  ์˜์†์„ฑ)
      partialize: (state) => ({ isLoggedIn: state.isLoggedIn, username: state.username }),
    }
  )
);

์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์ˆœ๊ฐ„, Zustand๋Š” ์ƒํƒœ์˜ ๋ณ€๊ฒฝ์ด ์ผ์–ด๋‚  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์ง๋ ฌํ™”ํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ € ์Šคํ† ๋ฆฌ์ง€์— ๊ฐ’์„ ๋™๊ธฐํ™”์‹œํ‚ต๋‹ˆ๋‹ค. ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ๋‹ค์‹œ ๊ทธ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์—ญ์ง๋ ฌํ™”ํ•˜์—ฌ ์ƒํƒœ ๋ณต์›์„ ์•Œ์•„์„œ ์ฒ˜๋ฆฌํ•ด ์ค๋‹ˆ๋‹ค.


๐Ÿ›ก๏ธ 2. ์˜์ฒ ์ด์˜ ๋Œ€์ฐธ์‚ฌ: JWT ๋ณด์•ˆ์˜ ๋ซ

์ด์ œ ๋กœ๊ทธ์ธ ์‹œ ๋ฐ›์•„์˜ค๋Š” ํ† ํฐ์„ ๊ด€๋ฆฌํ•ด๋ณผ๊นŒ์š”?

// โŒ ์˜์ฒ ์ด์˜ ํญํƒ„ ์ฝ”๋“œ: ๋ชจ๋“  ๊ฑธ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ๊ธฐ๋ฐ˜ Zustand์— ์ฒ˜๋ฐ•์Œ
login: async (credentials) => {
  const { accessToken, refreshToken, user } = await api.post('/login');
  // ๐Ÿ‘” ์˜์ˆ˜ ๊ทน๋Œ€๋…ธ: "Refresh Token์„ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ–ˆ๋‹ค๊ณ ์š”?!"
  set({ accessToken, refreshToken, user }); 
}

์™œ ์•ˆ ๋ ๊นŒ์š”? (์›น ๋ณด์•ˆ ๊ธฐ์ดˆ)

  • ๋ธŒ๋ผ์šฐ์ €์˜ localStorage๋‚˜ sessionStorage์— ์ €์žฅ๋œ ๊ฐ’์€ ํ˜„์žฌ ํŽ˜์ด์ง€์—์„œ ๋™์ž‘ํ•˜๋Š” ์–ด๋– ํ•œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ๋ผ๋„ ์ ‘๊ทผ(getItem)ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์œ ์ž…๋˜๋Š” ํ•ดํ‚น ๊ธฐ๋ฒ•์ธ XSS(Cross-Site Scripting) ๊ฐ€ ๋ฐœ๋™ํ•˜๋ฉด ํ•ด์ปค๊ฐ€ ์ฆ‰์‹œ ๊ทธ ํ† ํฐ์„ ์Šคํ‹ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Refresh Token์ด ํ„ธ๋ฆฌ๋ฉด, ํ•ด์ปค๋Š” ํ† ํฐ ๋งŒ๋ฃŒ ์ „๊นŒ์ง€ ์œ ์ € ํ–‰์„ธ๋ฅผ ๋ฌดํ•œ๋Œ€๋กœ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’Ž 3. ํ† ํฐ ์Šคํ† ๋ฆฌ์ง€ ๋ถ„๋ฆฌ ์ „๋žต (์‹ค๋ฌด ์•„ํ‚คํ…์ฒ˜)

5๋…„ ์ฐจ ์‹œ์•ผ์—์„œ๋Š” ์„ฑ๋Šฅ๊ณผ ๋ณด์•ˆ ์‚ฌ์ด์˜ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋ฅผ ๋งž์ถ˜ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ „๋žต์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

A. ์œ ์ € ์ •๋ณด (User Profile) -> Zustand + Persist [์•ˆ์ „]

  • ๋‹‰๋„ค์ž„, ํ”„๋กœํ•„ ์‚ฌ์ง„ URL, ํ…Œ๋งˆ ์„ค์ • ๋“ฑ์€ ํƒˆ์ทจ๋˜์–ด๋„ ์ฆ‰๊ฐ์ ์ธ ํŒŒ๋ฉธ์— ์ด๋ฅด์ง€ ์•Š๋Š” ์ฝ๊ธฐ ์ „์šฉ UI ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.
  • ์ด ๋ฐ์ดํ„ฐ๋“ค์€ ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅ(Zustand Persist)ํ•˜์—ฌ ์ƒˆ๋กœ๊ณ ์นจ ์งํ›„์—๋„ ๋ฐ”๋กœ ๋ Œ๋”๋ง๋˜๊ฒŒ ๋‘ก๋‹ˆ๋‹ค.

B. Access Token -> ์ˆœ์ˆ˜ Zustand (๋ฉ”๋ชจ๋ฆฌ) [๋‹จ๊ธฐ ์ฒด๋ฅ˜]

  • API ํ˜ธ์ถœ ๋•Œ ํ—ค๋”์— ์‹ค์–ด์•ผ ํ•˜๋ฏ€๋กœ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ ‘๊ทผํ•˜๊ธด ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ XSS ๋ฒ”์œ„์— ๋‚จ์ง€ ์•Š๋„๋ก ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (์ฆ‰, partialize ์„ค์ •์—์„œ ๊ฑธ๋Ÿฌ๋ƒ…๋‹ˆ๋‹ค).
  • ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ์ด ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๋‚ ์•„๊ฐ€์ง€๋งŒ, ๊ณง๋ฐ”๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ API๋ฅผ ํƒœ์›Œ ์ƒˆ๋กœ ๋ฐ›์•„์˜ค๋ฉด ๊ทธ๋งŒ์ž…๋‹ˆ๋‹ค(Silent Refresh).

C. Refresh Token -> HttpOnly ์ฟ ํ‚ค [JS ์ ‘๊ทผ ์ฐจ๋‹จ]

  • ์ด๊ฑด ์•„์˜ˆ ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ๊ด€๋ฆฌํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
  • ๋ฐฑ์—”๋“œ(์˜์ˆ˜)๊ฐ€ Set-Cookie: RefreshToken=...; HttpOnly; Secure; ์†์„ฑ์„ ๋ถ™์—ฌ์„œ ๋ธŒ๋ผ์šฐ์ € ์ฟ ํ‚ค ์ง€๊ฐ‘์— ์ง์ ‘ ๊ฝ‚์•„์ค๋‹ˆ๋‹ค.
  • ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋Š” ๊ตฌ์กฐ์ ์œผ๋กœ(HttpOnly) ์ด ์ฟ ํ‚ค์˜ ๊ฐ’์„ ์˜์›ํžˆ ์ฝ์„ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ๋ฌด์ ์˜ ๋ฐฉ์–ด๋ ฅ์ด ์ƒ๊น๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋‹ค์Œ ์ธ์ฆ API ์š”์ฒญ์„ ์  ๋•Œ ์ž๋™์œผ๋กœ ๋™๋ด‰ํ•ด์„œ ๋ณด๋‚ผ ๋ฟ์ž…๋‹ˆ๋‹ค.

์‹ค๋ฌด ์ฝ”๋“œ: ๋ถ€๋ถ„ ์˜์†์„ฑ(Partialize) ํ†ต์ œ

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,         // ์˜์†ํ™” ๋Œ€์ƒ (UI์šฉ)
      accessToken: null,  // ํœ˜๋ฐœ์„ฑ ๋ฉ”๋ชจ๋ฆฌ์šฉ (API ์ธ์ฆ์šฉ)
      
      setAuth: (user, token) => set({ user, accessToken: token }),
      clearAuth: () => set({ user: null, accessToken: null })
    }),
    {
      name: 'user-storage',
      // ๐Ÿ”ฅ ํ•ต์‹ฌ: ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€์—๋Š” ์ˆœ์ˆ˜ ์œ ์ € ์ •๋ณด๋งŒ ๋ฐฑ์—…ํ•˜๊ณ  ํ† ํฐ์€ ๊ฑธ๋Ÿฌ๋‚ธ๋‹ค!
      partialize: (state) => ({ user: state.user }),
    }
  )
);

๐Ÿ“ 4. ๋ฒˆ์™ธ: Next.js ์˜ Hydration ๋ฏธ์Šค๋งค์น˜ ์ฃผ์˜

Zustand์˜ persist๋Š” ๊ทผ๋ณธ์ ์œผ๋กœ "์„œ๋ฒ„๊ฐ€ ๋ชจ๋ฅด๋Š” ๋ธŒ๋ผ์šฐ์ €๋งŒ์˜ ๋ฐ์ดํ„ฐ"๋ฅผ ์ดˆ๊ธฐ ์ƒํƒœ์— ์ง‘์–ด๋„ฃ์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ Next.js (SSR/SSG ํ™˜๊ฒฝ)๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด:

  1. ์„œ๋ฒ„์—์„œ ์ฒ˜์Œ์— ๋นˆ ๊ฐ’(isLoggedIn: false)์ธ ์ฑ„๋กœ HTML ๊ป๋ฐ๊ธฐ๋ฅผ ๋งŒ๋“ค์–ด ํด๋ผ์ด์–ธํŠธ๋กœ ๋ณด๋ƒ„.
  2. ํด๋ผ์ด์–ธํŠธ React ๋ชจ๋“ˆ์ด ๊นจ์–ด๋‚˜๊ณ (Hydration) Zustand๊ฐ€ ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€์—์„œ true๋ฅผ ๋„์ง‘์–ด๋ƒ„.
  3. ๋ฆฌ์•กํŠธ ์—”์ง„: "์–ด๋ผ? ์„œ๋ฒ„๊ฐ€ ์ค€ HTML์ด๋ž‘ ๋‹ˆ๊ฐ€ ๊ทธ๋ฆด HTML์ด ๋‹ค๋ฅด๋„ค?!" ๐Ÿ‘‰ Hydration Mismatch Error ํญ๋ฐœ

ํ•ด๊ฒฐ์ฑ…: Hydration์ด ์™„์ „ํžˆ ์ข…๋ฃŒ๋˜๊ธฐ ์ „๊นŒ์ง€๋Š” Persist ์Šคํ† ์–ด ๋ Œ๋”๋ง์„ ๋Šฆ์ถ”๋Š”(useEffect ๋‚ด๋ถ€์—์„œ ๊ฐ’ ๊บผ๋‚ด์˜ค๊ธฐ) ํŠธ๋ฆญ์ด ํ•„์ˆ˜์ ์œผ๋กœ ๋™๋ฐ˜๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (์ด ๋ถ€๋ถ„์€ Next.js ์‹ฌํ™” ํŽธ ๊ฐ€์ด๋“œ์—์„œ ๋” ๋‹ค๋ฃจ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.)


๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. Zustand์˜ persist ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๋ธŒ๋ผ์šฐ์ € ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— JWT(Access Token ๋˜๋Š” Refresh Token)๋ฅผ ์ €์žฅํ•˜๋ฉด ๋ฐœ์ƒํ•˜๋Š” ๊ฐ€์žฅ ์น˜๋ช…์ ์ธ ๋ณด์•ˆ ์ทจ์•ฝ์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?

  • A) CSRF (Cross-Site Request Forgery)
  • B) SQL Injection
  • C) XSS (Cross-Site Scripting)
  • D) Clickjacking

โœ… ์ •๋‹ต: C

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • ์›๋ฆฌ ์„ค๋ช…: ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ๋ฐ์ดํ„ฐ๋Š” ๋ธŒ๋ผ์šฐ์ € ๋‚ด์—์„œ ์‹คํ–‰๋˜๋Š” ๋ชจ๋“  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ(window.localStorage.getItem)์— ๋…ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ๊ฒŒ์‹œํŒ ๊ธ€์ด๋‚˜ ๊ด‘๊ณ  ์Šคํฌ๋ฆฝํŠธ ๋“ฑ์„ ํ†ตํ•ด ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ฃผ์ž…๋˜๋Š” XSS ๊ณต๊ฒฉ ์‹œ, ํ•ด์ปค๊ฐ€ ์ฆ‰๊ฐ์ ์œผ๋กœ ํ† ํฐ์„ ํƒˆ์ทจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์˜ค๋‹ต ํ”ผ๋“œ๋ฐฑ: "์˜์ฒ  ๋‹˜, CSRF ๋ฐฉ์–ด๋ฅผ ์œ„ํ•ด์„œ๋Š” ์ฟ ํ‚ค(HttpOnly, SameSite)๋ฅผ ์“ฐ๊ฑฐ๋‚˜ ํ—ค๋” ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•˜๊ณ , ์Šคํ† ๋ฆฌ์ง€ ํ† ํฐ ํƒˆ์ทจ๋Š” ์ „ํ˜•์ ์ธ XSS ๋ฐฅ์ž…๋‹ˆ๋‹ค."
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: ๐Ÿ‘” ์˜์ˆ˜: "๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€๋Š” ์œ ๋ฆฌ๋ฒฝ ์•ˆ์˜ ๊ธˆ๊ณ ์ž…๋‹ˆ๋‹ค. ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ผ๋Š” ์—ด์‡ ๋งŒ ์žˆ์œผ๋ฉด ๋ˆ„๊ตฌ๋‚˜ ๋“ค์—ฌ๋‹ค๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."

Q2. Zustand ์Šคํ† ์–ด์˜ persist ๋ฏธ๋“ค์›จ์–ด ์‚ฌ์šฉ ์‹œ, ๋ณด์•ˆ์ƒ ๋ฏผ๊ฐํ•œ ํ† ํฐ ๋ฐ์ดํ„ฐ๋งŒ ์Šคํ† ๋ฆฌ์ง€ ๋ฐฑ์—…์—์„œ ์ œ์™ธํ•˜๊ณ  ์œ ์ € ํ”„๋กœํ•„ ๋“ฑ ์•ˆ์ „ํ•œ ์ •๋ณด๋งŒ ์ €์žฅํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์†์„ฑ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?

  • A) exclude
  • B) partialize
  • C) serialize
  • D) getStorage

โœ… ์ •๋‹ต: B

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • ์›๋ฆฌ ์„ค๋ช…: persist ์„ค์ • ๊ฐ์ฒด ๋‚ด์˜ partialize: (state) => ({ user: state.user }) ์™€ ๊ฐ™์ด ์„ ์–ธํ•˜๋ฉด, ์ „์ฒด Zustand ์ƒํƒœ ์ค‘์—์„œ ์ง€์ •๋œ ์†์„ฑ๋งŒ ๊ณจ๋ผ์„œ ์Šคํ† ๋ฆฌ์ง€์— ์ง๋ ฌํ™”ํ•ฉ๋‹ˆ๋””.
  • ์˜ค๋‹ต ํ”ผ๋“œ๋ฐฑ: "์˜์ฒ  ๋‹˜, ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ๊ฑธ๋Ÿฌ๋‚ด๋Š” ๊ฑฐ๋ฆ„๋ง ์ด๋ฆ„์ด partialize(๋ถ€๋ถ„ํ™”)์ž…๋‹ˆ๋‹ค. ๊ผญ ์™ธ์›Œ๋‘์„ธ์š”!"
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: ๐Ÿฆ ์˜ํ˜ธ: "ํ† ํฐ์€ ํœ˜๋ฐœ์‹œํ‚ค๊ณ , ํ”„๋กœํ•„์€ Partialize๋กœ ๋ฐฑ์—…ํ•˜์„ธ์š”."

Q3. ๐Ÿ‹๏ธโ€โ™‚๏ธ ์˜์ฒ ์˜ ํ…Œ์ŠคํŠธ ํƒ€์ž„ (Q&A)

์˜์ฒ ์ด๊ฐ€ ์‡ผํ•‘ ์•ฑ์˜ '์ตœ๊ทผ ๋ณธ ์ƒํ’ˆ ID ๋ชฉ๋ก'์„ ์‚ฌ์šฉ์ž์˜ ๊ธฐ๊ธฐ์— ์˜๊ตฌํžˆ ๊ธฐ์–ต์‹œํ‚ฌ ๋ชฉ์ ์œผ๋กœ Zustand persist (localStorage) ์— ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค. ํ—Œ๋ฐ ์˜†์ž๋ฆฌ ์˜์ˆ˜ ๋‹˜์ด "์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ฒฐ์ œ ํ† ํฐ๋„ ๊ฑฐ๊ธฐ์— ๊ฐ™์ด ์ €์žฅํ•˜๋ฉด ์•ˆ ๋˜๋ƒ"๊ณ  ์ œ์•ˆ์„ ํ•˜๋„ค์š”. ์–ด๋–ป๊ฒŒ ๋ฐฉ์–ดํ•ด์•ผ ํ• ๊นŒ์š”?

โœ… ์ •๋‹ต: ๊ฒฐ์ œ ํ† ํฐ์ฒ˜๋Ÿผ ํƒˆ์ทจ ์‹œ ์น˜๋ช…์ ์ธ ๊ถŒํ•œ ์ œ์–ด ํ‚ค๋Š” partialize ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ๋ฐฑ์—… ๋Œ€์ƒ์—์„œ ๋ฐ˜๋“œ์‹œ ์ œ์™ธ์‹œํ‚จ๋‹ค. ํ˜น์€ ๋ฐฑ์—”๋“œ์—์„œ HttpOnly ์ฟ ํ‚ค๋กœ ์„ค๊ณ„ํ•˜๋„๋ก ์—ญ์ œ์•ˆํ•œ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • ์›๋ฆฌ ์„ค๋ช…: LocalStorage์— ๊ธฐ๋ก๋œ ๊ฐ’์€ ํ˜„์žฌ ํƒญ์— ์—ด๋ ค์žˆ๋Š” ์–ด๋– ํ•œ ์„œ๋“œํŒŒํ‹ฐ ์Šคํฌ๋ฆฝํŠธ(๊ตฌ๊ธ€ ์• ๋„๋ฆฌํ‹ฑ์Šค, ์™ธ๋ถ€ ๊ด‘๊ณ  ๋ฐฐ๋„ˆ ๋“ฑ)๋‚˜ ์•…์„ฑ XSS ์ฃผ์ž… ์ฝ”๋“œ๋ผ๋„ window.localStorage.getItem ๋ช…๋ น ํ•˜๋‚˜๋กœ ํ›”์ณ๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. UI ๋ Œ๋”๋ง ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ๋ถ€๊ฐ€ ์ •๋ณด๋งŒ Persist์— ์˜์กดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์˜ค๋‹ต ํ”ผ๋“œ๋ฐฑ: "์˜์ฒ  ๋‹˜, PM์ด ํ•ด๋‹ฌ๋ผ๊ณ  ๋‹ค ํ•ด์ฃผ๋ฉด ์•ˆ ๋ผ์š”. ๊ฒฐ์ œ ํ† ํฐ์€ ๋ฉ”๋ชจ๋ฆฌ(accessToken ์†์„ฑ)์— ๋‘๊ณ  ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ๋‚ ๋ฆฐ ๋‹ค์Œ Silent Refresh ๊ธฐ๋ฒ•์œผ๋กœ ๋ฐฑ์—”๋“œ์—์„œ ๋‹ค์‹œ ๋ฐ›์•„์˜ค๊ฒŒ ์„ค๊ณ„ํ•ด์•ผ ์‚ฌ๊ณ ๋ฅผ ๋ง‰์Šต๋‹ˆ๋‹ค."
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: ๐Ÿ‘” ์˜์ˆ˜: "์„œ๋ฒ„๊ฐ€ ์•Œ๋ฉด ํฐ์ผ ๋‚˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ์—†์ง€๋งŒ, ํด๋ผ์ด์–ธํŠธ(JS) ์˜์—ญ์— ๋‚จ์œผ๋ฉด ํฐ์ผ ๋‚˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ์ˆ˜๋„ ์—†์ด ๋งŽ๋‹ค."

๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์˜ค๋Š˜์€ ์˜์ˆ˜ PM๋‹˜ํ•œํ…Œ ๋ผˆ๋ฅผ ์—„์ฒญ ์„ธ๊ฒŒ ๋งž์•˜๋‹ค. "๋‚ด ์ •๋ณด ๋‚ด๊ฐ€ ์“ฐ๊ฒŒ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•˜๊ฒ ๋‹ค๋Š”๋ฐ ๋ญ๊ฐ€ ๋ฌธ์ œ์•ผ!" ๋ผ๊ณ  ์†์œผ๋กœ ํˆฌ๋œ๋Œ”์—ˆ๋Š”๋ฐ, XSS ๊ณต๊ฒฉ์ด๋ž€ ๊ฑธ ๋“ฃ๊ณ  ๋‚˜๋‹ˆ๊นŒ ๊ทธ๋™์•ˆ ๋‚ด๊ฐ€ ๋งŒ๋“  ํšŒ์›๊ฐ€์ž… ๋กœ์ง์ด ์‹œํ•œํญํƒ„์ด์—ˆ๋‹ค๋Š” ๊ฑธ ๊นจ๋‹ฌ์•˜๋‹ค. Zustand persist ๋ฏธ๋“ค์›จ์–ด์—์„œ partialize ์˜ต์…˜ ํ•˜๋‚˜ ๋”ฑ ๋„ฃ์–ด์„œ ํ† ํฐ ๋นผ๊ณ  ์ €์žฅํ•˜๋‹ˆ๊นŒ ๋ณด์•ˆ๊ณผ ์‚ฌ์šฉ์ž ํŽธ์˜๋ฅผ ๋‘˜ ๋‹ค ์žก์€ ๊ธฐ๋ถ„์ด๋‹ค! ํ›„ํ›„, ์ด๋Ÿฐ ๋ณด์•ˆ ๊ฐœ๋…๊นŒ์ง€ ์•„๋Š” ์ฃผ๋‹ˆ์–ด๊ฐ€ ํ”ํ• ๊นŒ? ๋‚ด์ผ ์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜ํ•œํ…Œ ์€๊ทผ์Šฌ์ฉ ์ž๋ž‘ํ•ด๋ด์•ผ์ง€. ๋‹ฌ๋‹ฌํ•œ ์•„์ด์Šคํฌ๋ฆผ ํ•˜๋‚˜ ๋จน๊ณ  ํ‘น ์ž์•ผ๊ฒ ๋‹ค ๐Ÿฆ!

๐Ÿ’ก "๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€๋Š” ๋ชจ๋‘๊ฐ€ ๋ณด๋Š” ํˆฌ๋ช…ํ•œ ๊ฒŒ์‹œํŒ์ด๋‹ค. ์ธ์ฆ ํ‚ค๋Š” ์ˆจ๊ธฐ๊ณ , ๊ป๋ฐ๊ธฐ ์ •๋ณด๋งŒ ๊ธฐ๋กํ•˜์ž."