๊ฐœ๋ฐœ/FRONTEND

Next.js์—์„œ ์•ˆ์ „ํ•œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ ์—ฌ์ • (without NextAuth)

clein 2024. 7. 10. 17:04

 

๊ตฌํ˜„ ๋ชฉํ‘œ

์ €ํฌ๋Š” ์‹ค์ œ๋กœ ์„œ๋น„์Šค๋ฅผ ์šด์˜ํ•˜๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ์˜€๊ธฐ ๋•Œ๋ฌธ์— ์œ ์ € ์ธ์ฆ ๊ด€๋ จ ๋กœ์ง๋“ค์—์„œ๋Š” ๋ณด์•ˆ์ด ๋งค์šฐ ์ค‘์š”ํ–ˆ๊ณ , ๊ทธ์— ๋”ฐ๋ผ ์š”๊ตฌ์‚ฌํ•ญ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์•˜์Šต๋‹ˆ๋‹ค

์š”๊ตฌ์‚ฌํ•ญ

  1. ์ธ์ฆ ๊ด€๋ จ ๋กœ์ง์„ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์„ ๊ฒƒ
  2. ํด๋ผ์ด์–ธํŠธ์—์„œ ์Šคํฌ๋ฆฝํŠธ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๊ณณ์— ๊ฐœ์ธ์ •๋ณด๋ฅผ ๋‚จ๊ธฐ์ง€ ์•Š์„ ๊ฒƒ ex) ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€, ์„ธ์…˜์Šคํ† ๋ฆฌ์ง€, ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ, ์ฟ ํ‚ค ๋“ฑ
  3. ๋กœ๊ทธ์ธ ํ›„ ์ƒˆ๋กœ๊ณ ์นจ์„ ํ•ด๋„ ์ผ์ •์‹œ๊ฐ„๋™์•ˆ์€ ๋กœ๊ทธ์ธ ์ƒํƒœ๊ฐ€ ํ’€๋ฆฌ์ง€ ์•Š์„ ๊ฒƒ
  4. ๊ตฌ๊ธ€์—์„œ ์ง€์›ํ•˜๋Š” OAuth ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•  ๊ฒƒ

 

๋˜ํ•œ Next.js ๋ฅผ ์ฐจ์šฉํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ ๋Œ€์‘์ด ๊ฐ€๋Šฅํ–ˆ์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค

 

 

์™œ ๋ธŒ๋ผ์šฐ์ €์— ๊ฐœ์ธ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋ฉด ์•ˆ๋ ๊นŒ?

Redux ๋˜๋Š” Zustand ๊ฐ™์€ ์ „์—ญ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ๋Š” ์ „์—ญ ์ƒํƒœ์˜ ์œ ์ง€๋ฅผ ์œ„ํ•ด ๋ฏธ๋“ค์›จ์–ด์˜ ํ˜•ํƒœ๋กœ persist ๋ฅผ ์ง€์›ํ•˜๋Š”๋ฐ์š”, ๋ฆฌ์•กํŠธ ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•  ๋•Œ persist ๋ฅผ ๊ณ ๋ คํ•ด๋ณธ ์ ์ด ์žˆ์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค

๊ผญ persist ๊ฐ€ ์•„๋‹ˆ๋”๋ผ๋„ ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€ ๊ฐ™์€ ๊ณณ์— ์œ ์ € ์ •๋ณด๋ฅผ ์ €์žฅํ•ด๋†“๊ณ  ์“ฐ๋ฉด ํŽธ๋ฆฌํ•œ๋ฐ ์™œ ์ €์žฅํ•˜์ง€ ๋ง๋ผ๊ณ  ํ•˜๋Š” ๊ฑธ๊นŒ์š”?

 

์กฐ๊ธˆ ์ง„๋ถ€ํ•œ ๋Œ€๋‹ต์ด์ง€๋งŒ ์—ญ์‹œ๋‚˜ XSS ๋˜๋Š” CSRF ๋ฅ˜์˜ ๊ณต๊ฒฉ์„ ๋Œ€๋น„ํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค

  XSS (๊ต์ฐจ ์‚ฌ์ดํŠธ ์Šคํฌ๋ฆฝํŒ…) CSRF (๊ต์ฐจ ์‚ฌ์ดํŠธ ์š”์ฒญ ์œ„์กฐ)
