๊ด€๋ฆฌ ๋ฉ”๋‰ด

Coding Archive

[Next.js] Next.js๋กœ HttpOnly ์ฟ ํ‚ค + BFF(ํ”„๋ก์‹œ) ์•„ํ‚คํ…์ฒ˜๋กœ ์ธ์ฆ ๋ณด์•ˆ/์„ฑ๋Šฅ ์žก๊ธฐ(๋ฒˆ์™ธํŽธ) - ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋Œ€์‹  BFF ์•„ํ‚คํ…์ฒ˜๋ฅผ ํƒํ•œ ์ด์œ  ๋ณธ๋ฌธ

๐Ÿ’ป Programming/Next.js & project

[Next.js] Next.js๋กœ HttpOnly ์ฟ ํ‚ค + BFF(ํ”„๋ก์‹œ) ์•„ํ‚คํ…์ฒ˜๋กœ ์ธ์ฆ ๋ณด์•ˆ/์„ฑ๋Šฅ ์žก๊ธฐ(๋ฒˆ์™ธํŽธ) - ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋Œ€์‹  BFF ์•„ํ‚คํ…์ฒ˜๋ฅผ ํƒํ•œ ์ด์œ 

์ฝ”๋“ฑ์–ด 2025. 8. 23. 13:20

์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋Œ€์‹  BFF ์•„ํ‚คํ…์ฒ˜๋ฅผ ํƒํ•œ ์ด์œ  ์ด๋ฏธ์ง€

 

 

์ด์ „์— ์˜ฌ๋ฆฐ ๊ธ€์—์„œ Next.js BFF(Backend For Frontend, ํ”„๋ก์‹œ) ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ ์šฉํ•ด, HttpOnly ์ฟ ํ‚ค ๊ธฐ๋ฐ˜ ์ธ์ฆ์œผ๋กœ ๋ณด์•ˆ๊ณผ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๋Š” ๊ณผ์ •์„ ์†Œ๊ฐœํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ํ”„๋ก์‹œ ๊ตฌ์กฐ๋ฅผ ๋„์ž…ํ•˜๊ธฐ ์ „, ์ด๋Ÿฐ ์˜๋ฌธ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

“๊ตณ์ด ํ”„๋ก์‹œ๊นŒ์ง€ ์“ฐ์ง€ ์•Š๊ณ , ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ + ์ธํ„ฐ์…‰ํ„ฐ ๊ตฌ์กฐ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ?”

 

์‹ค์ œ๋กœ ์ด์ „์˜ ํ”„๋กœ์ ํŠธ์—์„œ react-cookie + Axios ์ธํ„ฐ์…‰ํ„ฐ ์กฐํ•ฉ์œผ๋กœ ์ธ์ฆ์„ ๊ตฌํ˜„ํ•œ ์ ์ด ์žˆ์—ˆ๊ณ , ์ž‘์€ ๊ทœ๋ชจ์˜ ํ”„๋กœ์ ํŠธ์—์„œ ์ด ๋ฐฉ์‹์ด ๊ฝค ๋‹จ์ˆœํ•˜๊ณ  ๋น ๋ฅด๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋‚˜์˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ, ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ์œผ๋กœ๋Š” HttpOnly ์ฟ ํ‚ค์˜ ๋ณด์•ˆ ์žฅ์ ์„ ์‚ด๋ฆด ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” ๊ทธ ์ด์œ ์™€ ํ•จ๊ป˜ ๊ฐ ๋ฐฉ์‹์„ ์ž์„ธํžˆ ๋น„๊ตํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

 


 

๋Œ€๋ถ€๋ถ„์˜ ํ”„๋ก ํŠธ์—”๋“œ ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์‚ฌ์‹ค์ƒ HttpOnly ์ฟ ํ‚ค๋ฅผ ๋‹ค๋ฃฐ ์ˆ˜ ์—†๋‹ค!

 

