인증과 Next.js (4) - BFF 패턴 & Proxy Handler, Next.js를 API 게이트웨이로
시리즈 목차: 인증과 Next.js
- (1편) Next.js + Axios와 CORS 에러 제대로 이해하기
- (2편) 쿠키(Cookie) 완전 정복 — Secure, HttpOnly, SameSite의 진짜 의미
- (3편) Axios + 쿠키 인증 — withCredentials와 CORS의 콜라보
- (4편) BFF 패턴 & Proxy Handler — Next.js를 API 게이트웨이로 ← 현재 글

(4) BFF 패턴 & Proxy Handler — Next.js를 API 게이트웨이로
앞의 세 편에서 CORS, 쿠키 보안, withCredentials의 복잡한 조건들을 살펴봤다.
"이 모든 걸 다 맞춰야 동작한다고 생각하면, 너무 복잡한 것 같다."
맞다. 복잡하다. 그래서 Next.js는 이 복잡함을 아키텍처 수준에서 해결하는 방법을 제공한다. 바로 BFF(Backend for Frontend) 패턴과 Proxy 핸들러다.
이 글에서는 Next.js를 단순 프론트엔드 서버가 아닌 API 게이트웨이로 활용하는 방법과, 각 기능의 책임과 적합한 사용 시나리오를 정리합니다. 예시는 Next.js 앱을 기준으로 들지만, **"프론트엔드 서버를 BFF 레이어로 세운다"**는 관점은 다른 SPA 프레임워크(React, Vue, Svelte 등)에서도 그대로 응용할 수 있습니다.
1. BFF(Backend for Frontend) 패턴이란
BFF는 "프론트엔드를 위한 전용 백엔드"라는 의미입니다. 프론트엔드와 실제 백엔드 API 사이에 중간 레이어를 두는 아키텍처 패턴입니다.
기존 구조의 문제점
[브라우저] ─────────────► [백엔드 API 서버]
Client Component api.example.com
axios로 직접 호출
문제:
- CORS 설정 복잡함
- API Key가 클라이언트에 노출됨(NEXT_PUBLIC_)
- 여러 API를 조합하는 로직이 클라이언트에 존재할 수 있음
- 인증 토큰이 브라우저에 노출될 수 있음
BFF 구조
가장 간략한 구조를 그려보자면 다음과 같습니다.
[브라우저] ──► [Next.js BFF] ──► [백엔드 API 서버]
Route Handler api.example.com
(내 서버) (외부 서버)
이 구조는 BFF 레이어를 중심으로 더 확장될 수 있습니다.
BFF가 해결하는 것들
① CORS 완전 우회
CORS는 브라우저 즉, 클라이언트에서 발생하는 문제라고 했었죠?
중간에 BFF 레이어로써 작용하는 Next.js 서버가 들어오는 것만으로 CORS 문제가 구조적으로 사라질 수 있습니다.
1) 브라우저 ↔ Next.js: Same-Origin → CORS 없음
브라우저는 자신이 속한 Next.js 서버(my-app.com)와 통신하므로 Same-Origin이므로 CORS가 없습니다.
2) Next.js ↔ 외부 API: 서버 간 통신 → CORS 없음
실제 외부 API 호출은 서버에서 서버로 일어나므로 역시 CORS가 없습니다.
② 민감한 정보 은닉 서버를 거치기에 API Key, 내부 서버 주소, 토큰 등이 클라이언트에 전혀 노출되지 않습니다.
ts// ✅ API Key가 서버에만 존재 const res = await fetch("https://api.example.com/data", { headers: { Authorization: `Bearer ${process.env.API_SECRET}`, // process.env.API_SECRET → 서버 전용, 클라이언트 번들에 포함 안 됨 }, });
③ 데이터 집계 및 변환
클라이언트가 필요한 형태로 여러 API 응답을 조합하거나 불필요한 필드를 제거할 수 있습니다.
ts// 클라이언트가 필요한 데이터만 골라서 전달 const [user, orders, notifications] = await Promise.all([ fetch("/internal/user/42").then((r) => r.json()), fetch("/internal/orders?userId=42").then((r) => r.json()), fetch("/internal/notifications?userId=42").then((r) => r.json()), ]); return Response.json({ user: { name: user.name, email: user.email }, // 민감한 필드 제거 orderCount: orders.total, hasUnread: notifications.unread > 0, });
아래는 기존 글의 흐름을 유지하면서 독자가 헷갈리기 쉬운 부분을 보완한 버전이다. 특히 다음 부분을 보강했다.
- 5단계 파이프라인 vs 3레이어 구조의 관계
- Route Handler와 Server Component의 역할 차이
- 왜 BFF가 Route Handler에 위치하는지
- Middleware가 BFF가 아닌 이유
블로그에 그대로 넣어도 자연스럽게 읽히도록 작성했다.
2. 사전 지식: Next.js의 세 가지 서버 레이어
더 자세히 들어가봅시다.
Next.js에서 서버 측으로 BFF를 처리하기 위해서는, Next.js에서 요청이 어떤 순서로 처리되는지부터 이해하는 것이 좋습니다.
Next.js 공식 문서 기준으로, 한 요청이 처리되는 파이프라인을 프레임워크 내부 관점에서 보면 대략 다음과 같습니다.
프레임워크 내부 요청 처리 파이프라인 (요약)
1. next.config.js의 headers
2. next.config.js의 redirects
3. Proxy/Middleware (proxy.ts 혹은 middleware.ts)
4. next.config.js의 rewrites
5 Rendered Route
├ route handler (API)
└ page / server component (UI)
여기서 중요한 점은 1~4단계는 라우트가 실행되기 전 단계이고, 실제 애플리케이션 코드가 실행되는 시점은 **5단계(Rendered Route)**라는 것입니다.
즉, 우리가 작성하는 대부분의 서버 코드는 5단계 안에서 동작합니다.
Rendered Route 내부 구조
Rendered Route 단계에 도달하면 Next.js는 해당 경로가 API인지, 페이지인지를 판단하여 적절한 코드를 실행합니다.
Rendered Route
├ route handler (API endpoint)
└ page / server component (UI rendering)
- Route Handler는
/api/...같은 경로에서 직접 HTTP 응답을 생성하는 API 엔드포인트입니다. - Page / Server Component는 UI를 렌더링하면서 서버에서 데이터를 가져오는 레이어입니다.
이 글에서 다루는 BFF(Backend For Frontend) 로직은 대부분 Route Handler에서 구현됩니다.
5단계? 좀 더 직관적으로, 앱 서버 관점에서의 3가지 레이어
하지만 실제 애플리케이션 구조를 이해할 때는 위의 프레임워크 파이프라인보다, 개발자가 작성하는 서버 레이어 기준으로 보는 것이 더 직관적입니다.
앱 서버 관점에서 보면 Next.js 요청 처리는 다음 세 가지 레이어로 나눌 수 있습니다.
-
proxy / middleware 라우트에 도달하기 전에 실행되는 전역 프록시. 인증 검사, 리다이렉트, 요청 헤더 수정 등의 공통 로직을 처리합니다.
-
route handler 특정 URL 경로에 대한 API 엔드포인트. BFF 로직, 외부 API 프록시, 인증 처리 등을 담당합니다.
-
page / server component UI를 렌더링하는 레이어. 서버에서 데이터를 직접 가져와 HTML을 생성합니다.
이 세 레이어의 관계는 다음과 같이 정리할 수 있습니다.
mermaid
왜 BFF는 Route Handler에 위치할까?
BFF는 보통 다음과 같은 역할을 수행합니다.
- 외부 API 호출
- 인증 토큰 처리
- 응답 데이터 가공
- 여러 API 응답을 하나로 합치기
이러한 작업은 HTTP 요청과 응답을 직접 제어해야 하는 로직이기 때문에 Route Handler에서 구현하는 것이 가장 자연스럽습니다.
Middleware는 왜 BFF가 아닐까?
많은 사람들이 처음에 헷갈리는 부분이 바로 이것입니다.
Middleware도 요청을 가로채기 때문에 BFF처럼 보일 수 있습니다.
하지만 Middleware는 다음과 같은 제약이 있습니다.
- Edge Runtime에서 실행
- 응답 생성 로직이 제한적
- 무거운 비즈니스 로직에 부적합
그래서 Middleware는 보통 게이트웨이 역할에 가깝다고 볼 수 있습니다.
예를 들면 다음과 같은 작업을 할 수 있을 겁니다.
- 로그인 여부 검사
- 권한 없는 사용자 리다이렉트
- 특정 경로 접근 차단
최종적으로 각 레이어의 역할을 정리하면 다음과 같습니다.
| 레이어 | 역할 |
|---|---|
| Middleware | 요청을 검사하고 라우팅을 제어하는 게이트웨이 |
| Route Handler | 외부 API와 통신하는 BFF 레이어 |
| Server Component | 데이터를 사용해 UI를 렌더링하는 프레젠테이션 레이어 |
3. Route Handler — BFF의 핵심
자 이제 BFF 역할을 할 Next.js의 Route Handler에 대해서 알아봅시다.
Route Handler는 Next.js에서 HTTP 엔드포인트를 만드는 기능입니다.
app/api/ 하위에 route.ts 파일을 생성하면 해당 경로가 API 엔드포인트가 됩니다(앱 라우터 기준).
app/
├── api/
│ ├── auth/
│ │ ├── login/route.ts → POST /api/auth/login
│ │ └── logout/route.ts → POST /api/auth/logout
│ └── user/
│ └── route.ts → GET /api/user
└── dashboard/
└── page.tsx
예시 1: 기본 Route Handler
ts// app/api/user/route.ts (예시) import { cookies } from "next/headers"; export async function GET() { const cookieStore = await cookies(); const sessionId = cookieStore.get("session_id")?.value; if (!sessionId) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } // 서버 간 통신 → CORS 없음, API Key 안전하게 사용 가능 const backendResponse = await fetch(`${process.env.INTERNAL_API_BASE_URL}/user`, { headers: { "X-Session-Id": sessionId, "X-API-Key": process.env.API_SECRET!, }, }); if (!backendResponse.ok) { return Response.json({ error: "Failed to fetch user" }, { status: backendResponse.status }); } const user = await backendResponse.json(); // 민감한 필드는 제거하고 응답 return Response.json({ id: user.id, name: user.name, email: user.email, }); }
저 handler의 호출 흐름은 다음과 같습니다.
- 브라우저는
/api/user를 호출한다.- Same-Origin이므로 CORS 없음.
- 서버가
process.env.INTERNAL_API_URL로 내부 API를 호출한다.- 서버 간 통신이므로 역시 CORS 없음.
- API Key는 서버 환경변수에 안전하게 보관됨.
예시 2: 로그인 처리 — 쿠키 설정
ts// app/api/auth/login/route.ts (예시) import { cookies } from "next/headers"; export async function POST(request: Request) { const { email, password } = await request.json(); // 백엔드 인증 API 호출 const backendResponse = await fetch(`${process.env.INTERNAL_API_BASE_URL}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); if (!backendResponse.ok) { return Response.json({ error: "Invalid credentials" }, { status: 401 }); } const { sessionId, refreshToken } = await backendResponse.json(); const cookieStore = await cookies(); // 액세스용 세션 쿠키 cookieStore.set("session_id", sessionId, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", maxAge: 60 * 60, // 1시간 path: "/", }); // 갱신용 리프레시 토큰 cookieStore.set("refresh_token", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", maxAge: 60 * 60 * 24 * 30, // 30일 path: "/api/auth/refresh", // 갱신 엔드포인트에만 전송 }); return Response.json({ success: true }); }
살펴보시면, 직접 클라이언트에서 외부 API 서버에 요청을 할 때의 설정에 비해, 훨씬 간단합니다.
- 클라이언트에서
SameSite: 'lax'와 같은 옵션을 사용할 수 있습니다. - 브라우저와 Next.js 서버가 Same-Origin이기 때문에 CORS 이슈가 없습니다.
- 3편에서 설명한
SameSite=None; Secure의 복잡한 조건이 필요 없습니다.
(추가) 기존 클라이언트 → API 구조에서 BFF로 옮길 때, 백엔드에 요청해야 할 쿠키 옵션들
지금까지 예시는 Next.js의 Route Handler가 직접 쿠키를 설정하는 구조였습니다.
하지만 현실 세계에서는 이미 백엔드(API 서버)가 Set-Cookie로 세션/토큰을 내려주고,
프론트는 단순히 클라이언트 → API 서버로 직접 붙던 구조에서 출발하는 경우가 훨씬 많습니다.
이런 상황에서 **“이제는 프론트가 BFF(Next.js)를 거쳐서 API 서버로 가게 바꾸자”**라고 결정하면,
브라우저 입장에서는 라우팅 구조와 도메인이 바뀌기 때문에 쿠키 설정도 함께 정리해 줘야 합니다.
특히, 아래 옵션들은 백엔드 팀과 합의해서 변경을 요청해야 할 가능성이 높은 부분입니다.
저의 최근 경험입니다.
1) Domain — 브라우저가 실제로 붙는 도메인 기준으로
- 과거 구조
- 브라우저가
api.example.com에 직접 요청을 보내고, API 서버가Set-Cookie를 내려줬을 수 있습니다. - 이때는
Domain=api.example.com이거나, 아예 생략해서 “해당 호스트 한정”으로 두어도 문제가 없습니다(저는 생략했으며, 오히려 더 안전할 수 있습니다).
- 브라우저가
- BFF 도입 후 구조
- 브라우저는 이제
app.example.com/my-app.com처럼 BFF가 올라간 도메인으로만 요청을 보냅니다. - 쿠키도 이 도메인 기준으로 설정되어야, 브라우저가 BFF 요청에 쿠키를 붙여서 보냅니다.
- 브라우저는 이제
실무에서 백엔드 팀에 요청할 때는 보통 이렇게 정리해서 전달하면 좋습니다.
- 프론트/BFF와 동일 호스트에서만 쓰는 경우
- 예: 서비스가
app.example.com하나에서만 동작하고, BFF도 여기에 올라가 있다면Domain=app.example.com또는 Domain 옵션을 생략(호스트 한정) 해도 됩니다.
- 예: 서비스가
- 여러 서브도메인에서 공유해야 하는 경우
- 예:
app.example.com,admin.example.com등 여러 앱에서 같은 세션을 쓰고 싶다면Domain=.example.com처럼 루트 도메인 기준으로 지정합니다.
- 예:
중요한 포인트는 하나입니다.
여전히
Domain=api.example.com으로 고정해 두면,
브라우저가app.example.com(BFF)으로 요청을 보낼 때 그 쿠키를 전송하지 않을 수 있다.
그래서 BFF로 마이그레이션할 때는,
- “이제 쿠키를 기준으로 인증을 처리하는 주체는
api.example.com이 아니라
브라우저가 실제로 붙는app.example.com(BFF)다.” - “그러니 쿠키의
Domain도 그에 맞춰 조정해 달라.”
라는 메시지를 백엔드/인프라 쪽에 꼭 전달해 주는 게 좋습니다.
2) Path — BFF 경로 구조에 맞게 풀어주기
기존에는 API 서버의 라우트 구조에 맞춰 Path=/api/auth 같이 좁은 범위로 설정되어 있을 수 있습니다.
하지만 BFF를 도입하면, 브라우저 입장에서의 경로 구조가 바뀝니다.
- 예전:
- 브라우저:
https://api.example.com/login,https://api.example.com/user… - 쿠키:
Path=/또는Path=/api등으로도 크게 문제 없었음
- 브라우저:
- BFF 이후:
- 브라우저:
https://app.example.com/api/auth/login,https://app.example.com/api/user… - 혹은
https://app.example.com/dashboard/...와 같은 일반적인 페이지 접근 경로와 같이 인증과 전혀 다른 경로일 수도 있음(하지만 쿠키를 전달해야 함)
- 브라우저:
이때 가장 안전한 기본값은 Path=/ 입니다.
- 특별히 경로를 제한해야 할 이유가 없다면,
- “브라우저가 BFF에 보내는 모든 요청에 세션 쿠키가 붙어야 한다”는 쪽이 보통 더 자연스럽고,
- 그럴 경우
Path=/로 완화해 달라고 요청하는 편이 좋습니다.
- 특정 엔드포인트에서만 쿠키가 전송되어야 하는 정당한 이유(보안 정책 등)가 있다면
- 그 정책을 BFF 경로 구조에 맞게 다시 설계해야 합니다.
3) Secure — 운영은 무조건 유지, 개발 환경 전략은 분리
운영 환경에서는 고민할 게 없습니다.
HTTPS + Secure 를 유지하는 것이 정석입니다.
다만, BFF를 도입하면 로컬/개발 환경에서의 개발 경험이 살짝 꼬일 수 있습니다.
- 브라우저는 표준상
Secure쿠키를 HTTPS 요청에만 보내지만,- 대부분의 현대 브라우저는 개발 편의를 위해
http://localhost를 예외적으로 “secure context”로 취급합니다(이전 글 참고). - 그래서
http://localhost:3000에서도Secure쿠키가 전송되는 경우가 많습니다.
- 대부분의 현대 브라우저는 개발 편의를 위해
- 하지만 실제로 문제되는 구간은 보통 로컬이 아닌 개발/스테이징 도메인입니다.
- 예:
http://dev.example.com처럼 HTTPS가 아닌 도메인, - 혹은
https://dev.example.com이지만 인증서가 불완전하거나, 프록시/환경에 따라 브라우저가 신뢰하지 않는 경우 등.
- 예:
이런 환경을 설계할 때 선택지는 대략 세 가지 정도가 있습니다.
- 로컬/개발 환경도 가능하면 HTTPS로 띄우는 구조를 만든다. (예: 로컬 리버스 프록시, mkcert 등)
https://dev.example.com같은 개발용 도메인에서 테스트하고, 항상 유효한 HTTPS만 사용한다.- 정말 불가피할 때만 **“개발용 프로파일에서만
Secure를 끈다”**는 옵션을 둔다.
문서/팀 합의 차원에서는 이렇게 정리해 두면 좋습니다.
- 운영(Production):
Secure항상 켜둔다. - 개발/스테이징:
- “우리는 로컬/스테이징에서 어떤 방식으로 HTTPS를 확보할 것인가?”
- “정말 안 되면 개발용 설정에서만
Secure를 끌 것인가?”를 명시적으로 합의.
4) HttpOnly — 그대로 유지 (오히려 BFF와 더 잘 맞음)
인증/세션 쿠키는 HttpOnly를 유지하는 쪽이 훨씬 안전합니다.
BFF 패턴의 장점도 사실 여기서 나옵니다.
- 프론트 JS는 쿠키 값을 직접 읽지 않습니다.
- 대신 서버(Next.js BFF) 가
cookies()API를 통해서만 쿠키를 읽고,
그 정보를 가지고 백엔드 API를 호출하거나 응답을 가공합니다.
그래서 백엔드 팀에게는 이렇게 요청하면 됩니다.
- “인증/세션 쿠키의
HttpOnly는 기존처럼 그대로 유지해 달라.” - “토큰을 프론트 JS가 읽어서 로컬스토리지 등에 넣는 패턴은 지양하고,
BFF 레이어에서만 쿠키를 읽도록 설계하겠다.”
5) SameSite — 인증/리다이렉트 플로우에 맞춰 선택
SameSite는 **“이 쿠키를 크로스 사이트 요청에도 보낼 것인가?”**를 결정하는 옵션입니다.
BFF 도입 후 구조에서는 기본적으로 브라우저 ↔ BFF는 Same-Origin이라 비교적 단순하지만,
외부 OAuth/SSO, 결제 리다이렉트 등이 있으면 이야기가 달라집니다.
- 단순 SPA + BFF(같은 도메인 내) 구조
- 예:
app.example.com에서만 로그인과 화면 전환이 이루어지는 구조라면 SameSite=Lax정도가 기본값으로 무난합니다.
- 예:
- 외부 OAuth/SSO, 결제 모듈 등 크로스 사이트 리다이렉트가 있는 경우
- 예:
accounts.google.com→app.example.com으로 리다이렉트되는 로그인 플로우 - 이런 경우에는 세션 쿠키가 리다이렉트 이후에도 살아 있어야 하므로
SameSite=None; Secure조합이 필요할 수 있습니다.
- 예:
그래서 BFF로 옮기기 전에, 백엔드와 함께 다음을 먼저 정리하는 것이 좋습니다.
- “이 서비스의 로그인/결제 플로우에 크로스 사이트 리다이렉트가 포함되어 있는가?”
- 포함되어 있다면, 어느 쿠키는
SameSite=None; Secure로 설정해야 하는가? - 그렇지 않다면, 기본적으로
SameSite=Lax정도로 두고 시작할 것인가?
덧붙여서, BFF를 도입했다고 해서 보안 위협 자체가 사라지는 것은 아니라는 점도 같이 기억해 두면 좋습니다.
- 쿠키 기반 세션을 유지하는 한, 특히
SameSite=None을 쓰는 경우에는 CSRF 위협 모델이 여전히 존재합니다. - BFF는 액세스 토큰을 프론트 JS로부터 숨길 수 있지만, XSS가 발생하면 BFF를 향한 요청 자체를 위조할 수 있기 때문에 XSS 방어는 별도의 과제로 남습니다.
- 즉, BFF는 주로 CORS/비밀 노출/엔드포인트 복잡성을 줄이는 역할이고, CSRF·XSS 같은 웹 보안 기본기는 여전히 그대로 챙겨야 합니다.
4. Hybrid BFF — Server Component + Route Handler
방금까진 완전한 handler 역할의 집중된 예시를 보았습니다.
이제는 Server Component와 Route Handler를 상황에 맞게 조합할 수 있는 예시를 보겠습니다. Next.js는 이 둘을 조합할 수 있는 유연한 아키텍처를 제공합니다.
Server Component에서 직접 데이터 패칭
ts// app/dashboard/page.tsx (Server Component, 예시) import { cookies } from "next/headers"; export default async function DashboardPage() { const cookieStore = await cookies(); const sessionId = cookieStore.get("session_id")?.value; if (!sessionId) redirect("/login"); // Route Handler를 거치지 않고 직접 내부 API 호출 // 서버 컴포넌트 → 서버 간 통신 → CORS 없음 const user = await fetch( `${process.env.INTERNAL_API_BASE_URL}/user`, { headers: { "X-Session-Id": sessionId }, // Next.js의 fetch 캐싱 설정 next: { revalidate: 60 }, // 60초마다 갱신 } ).then((res) => res.json()); return <Dashboard user={user} />; }
주의: 왜 Server Component에서 Route Handler를 호출하면 안 될까요?
공식 문서에서 명시적으로 주의를 주는 부분입니다.
❌ 나쁜 패턴: 금지는 아니지만, 최대한 피하는 것이 좋습니다.
Server Component → fetch('/api/user') → Route Handler → 외부 API
추가 HTTP 왕복 발생:
- Server Component에서 자기 서버의 Route Handler로 HTTP 요청
- 같은 서버 안에서 불필요한 네트워크 레이어 추가(같은 서버에서 또 다시 요청을 보내는 것)
- 빌드 타임에는 HTTP 서버가 실행 중이지 아닐 수 있으므로 빌드 자체가 실패할 수도 있음
- 무엇보다, Next.js의 Server Component 설계 철학인 'Server Component는 서버에서 직접 데이터를 가져온다'라는 점에 어긋납니다.
✅ 좋은 패턴:
Server Component → 직접 fetch(내부 API URL 또는 DB)
Route Handler → 클라이언트 요청을 받아서 → 내부 API
언제 어떤 것을 쓸까
데이터를 어디서 가져오는가?
초기 페이지 렌더링에 필요한 데이터
→ Server Component에서 직접 패칭
→ SEO에 유리, 초기 로딩 빠름
→ 빌드 타임 캐싱 가능
사용자 인터랙션에 반응하는 데이터 (버튼 클릭, 폼 제출 등)
→ Client Component + axios → Route Handler
→ 동적으로 필요할 때만 호출
주기적으로 업데이트가 필요한 데이터
→ Client Component + SWR/React Query → Route Handler
→ 폴링, 실시간 업데이트
5. Proxy 핸들러 — 요청의 첫 번째 관문
Next.js 16 변경사항
Next.js 16에서는proxy.ts가 기존middleware.ts를 대체하는 새로운 컨벤션으로 도입되었습니다.(난 15 사용 중임...)
이름만 바뀐 것이 아니라, 네트워크 경계(프록시 레이어)를 더 명확히 표현하는 역할을 하고, 기본적으로 Node.js 런타임에서 실행됩니다.
middleware.ts는 Edge 용도로는 여전히 동작하지만, 점차proxy.ts중심으로 옮겨가라는 메시지에 가깝습니다.
(이 글에서는 앞으로 proxy.ts 기준으로 설명하지만, Next 15에서는middleware.ts/export function middleware()패턴으로 읽어도 됩니다.)
3단계의 레이어에 대해 설명했듯이, Proxy는 모든 요청이 라우트에 도달하기 전에 실행되는 코드입니다.
일반적으로 페이지든 API든 정적 파일이든, 요청이 들어오면 Proxy를 먼저 통과하도록 설정할 수 있지만,
실제 서비스에서는 matcher로 어떤 경로들에만 적용할지를 신중하게 제한해 주는 편이 좋습니다.
ts// proxy.ts (프로젝트 루트 또는 src/ 하위) import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export function proxy(request: NextRequest) { // 모든 /dashboard 경로 요청이 여기를 통과 const sessionId = request.cookies.get("session_id"); if (!sessionId) { // 로그인 페이지로 리다이렉트 return NextResponse.redirect(new URL("/login", request.url)); } // 다음 단계(Route Handler 또는 Page)로 진행 return NextResponse.next(); } // 어떤 경로에서 실행할지 지정 export const config = { matcher: ["/dashboard/:path*", "/admin/:path*"], // next.js 공식 문서 제안: api, _next/static, _next/image, favicon.ico 제외한 모든 경로 // matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
Proxy에서 할 수 있는 것들
① 인증 체크 후 리다이렉트
위 예시처럼 로그인 여부를 확인하고 미인증 사용자를 로그인 페이지로 보낸다.
모든 보호된 페이지마다 인증 체크 코드를 넣지 않아도 된다.
② 요청 헤더 변조
tsexport function proxy(request: NextRequest) { const requestHeaders = new Headers(request.headers); // 요청에 사용자 정보를 헤더로 추가 (downstream에서 활용) const userId = getUserIdFromSession(request.cookies.get("session_id")); requestHeaders.set("X-User-Id", userId ?? ""); return NextResponse.next({ request: { headers: requestHeaders }, }); }
③ A/B 테스트 라우팅
tsexport function proxy(request: NextRequest) { const bucket = (request.cookies.get("ab_bucket")?.value ?? Math.random() > 0.5) ? "A" : "B"; if (bucket === "B") { return NextResponse.rewrite(new URL("/landing-v2", request.url)); } return NextResponse.next(); }
④ 봇 차단, 지역별 리다이렉트
tsexport function proxy(request: NextRequest) { const country = request.geo?.country ?? "US"; if (country === "KR" && !request.nextUrl.pathname.startsWith("/ko")) { return NextResponse.redirect(new URL(`/ko${request.nextUrl.pathname}`, request.url)); } return NextResponse.next(); }
Proxy에서 하면 안 되는 것들
❌ 느린 데이터 패칭
Proxy는 설정에 따라 매우 많은 요청마다 실행된다.
DB 조회나 외부 API 호출을 여기서 하면 해당 구간을 지나는 모든 요청이 느려진다.
❌ 무거운 연산
같은 이유로 성능에 민감한 작업은 금물.
❌ 완전한 인증/인가 시스템
Proxy는 낙관적 체크(Optimistic Check)용으로만 사용하는 편이 좋다.
쿠키가 있는지 정도는 확인하지만, 쿠키가 실제로 유효한지 DB에서 검증하는 건
Route Handler나 Server Component에서 해야 한다.
예: '매 요청마다 유저 인증 여부 확인해야지' 한다면, 쿠키 여부 정도만 체크하고, 실제 유저를 DB에서 조회하는 것은 지양하자. 이 부분은 Server Component나 Route Handler에서 처리하자.
6. 전체 아키텍처 — 3단계 레이어의 역할 분담
지금까지 다룬 내용을 하나의 다이어그램으로 정리하면 다음과 같다.
mermaid
각 레이어의 책임 요약
| 레이어 | 파일 | 역할 | 특징 |
|---|---|---|---|
| Proxy | proxy.ts | 요청 필터링, 리다이렉트 | 빠르고 가벼운 작업만 |
| Server Component | page.tsx | 초기 데이터 패칭 + 렌더링 | 직접 내부 API 호출 |
| Route Handler | route.ts | 클라이언트 API 엔드포인트 | BFF 역할, 쿠키 조작 가능 |
| Client Component | *.tsx | 동적 UI, 사용자 인터랙션 | axios로 Route Handler 호출 |
실제 운영 관점에서 보면, BFF를 도입하는 것은 다음과 같은 트레이드오프도 함께 가져옵니다. 그래서 무작정 BFF로 모든 요청을 처리하는 것은 좋지 않습니다.
- 요청 한 번당 한 홉이 추가되므로, 레이턴시와 서버 비용을 함께 고려해야 합니다.
- Next.js의 fetch 캐시,
revalidate옵션, CDN 등을 활용해 캐싱 전략을 잘 짜지 않으면 BFF 레이어에서 병목이 생길 수 있습니다. - 백엔드 API 호출과 BFF 응답을 함께 추적할 수 있도록, 로그/트레이싱 상관관계 ID를 설계해 두면 운영·디버깅이 훨씬 수월합니다.
- 권한 모델은 여전히 백엔드에서 최종 결정하고, BFF는 “대리 호출자”라는 사실을 명확히 해 두는 편이 좋습니다.
7. 실전 시나리오 — 로그인 플로우 전체 흐름
거의 다 왔습니다.
지금까지 배운 내용을 종합해 로그인부터 인증된 API 호출까지의 전체 흐름을 예시 시나리오로 정리해보겠습니다.
[1] 로그인 페이지 접근
브라우저 GET /login
→ Proxy: 쿠키 없음 → 통과 (로그인 페이지는 matcher에서 제외)
→ page.tsx 렌더링
[2] 로그인 폼 제출
브라우저 POST /api/auth/login (axios, Same-Origin)
→ Proxy: 통과
→ Route Handler /api/auth/login
→ 백엔드 인증 API 호출 (서버 간 통신)
→ 성공 시 Set-Cookie: session_id=abc; HttpOnly; Secure; SameSite=Lax
→ 브라우저가 쿠키 저장
[3] 대시보드 접근
브라우저 GET /dashboard
→ Proxy: session_id 쿠키 존재 확인 → 통과
→ page.tsx (Server Component)
→ cookies()로 session_id 읽기
→ 내부 API 직접 호출 (서버 간 통신)
→ 데이터 패칭 후 HTML 렌더링
[4] 대시보드에서 데이터 갱신 (버튼 클릭)
브라우저 PUT /api/user/profile (axios, Same-Origin)
→ Proxy: 쿠키 존재 확인 → 통과
→ Route Handler /api/user/profile
→ cookies()로 session_id 읽기
→ 백엔드 API 호출 + 응답 반환
[5] 세션 만료 후 갱신
브라우저 axios 요청 → 401 응답
→ 인터셉터: /api/auth/refresh 호출
→ Route Handler: refresh_token 쿠키로 새 session_id 발급
→ 새 쿠키 설정
→ 원래 요청 재시도
8. 정리 — 시리즈 마무리
네트워크 이야기를 했다가, Next.js 아키텍처 이야기로 넘어가네요. 읽느라 고생하셨습니다.
마지막으로 이 시리즈에서 다룬 내용의 큰 그림을 정리해봅시다.
1편: CORS의 뿌리 브라우저가 강제하는 SOP가 있고, CORS는 그 예외를 허용하는 메커니즘입니다. CORS 에러는 서버가 허용 헤더를 보내지 않아서 브라우저가 차단하는 것입니다.
2편: 쿠키 보안
HTTP의 Stateless 문제를 쿠키가 해결합니다.
HttpOnly는 XSS, Secure는 MITM, SameSite는 CSRF를 방어합니다.
각 속성은 특정 공격 시나리오에 대한 방어막입니다.
3편: 크로스오리진 쿠키 인증
브라우저는 크로스오리진 요청에 기본적으로 쿠키를 포함하지 않습니다.
withCredentials: true와 서버의 Access-Control-Allow-Credentials: true,
그리고 SameSite=None; Secure가 모두 맞아야 동작합니다.
와일드카드 Origin과 credentials는 함께 사용할 수 없습니다.
4편: Next.js 아키텍처와 BFF 패턴
BFF 패턴으로 CORS 문제를 구조적으로 해결합니다.
Proxy는 요청 필터링, Route Handler는 BFF API, Server Component는 초기 렌더링에 사용합니다.
각 레이어의 책임을 명확히 하면 복잡한 인증 플로우도 깔끔하게 구현됩니다.
9. BFF 도입 체크리스트 (실전용 메모)
마지막으로, 실제 프로젝트에서 BFF를 도입하거나 마이그레이션할 때 체크해 보면 좋은 항목들을 간단히 정리해 둡니다.
- 환경 변수 이름을 통일했는가?
- 예: 내부 API 베이스 URL을
INTERNAL_API_BASE_URL하나로 통일해서 Server Component와 Route Handler에서 함께 사용.
- 예: 내부 API 베이스 URL을
- Route Handler의
fetch에 캐싱 정책을 명시했는가?cache,next: { revalidate }등을 통해, 언제까지 캐시/언제 다시 패칭할지 의도를 드러낸다.
- Proxy/미들웨어의
matcher가 과도하게 넓지 않은가?/api,/_next/static,/_next/image등은 기본적으로 제외하고, 보호가 필요한 경로 위주로 한정.
- 쿠키의
Domain/Path가 “브라우저가 실제로 붙는 호스트” 기준으로 설계되어 있는가?- 기존
api.example.com기준 설정이 남아 있지 않은지 다시 한 번 점검.
- 기존
- OAuth/결제 리다이렉트가 있다면
SameSite=None; Secure+ CSRF 방어 전략을 함께 설계했는가? - Server Component가 자기 서버의
/api/...를 거치지 않고, 가능한 한 직접 내부 API/DB를 호출하도록 정리했는가?
이 정도만 점검해도, “클라이언트에서 온갖 CORS/쿠키 설정으로 싸우던 것”보다 훨씬 예측 가능한 구조로 정리할 수 있습니다.
물론 이 글이 제안하는 것은 무조건적인 정답은 아닐 수 있습니다. 하지만 이 시리즈를 통해서 여러분들이 몰랐거나 놓쳤던 부분을 다시 한 번 생각해보고, 현재 상황에 맞게 적용해보는 계기가 되었으면 좋겠습니다.
📋 시리즈 전체 요약
| 편 | 제목 | 핵심 키워드 |
|---|---|---|
| 1편 | Next.js + Axios와 CORS 에러 제대로 이해하기 | SOP, Preflight, rewrites |
| 2편 | 쿠키 완전 정복 — Secure·HttpOnly·SameSite | XSS, MITM, CSRF, 쿠키 생명주기 |
| 3편 | Axios + 쿠키 인증 — withCredentials와 CORS의 콜라보 | withCredentials, 와일드카드 금지, 인터셉터 |
| 4편 | BFF 패턴 & Proxy Handler | Route Handler, proxy.ts, 레이어 설계 |
아직 댓글이 없습니다
첫 번째 댓글을 작성해보세요!