๊ฐœ์š” ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ํด๋ผ์ด์–ธํŠธ์—์„œ ์‹คํ–‰๋จ ๊ณต๊ฒฉ์ž๊ฐ€ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์‚ฌ์นญํ•˜๊ณ  ์›น ์‚ฌ์ดํŠธ์— ์›์น˜ ์•Š๋Š” ์š”์ฒญ์„ ๋ณด๋ƒ„
๊ณต๊ฒฉ ๋Œ€์ƒ ํด๋ผ์ด์–ธํŠธ ์„œ๋ฒ„
๋ชฉ์  ์„ธ์…˜ ํƒˆ์ทจ, ์›น์‚ฌ์ดํŠธ ๋ณ€์กฐ ๊ถŒํ•œ ๋„์šฉ

 

XSS ๊ณต๊ฒฉ ์˜ˆ์‹œ ์ด๋ฏธ์ง€

 

๋ฆฌ์•กํŠธ์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ innerHTML์„ ๋ง‰์•„๋‘์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์œ„์™€ ๊ฐ™์ด ๋‹จ์ˆœํ•œ ๊ณต๊ฒฉ ํŒจํ„ด์€ ๋จนํžˆ์ง€ ์•Š๊ฒ ์ง€๋งŒ, ๋งŒ์•ฝ ๋ณด์•ˆ์ด ๋šซ๋ ธ์„ ๊ฒฝ์šฐ ์Šคํฌ๋ฆฝํŠธ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ์ •๋ณด๋Š” ํƒˆ์ทจ๋  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค

 

๋งŒ์•ฝ ์Šคํฌ๋ฆฝํŠธ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๊ณณ์— ์œ ์ €์˜ ๊ฐœ์ธ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๋ฉด ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ๊ณต๊ฒฉ์ž์—๊ฒŒ ๋ฌด๋ฐฉ๋น„๋กœ ์œ ์ €๋“ค์˜ ๊ฐœ์ธ์ •๋ณด๋ฅผ ๋‚ด์–ด์ค„ ์ˆ˜ ๋ฐ–์— ์—†๊ฒ ์ฃ ?

 

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋กœ์จ ํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ์„ ์€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ทจ์•ฝ์ ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ์ตœ์†Œํ™”ํ•˜๊ณ , ์œ ์ €์™€ ๊ด€๋ จ๋œ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ๋‹ค๋ฃฐ ๋•Œ๋Š” ์ตœ๋Œ€ํ•œ ์„œ๋ฒ„์— ์œ„์ž„ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค

์„œ๋ฒ„๋Š” ๋น„๊ต์  ํด๋ผ์ด์–ธํŠธ ๋ณด๋‹ค ๋ณด์•ˆ ์œ„ํ˜‘์ด ์ ๊ธฐ ๋•Œ๋ฌธ์ด์ฃ 

 

ํด๋ผ์ด์–ธํŠธ์—์„œ ๊ฐ€์žฅ ๋ณด์•ˆ์ˆ˜์ค€์ด ๋†’์€ ์ €์žฅ๋ฐฉ์‹์€ ์ธ๋ฉ”๋ชจ๋ฆฌ(๋ณ€์ˆ˜)์— ๊ฐ’์„ ์ €์žฅํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค
ํ•˜์ง€๋งŒ ๋‹จ๋…์œผ๋กœ ์‚ฌ์šฉํ•˜๋ฉด ์ƒˆ๋กœ๊ณ ์นจํ•  ๋•Œ ๋งˆ๋‹ค ํœ˜๋ฐœ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์œ ์ € ๊ฒฝํ—˜์— ํฐ ์ฐจ์งˆ์ด ์ƒ๊ธฐ์ฃ 

 

๊ทธ๋ ‡๋‹ค๊ณ  ์Šคํฌ๋ฆฝํŠธ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€, ์„ธ์…˜์Šคํ† ๋ฆฌ์ง€, ์ฟ ํ‚ค ๋“ฑ์— ์œ ์ € ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ธฐ์—๋Š” XSS์™€ CSRF ๋ฅ˜์˜ ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

 

๊ทธ๋Ÿผ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•ด์•ผ ํ• ๊นŒ์š”?

 

 

๊ทธ๋ž˜์„œ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋‚˜?

๋กœ๊ทธ์ธ ํ”Œ๋กœ์šฐ ์ฐจํŠธ