HttpOnly ์ฟ ํ‚ค๋Š” ๋ธŒ๋ผ์šฐ์ € JS์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€

  • ์ด๊ฑด ๋ธŒ๋ผ์šฐ์ €์˜ ๋ณด์•ˆ ์ •์ฑ…(Web Standard)์ด๋ผ, ์–ด๋–ค JS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“  ๋šซ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
  • react-cookie, next-cookie, universal-cookie, js-cookie ์ „๋ถ€ ๊ณตํ†ต์ ์œผ๋กœ HttpOnly ์ฟ ํ‚ค๋ฅผ ์ฝ๊ฑฐ๋‚˜ ์“ธ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

 

์ •๋ฆฌํ•˜๋ฉด, ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์‚ฌ์‹ค์ƒ non-HttpOnly ์ฟ ํ‚ค(=JS ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ์ฟ ํ‚ค)๋ฅผ ๋‹ค๋ฃจ๋Š” ๋„๊ตฌ์ผ ๋ฟ์ž…๋‹ˆ๋‹ค. 

 

 


 

 

! ์—ฌ๊ธฐ์„œ ์ž ์‹œ ์ฃผ์š” ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ณ„ ์ œ์•ฝ์‚ฌํ•ญ์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ค‘์š”ํ•œ ๋‚ด์šฉ์€ ์•„๋‹ˆ๋‹ˆ ๊ฒฐ๋ก ๋งŒ ๋ณด๊ณ  ๋„˜์–ด๊ฐ€๋„ ์ƒ๊ด€์—†์Šต๋‹ˆ๋‹ค.

โœ”๏ธ ์ฃผ์š” ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ณ„ ์ œ์•ฝ์‚ฌํ•ญ

1. react-cookie

import { useCookies } from 'react-cookie';

function MyComponent() {
  const [cookies, setCookie] = useCookies(['token']);
  
  // โŒ HttpOnly ์ฟ ํ‚ค๋Š” ์ฝ์„ ์ˆ˜ ์—†์Œ
  console.log(cookies.token); // undefined (HttpOnly์ธ ๊ฒฝ์šฐ)
  
  // โŒ HttpOnly ์ฟ ํ‚ค๋Š” ์„ค์ •ํ•  ์ˆ˜ ์—†์Œ (ํด๋ผ์ด์–ธํŠธ์—์„œ)
  setCookie('token', 'value', { httpOnly: true }); // ๋ฌด์‹œ๋จ
}
  • ํด๋ผ์ด์–ธํŠธ์—์„œ document.cookie๋ฅผ ๊ฐ์‹ธ๋Š” ๋ž˜ํผ
  • SSR์—์„œ๋Š” req.headers.cookie๋ฅผ ํŒŒ์‹ฑํ•˜๋Š” ๋ณด์กฐ ์œ ํ‹ธ ์ •๋„

2. next-cookie / cookies-next

import { getCookie, setCookie } from 'cookies-next';

// โŒ HttpOnly ์ฟ ํ‚ค ์ฝ๊ธฐ ๋ถˆ๊ฐ€๋Šฅ
const token = getCookie('httponly-token'); // undefined

// โŒ ํด๋ผ์ด์–ธํŠธ์—์„œ HttpOnly ์ฟ ํ‚ค ์„ค์ • ๋ถˆ๊ฐ€๋Šฅ
setCookie('token', 'value', { httpOnly: true }); // ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฌด์‹œ
  • Next.js์˜ SSR/์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฟ ํ‚ค๋ฅผ ํŽธํ•˜๊ฒŒ ์ฝ๊ณ  ์“ฐ๋Š” ์šฉ๋„.
  • ์„œ๋ฒ„ ์ฝ”๋“œ์—์„œ๋Š” NextResponse.cookies.set()๋กœ HttpOnly ๊ฐ€๋Šฅ
  • ํ•˜์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ JS์—์„œ๋Š” ์—ญ์‹œ ๋ถˆ๊ฐ€

3. js-cookie / universal-cookie

import Cookies from 'js-cookie';