๊ฒฐ๋ก ์ ์œผ๋กœ ์ €ํฌ๋Š” api ๋ผ์šฐํŠธ์— HttpOnly ์™€ SameSite ๋ฐ secure ์˜ต์…˜์„ ์ ์šฉํ•œ ์ฟ ํ‚ค ๋ฐฉ์‹์„ ์ฑ„ํƒํ•˜์˜€๊ณ , ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์šฉ ํ›…์„ ๋งŒ๋“ค์–ด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋ถ€ํ„ฐ ์œ ์ € ์ •๋ณด๋ฅผ ๋ฐ›์•„์™€์„œ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค

๋˜ํ•œ rewrite ๋ผ๋Š” Next.js ์˜ ํ”„๋ก์‹œ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์˜ ๋ชจ๋“  ์š”์ฒญ์— ์„ธ์…˜์•„์ด๋”” ์ฟ ํ‚ค๊ฐ€ ๋‹ด๊ธธ ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ•˜์˜€์Šต๋‹ˆ๋‹ค

 

์ฟ ํ‚ค๋„ ์œ„ํ—˜ํ•œ๊ฑฐ ์•„๋‹ˆ์—ˆ๋ƒ?

๋ธŒ๋ผ์šฐ์ €์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ž‘๋™๋˜๋Š” ์ฟ ํ‚ค๋Š” ์‚ฌ์‹ค XSS, CSRF ๊ณต๊ฒฉ์— ๋ชจ๋‘ ์ทจ์•ฝํ•˜์ง€๋งŒ

ํŠน์ • ์˜ต์…˜์„ ํ†ตํ•ด ์ทจ์•ฝ์ ๋“ค์„ ์ตœ๋Œ€ํ•œ ๋ณด์™„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

์ ์šฉํ•œ ์˜ต์…˜๋“ค

  • HttpOnly : ์ ์šฉ ์‹œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ ์ƒ์—์„œ ์ ‘๊ทผ์ด ๋ถˆ๊ฐ€๋Šฅํ•ด์ง€๋ฉฐ, HTTP ์š”์ฒญ์—๋งŒ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค
  • SameSite : none , strict , lax ๋“ฑ์˜ ์˜ต์…˜์ด ์žˆ์Šต๋‹ˆ๋‹ค
    • None : ํŠน๋ณ„ํ•œ ์„ค์ •์„ ํ•˜์ง€ ์•Š๋Š” ์˜ต์…˜์ž…๋‹ˆ๋‹ค
    • Strict : ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์š”์ฒญ์—๋Š” ํ•ญ์ƒ ์ „์†ก๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
    • Lax : ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์š”์ฒญ ์ค‘ ์•ˆ์ „ํ•œ ์š”์ฒญ์—๋งŒ ์ „์†ก๋ฉ๋‹ˆ๋‹ค
  • secure : HTTPS๊ฐ€ ์•„๋‹Œ ํ†ต์‹ ์—์„œ๋Š” ์ฟ ํ‚ค๋ฅผ ์ „์†กํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค

์ ์šฉํ•œ ์ฟ ํ‚ค ์˜ต์…˜ ์˜ˆ์‹œ

 

* SameSite ์˜ต์…˜์„ Strict ๋กœ ์„ค์ •ํ•˜๊ฒŒ ๋˜๋ฉด ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์š”์ฒญ์‹œ์—๋Š” ์ฟ ํ‚ค๊ฐ€ ์‹ค๋ฆฌ์ง€ ์•Š์•„ ํ›„์— Lax ์˜ต์…˜์œผ๋กœ ์„ค์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค

 

์ฟ ํ‚ค๋ฅผ ์ฒ˜์Œ์— ์–ด๋–ป๊ฒŒ ์ƒ์„ฑํ• ๊นŒ?

OAuth ๋ฐฉ์‹์˜ ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ๋ณดํ†ต ํŠน์ •ํ•œ callbackURL ์„ ์„ค์ •ํ•˜๊ณ  ๋กœ๊ทธ์ธ์ด ์„ฑ๊ณตํ–ˆ์„ ๊ฒฝ์šฐ code ๋ฅผ ํฌํ•จํ•œ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ํ•จ๊ป˜ callbackURL ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด์ค๋‹ˆ๋‹ค

Next.js ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ง€์ •ํ•œ callbackURL ์— api ๋ผ์šฐํŠธ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์„œ๋ฒ„ ๋‹จ์—์„œ ๋กœ๊ทธ์ธ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const { url } = request
  const queryParams = new URL(url).searchParams
  const code = queryParams.get('code')

  const response = await fetch(`${process.env.๋ฐฑ์—”๋“œ_์„œ๋ฒ„_์ฃผ์†Œ}/๋กœ๊ทธ์ธ_API_๊ฒฝ๋กœ`, {
    method: 'POST',
    body: JSON.stringify({ provider: 'google', code }),
  })

  if (!response.ok) {
    throw new Error('๋กœ๊ทธ์ธ ์‹คํŒจ')
  }

  return NextResponse.redirect(process.env.ํ”„๋ก ํŠธ_์„œ๋ฒ„_์ฃผ์†Œ)
}

/api/oauth/callback/url/route.ts ์˜ˆ์‹œ ์ฝ”๋“œ

 

https://nextjs.org/docs/app/building-your-application/routing/route-handlers

 

 

์œ„ ๋กœ์ง์œผ๋กœ ๋ชจ๋‘ ์ฒ˜๋ฆฌ๋˜๋ฉด ์ข‹๊ฒ ์ง€๋งŒ ๋ฐฑ์—”๋“œ์—์„œ๋Š” Set-Cookie ํ—ค๋”๋ฅผ ํ†ตํ•ด ์„ธ์…˜์•„์ด๋””๋ฅผ ๋„˜๊ฒจ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค

์›๋ž˜๋ผ๋ฉด ๋ธŒ๋ผ์šฐ์ €์— ๋„์ฐฉํ•˜์˜€๊ฒ ์ง€๋งŒ ์–ด๋–ป๊ฒŒ ๋ณด๋ฉด ์ €ํฌ๊ฐ€ ์ค‘๊ฐ„์—์„œ ์š”์ฒญ์„ ํ•œ๋ฒˆ ๊ฐ€๋กœ์ฑ„๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„์˜ ์ฒ˜๋ฆฌ ๋กœ์ง์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค

const responseCookies = response.headers.get('Set-Cookie')
const parsedCookies = responseCookies
  .split(';')
  .map((s) => s.trim().split('='))
  .reduce((obj: { [key: string]: string }, [key, value]) => {
    return Object.assign(obj, { [key]: value ?? true })
  }, {})

cookies().set(SESSION_ID, parsedCookies[SESSION_ID], {
  path: '/',
  httpOnly: true,
  sameSite: 'strict',
  secure: true,
})

์‘๋‹ต๋ฐ›์€ ์ฟ ํ‚ค ํ—ค๋” ํŒŒ์‹ฑ ํ›„ ์ƒˆ๋กœ์šด ์ฟ ํ‚ค ์„ค์ •ํ•˜๋Š” ๋กœ์ง

 

https://nextjs.org/docs/app/api-reference/functions/cookies

 

 

๋งˆ์ง€๋ง‰์œผ๋กœ api ๋ผ์šฐํŠธ์—์„œ ์—๋Ÿฌ๊ฐ€ ํ„ฐ์ง€๋ฉด ๋ฐ”๋กœ 500๋ฒˆ๋Œ€ ์—๋Ÿฌ๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— ์ „๋‹ฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ ์ ˆํ•œ ์—๋Ÿฌ์ฒ˜๋ฆฌ ๋กœ์ง๊ณผ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ™œ์šฉํ•œ ์ธํ„ฐ๋ ‰์…˜ ์ฒ˜๋ฆฌ๊นŒ์ง€ ํ•ด์ฃผ๋ฉด ์™„์„ฑ์ž…๋‹ˆ๋‹ค

try {
  // ๋กœ๊ทธ์ธ ๋กœ์ง
  return NextResponse.redirect(`${process.env.ํ”„๋ก ํŠธ_์„œ๋ฒ„_์ฃผ์†Œ}/?๋กœ๊ทธ์ธ-์„ฑ๊ณต`)
} catch {
  cookies().delete(SESSION_ID) // ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋‚จ์•„์žˆ๋Š” ์ฟ ํ‚ค ์‚ญ์ œ ์ฒ˜๋ฆฌ
  return NextResponse.redirect(`${process.env.ํ”„๋ก ํŠธ_์„œ๋ฒ„_์ฃผ์†Œ}/?๋กœ๊ทธ์ธ-์‹คํŒจ`)
}

 

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ์–ด๋–ป๊ฒŒ ์ ‘๊ทผํ• ๊นŒ?

Next.js ์—์„œ๋Š” ์„œ๋ฒ„ ์ธก์—์„œ ๋™์ž‘ํ•˜๋Š” fetch ํ•จ์ˆ˜๋ฅผ ํ™•์žฅํ•˜์—ฌ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค

์ž์ฒด์ ์œผ๋กœ ์ง€์›ํ•˜๋Š” cookies API๋ฅผ ํ†ตํ•ด ์š”์ฒญ ํ—ค๋”์— ์ฟ ํ‚ค๋ฅผ ์ง์ ‘ ๋‹ด์•„์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

'use server'

import { cookies } from 'next/headers'

export default async function getUserInfo() {
  const response = await fetch(`${process.env.๋ฐฑ์—”๋“œ_์„œ๋ฒ„_์ฃผ์†Œ}/์ธ์ฆ์ด_ํ•„์š”ํ•œ_API_๊ฒฝ๋กœ`, {
    headers: {
      'Content-Type': 'application/json',
      Cookie: `${SESSION_ID}=${cookies().get(SESSION_ID)?.value}`,
    },
    credentials: 'include',
    cache: 'no-store',
    next: { revalidate: 0 },
  })

  if (!response.ok) {
    throw new Error('์—๋Ÿฌ ๋ฐœ์ƒ')
  }

  const { data } = await res.json()

  return { data, response }
}

 

cookies API๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ ๋˜‘๊ฐ™์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค

์•„๋ž˜๋Š” ์ด๋ฏธ ๋กœ๊ทธ์ธ ํ•œ ์œ ์ €๋ฅผ ๋ฉ”์ธํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ํฌํ•จํ•œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค

import { cookies } from 'next/headers'

export default function SignInPage() {
  const isLogin = cookies().has(SESSION_ID)

  if (isLogin) {
    return redirect(`${process.env.ํ”„๋ก ํŠธ_์„œ๋ฒ„_์ฃผ์†Œ}/?์ด๋ฏธ-๋กœ๊ทธ์ธ-๋˜์–ด์žˆ์Œ`)
  }

  return <SignIn />
}

 

์ƒํƒœ ๊ด€๋ฆฌ๋Š” ์–ด๋–ป๊ฒŒ ํ–ˆ๋‚˜?

ํ˜„์žฌ๊นŒ์ง€์˜ ๋กœ์ง์œผ๋กœ๋Š” ๋กœ๊ทธ์ธํ•œ ์งํ›„๋Š” ์ž˜ ๋™์ž‘ํ•˜์ง€๋งŒ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ๋กœ๊ทธ์ธ์ด ํ’€๋ฆฌ๊ฒŒ๋ฉ๋‹ˆ๋‹ค

์ €ํฌ๋Š” Next.js ์˜ layout.tsx ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ฃจํŠธ ๊ฒฝ๋กœ์— ์„œ๋ฒ„์šฉ ํ›…์„ ์‚ฌ์šฉํ•˜์—ฌ ์œ ์ € ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜จ ํ›„, props ๋ฅผ ํ†ตํ•ด context api ์— ๋„˜๊ฒจ์ฃผ์–ด ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค

'use server'

import type { PropsWithChildren } from 'react'

export default async function RootLayout({ children }: PropsWithChildren) {
  const authProps = await useSession()

  return <AuthProvider {...authProps}>{children}</AuthProvider>
}     

/app/layout.tsx ์˜ˆ์‹œ ์ฝ”๋“œ

'use server'

import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'

export default async function useSession(): Promise<DefaultAuthState> {
  const cookieStore = cookies()
  const sessionId = cookieStore.get(SESSION_ID)?.value
  const isLogin = !!sessionId

  if (!isLogin) {
    return { isLogin: false }
  }
  
  try {
    // ์œ ์ € ์ •๋ณด ์กฐํšŒ ๋กœ์ง
    return { isLogin: true }
  } catch {
    return redirect(`${process.env.ํ”„๋ก ํŠธ_์„œ๋ฒ„_์ฃผ์†Œ}/?๋กœ๊ทธ์ธ-์‹คํŒจ`)
  }
}

useSession ์˜ˆ์‹œ ์ฝ”๋“œ

 

๋ฃจํŠธ ๊ฒฝ๋กœ์˜ layout.tsx ๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๊ธฐ ๋•Œ๋ฌธ์— ์ƒˆ๋กœ๊ณ ์นจ์„ ํ•  ๋•Œ ๋งˆ๋‹ค ์„œ๋ฒ„์—์„œ ์œ ์ € ์ •๋ณด๋ฅผ ๊ฐฑ์‹ ํ•˜์—ฌ context api ์— ์ธ๋ฉ”๋ชจ๋ฆฌ๋กœ ์ €์žฅ๋˜์–ด ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์ธ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค

 