// โŒ HttpOnly ์ฟ ํ‚ค ์ ‘๊ทผ ๋ถˆ๊ฐ€๋Šฅ
Cookies.get('httponly-token'); // undefined

// โŒ HttpOnly ์˜ต์…˜ ์ž์ฒด๊ฐ€ ์ง€์›๋˜์ง€ ์•Š์Œ
Cookies.set('token', 'value'); // ํ•ญ์ƒ non-HttpOnly
  • ์ˆœ์ˆ˜ ํด๋ผ์ด์–ธํŠธ ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ.
  • ์—ญ์‹œ HttpOnly ์ฟ ํ‚ค๋Š” ๋‹ค๋ฃฐ ์ˆ˜ ์—†์Œ.

 


 

โœ…  ๊ฒฐ๋ก ์ ์œผ๋กœ ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š”

next-cookie, react-cookie๋„ HttpOnly ์ฟ ํ‚ค ์ž์ฒด๋ฅผ JS์—์„œ ์ œ์–ดํ•  ์ˆ˜๋Š” ์—†์Šต๋‹ˆ๋‹ค.

HttpOnly ์ฟ ํ‚ค๋Š” ์„œ๋ฒ„(Set-Cookie ํ—ค๋”)์—์„œ๋งŒ ์ œ์–ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์–ด๋””๊นŒ์ง€๋‚˜ non-HttpOnly ์ฟ ํ‚ค์šฉ ํŽธ์˜ ๊ธฐ๋Šฅ์ด๋ผ๊ณ  ๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

 

 


 

 

๐Ÿ” ์ฟ ํ‚ค ์ธ์ฆ ๋ฐฉ์‹ Trade-off ๋น„๊ต

1. ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ + ์ธํ„ฐ์…‰ํ„ฐ ๋ฐฉ์‹

// Axios ์ธํ„ฐ์…‰ํ„ฐ ์˜ˆ์‹œ
import { getCookie } from 'cookies-next';
import axios from 'axios';

axios.interceptors.request.use((config) => {
  const token = getCookie('token'); // โš ๏ธ non-HttpOnly๋งŒ ๊ฐ€๋Šฅ
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

 

2. BFF(ํ”„๋ก์‹œ) + HttpOnly ์ฟ ํ‚ค ๋ฐฉ์‹

// Next.js API Route
export async function POST(request) {
  // โœ… HttpOnly ์ฟ ํ‚ค์—์„œ ํ† ํฐ ์ฝ๊ธฐ
  const token = request.cookies.get('auth-token')?.value;
  
  if (!token) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  
  // โœ… ๋ฐฑ์—”๋“œ์— ์ธ์ฆ๋œ ์š”์ฒญ ์ „๋‹ฌ
  const response = await fetch('https://api.example.com/data', {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });
  
  return NextResponse.json(await response.json());
}

 

๊ตฌ๋ถ„ ์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ + ์ธํ„ฐ์…‰ํ„ฐ ๋ฐฉ์‹ BFF(ํ”„๋ก์‹œ) + HttpOnly ์ฟ ํ‚ค ๋ฐฉ์‹
๋ณด์•ˆ โŒ HttpOnly ๋ถˆ๊ฐ€ → XSS ์ทจ์•ฝ
โŒ ํ† ํฐ์ด JS ๋Ÿฐํƒ€์ž„์— ๋…ธ์ถœ
โš ๏ธ CSRF ๋ฐฉ์–ด ๋”ฐ๋กœ ํ•„์š”
โœ… HttpOnly ์ฟ ํ‚ค ์‚ฌ์šฉ ๊ฐ€๋Šฅ
โœ… ํ† ํฐ JS ์ ‘๊ทผ ๋ถˆ๊ฐ€ → XSS ๋ฐฉ์–ด
โœ… CSRF ํ† ํฐ ๊ฒ€์ฆ์„ ์„œ๋ฒ„์—์„œ ํ†ตํ•ฉ ๊ด€๋ฆฌ
ํŽธ์˜์„ฑ (๊ฐœ๋ฐœ ๋‚œ์ด๋„) โœ… ๊ตฌํ˜„ ๊ฐ„๋‹จ
โœ… ๋น ๋ฅธ ๊ฐœ๋ฐœ/ํ”„๋กœํ† ํƒ€์ž… ์ ํ•ฉ
โœ… ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(document.cookie) ๊ธฐ๋ฐ˜
โŒ ํ”„๋ก์‹œ ์„œ๋ฒ„ ํ•„์š”
โŒ ์ฝ”๋“œ ๊ตฌ์กฐ ๋ณต์žก
โŒ ์ดˆ๊ธฐ ๊ฐœ๋ฐœ ๋น„์šฉ ↑
ํ™•์žฅ์„ฑ (์žฅ๊ธฐ์  ์šด์˜) โŒ ๋ณด์•ˆ ๋ฌธ์ œ๋กœ ๋Œ€๊ทœ๋ชจ ์„œ๋น„์Šค์— ๋ถ€์ ํ•ฉ
โŒ API Gateway ์—ญํ•  ๋ถˆ๊ฐ€๋Šฅ
โœ… ๋ณด์•ˆ ํ™•์žฅ์„ฑ ๋†’์Œ
โœ… ๋กœ๊น…/๋ ˆ์ดํŠธ๋ฆฌ๋ฐ‹/A/B ํ…Œ์ŠคํŠธ ๋“ฑ ์ œ์–ด ๊ฐ€๋Šฅ
โœ… ์‹ค์„œ๋น„์Šค ํ‘œ์ค€ ํŒจํ„ด

 


 

 

โœ… ์ •๋ฆฌ

์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ => ๊ฐ„๋‹จํ•˜์ง€๋งŒ HttpOnly ์ฟ ํ‚ค๋ฅผ ๋ชป ์“ฐ๋ฏ€๋กœ XSS ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค.
ํ”„๋ก์‹œ ๋ฐฉ์‹ => ์ฝ”๋“œ๊ฐ€ ์ข€ ๋ณต์žกํ•ด์ง€์ง€๋งŒ, ๋ณด์•ˆ ์—…๊ณ„ ํ‘œ์ค€์ด๋ฉฐ ์žฅ๊ธฐ์ ์œผ๋กœ๋Š” ํ™•์žฅ์„ฑ๋„ ์ข‹์Šต๋‹ˆ๋‹ค.

 

 

 


 

 

 

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

๊ฒฐ๋ก ์€ ๋‹จ์ˆœํ•ฉ๋‹ˆ๋‹ค.
์ฟ ํ‚ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๊ฒฐ๊ตญ non-HttpOnly ์ฟ ํ‚ค๋งŒ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๊ณ , ๋ณด์•ˆ ์ทจ์•ฝ์ ์„ ํ”ผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
์ € ์—ญ์‹œ ์ด์ „ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” react-cookie + ์ธํ„ฐ์…‰ํ„ฐ ๊ตฌ์กฐ๋ฅผ ํ™œ์šฉํ–ˆ์ง€๋งŒ, ๊ณง ๋ณด์•ˆ์ƒ์˜ ํ•œ๊ณ„๋ฅผ ์ฒด๊ฐํ•˜๊ฒŒ ๋˜์—ˆ๊ณ , ์ด๋ฒˆ์—๋Š” ๋ณด๋‹ค ์•ˆ์ „ํ•œ ๊ตฌ์กฐ์ธ BFF(ํ”„๋ก์‹œ) + HttpOnly ์ฟ ํ‚ค ์•„ํ‚คํ…์ฒ˜๋กœ ๋ฐœ์ „์‹œ์ผœ ๊ฐœ๋ฐœํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์•ž์œผ๋กœ๋Š” ์ด ๊ฒฝํ—˜์„ ํ† ๋Œ€๋กœ ๋”์šฑ ์•ˆ์ •์ ์ด๊ณ  ํ™•์žฅ์„ฑ ์žˆ๋Š” ์ธ์ฆ/๋ณด์•ˆ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

 

Comments