'use client'

import { PropsWithChildren, createContext, useContext } from 'react'

const AuthContext = createContext<DefaultAuthState>({
  isLogin: false,
})

export default function useAuth() {
  return useContext(AuthContext)
}

export default function AuthProvider({
  children,
  ...authProps
}: PropsWithChildren<DefaultAuthState>) {
	return (
    <AuthContext.Provider value={authProps}>{children}</AuthContext.Provider>
  )
}

AuthProvider ์˜ˆ์‹œ ์ฝ”๋“œ

 

Protected Route ๊ตฌํ˜„

Protected Route ์œ ํ‹ธ์€ ๋ณดํ†ต HOC ํŒจํ„ด์œผ๋กœ ๋งŽ์ด ๊ตฌํ˜„ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค

ํ•˜์ง€๋งŒ ์ €ํฌ๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์ธ page.tsx ์—์„œ ์‚ฌ์šฉํ•ด์•ผ ํ–ˆ์ง€๋งŒ, context api ๋ฅผ ํ™œ์šฉํ•œ useAuth ๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ์‹คํ–‰๋  ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค

๊ทธ๋ž˜์„œ ์ €ํฌ๋Š” ์กฐ๊ธˆ์€ ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ children props ๋ฅผ ํ†ตํ•ด withAuth ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค

์ตœ๊ทผ jotai ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ๋Š” ์ „์—ญ ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ SSR ํ™˜๊ฒฝ์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์•„ ์ถ”ํ›„์—๋Š” HOC ํŒจํ„ด์œผ๋กœ ๋ฆฌํŒฉํ† ๋ง ํ•ด๋ณผ ์ˆ˜๋„ ์žˆ๊ฒ ์Šต๋‹ˆ๋‹ค

'use client'

import type { PropsWithChildren } from 'react'
import { useAuth } from '../model'
import NeedLogin './NeedLogin'

export default function WithAuth({ children }: PropsWithChildren) {
  const { isLogin } = useAuth()

  return isLogin ? children : <NeedLogin />
}

WithAuth.tsx ์˜ˆ์‹œ ์ฝ”๋“œ

'use server'

export default function MyPage() {
  return (
    <WithAuth>
      <MyPageContent />
    </WithAuth>
  )
}

WithAuth.tsx ์‚ฌ์šฉ ์˜ˆ์‹œ ์ฝ”๋“œ

WithAuth ๊ฒฐ๊ณผ UI

 

๊ฒฐ๋ก 

๋‹น์—ฐํ•˜๊ฒ ์ง€๋งŒ ํ˜„์žฌ ์ฐจ์šฉํ•˜๊ณ  ์žˆ๋Š” ๋ฐฉ์‹ ๋˜ํ•œ ์™„๋ฒฝํ•  ์ˆ˜ ์—†๊ณ , ์žฅ๋‹จ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค

์„ธ์…˜ ๋ฐฉ์‹์€ ์„œ๋ฒ„์— ๋ถ€๋‹ด์ด ๋งŽ์ด ๊ฐ€๊ธฐ ๋•Œ๋ฌธ์— ๋งŒ๋ฃŒ๋˜๋Š” ์ฃผ๊ธฐ๊ฐ€ ์งง์•„ ์œ ์ €๊ฐ€ ๋‹ค์‹œ ๋กœ๊ทธ์ธ์„ ์ˆ˜ํ–‰ํ•ด์•ผํ•˜๋Š” ์ฃผ๊ธฐ๊ฐ€ ์งง์•„์กŒ์œผ๋ฉฐ, ์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๋•Œ์˜ ์—๋Ÿฌ์ฒ˜๋ฆฌ ๋˜ํ•œ ์‰ฝ์ง€๋งŒ์€ ์•Š์•˜์Šต๋‹ˆ๋‹ค

๋˜ํ•œ ๊ฑฐ์˜ ๋ชจ๋“  ์œ ์ € ๊ด€๋ จ API ํ†ต์‹  ๋กœ์ง์„ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฒ˜๋ฆฌํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค

๋” ๋‚˜์€ ์ธ์ฆ ๋ฐฉ์‹์„ ์•Œ๊ณ  ์žˆ๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ์˜๊ฒฌ์ด ์žˆ๋‹ค๋ฉด ๊ณต์œ ํ•ด ์ฃผ์‹œ๊ธธ ๋ถ€ํƒ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค