BFF vs 미들웨어, 쿠키 도메인과 401
같은 계정으로 로그인했는데, A 앱은 멀쩡히 로그인 상태이고 B 앱은 계속 401이 떴다. 코드도 거의 비슷했다. 범인은 "내 서버가 요청 길목에 끼어 있느냐"였다.
프론트엔드 서버사이드 계층을 이야기할 때 자주 같이 등장하는 두 개념이 BFF와 미들웨어다. 둘 다 "내 서버(예: localhost, 내 도메인)에서 도는 서버사이드 코드"라 헷갈리기 쉽다. 그런데 역할이 다르고, 그 차이가 로그인/쿠키 같은 데서 실제 버그로 터졌다.
BFF — Backend For Frontend
BFF는 프론트엔드 하나만을 위해 그 앞단에 두는 얇은 서버 계층이다. 브라우저와 진짜 백엔드 API 사이에 끼어서 중계한다.
Next.js에서는 app/api/*의 route handler(요청이 들어오면 응답을 만들어 돌려주는 서버사이드 함수)가 곧 BFF다. Next.js 15부터 cookies()가 비동기로 바뀌어 await로 읽는다.
// app/api/user/me/route.ts ← 이게 BFF
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function GET() {
const session = (await cookies()).get('SESSION')?.value
if (!session) {
return NextResponse.json({ message: 'no session' }, { status: 401 })
}
// 서버사이드에서 진짜 API를 대신 호출
const res = await fetch('https://api.example.com/v1/user/me', {
headers: { Cookie: `SESSION=${session}` },
})
return NextResponse.json(await res.json(), { status: res.status })
}브라우저는 localhost/api/user/me까지만 부르고, 진짜 API 호출은 서버가 대신 한다.
BFF를 쓰는 이유
- 시크릿 은닉 — 토큰/API 키를 브라우저에 노출하지 않고 서버에서만 붙인다.
- 인증 중앙화 — 쿠키 처리·세션 검증을 한 곳(서버)에서 한다.
- 응답 가공/집계 — 여러 API를 묶거나 프론트가 쓰기 좋은 모양으로 변환한다.
- CORS 회피 — 브라우저는 same-origin(내 서버)만 부르니 CORS 문제가 없다.
미들웨어는 요청 길목의 검문소다
미들웨어는 데이터 엔드포인트가 아니라, 요청 파이프라인의 인터셉터다. 매칭된 모든 요청이 라우트/페이지에 닿기 전에 먼저 통과한다.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const hasSession = request.cookies.has('SESSION')
// 로그인 안 됐으면 통과시키지 않고 리다이렉트
if (!hasSession) {
return NextResponse.redirect(new URL('https://login.example.com'))
}
return NextResponse.next() // 통과
}
export const config = {
matcher: ['/((?!api|_next|favicon.ico).*)'], // 적용 범위
}미들웨어는 JSON 데이터를 돌려주는 게 아니라 매칭된 요청을 렌더 이전에 가로채 통과(next) / 리다이렉트(redirect) / 재작성(rewrite)을 한다.
미들웨어를 쓰는 이유
- 인증 게이팅 — 로그인 안 된 요청을 진입 전에 차단/리다이렉트한다.
- 리다이렉트 / 리라이트 — 경로 재작성, 국가/언어 분기, A/B 테스트.
- 헤더·쿠키 주입 — 모든 응답에 공통 헤더/쿠키를 심는다.
한눈에 비교
BFF (app/api/*) | 미들웨어 (middleware.ts) | |
|---|---|---|
| 본질 | 프론트가 호출하는 데이터 엔드포인트 (목적지) | 요청 파이프라인의 인터셉터 (길목) |
| 언제 실행 | 브라우저가 명시적으로 fetch할 때 | 매칭된 모든 요청 전에 자동 |
| 반환하는 것 | JSON 등 데이터 | next() / redirect() / rewrite() |
| 대표 용도 | 데이터 조회·가공, upstream 중계, 시크릿 은닉 | 인증 게이팅, 리다이렉트, 헤더/쿠키 주입 |
| 실행 시점 | route handler, 브라우저가 fetch할 때 | 매칭된 모든 요청, 렌더 이전 |
비유하면 BFF는 내가 찾아가서 "이거 주세요" 하는 창구다. 미들웨어는 들어올 때 모두가 지나는 검문소다 — 도장 찍어주거나(쿠키 set), 돌려보내거나(redirect), 딴 데로 보낸다(rewrite).
둘의 숨은 공통점, "내 도메인 서버사이드"
개념은 다르지만, 둘 다 내 origin에서 서버사이드로 돈다는 공통점이 있다. 그래서 응답에 쿠키를 set하면 그 쿠키가 내 도메인(예: localhost)에 박힌다. 이게 cross-domain 쿠키 문제를 푸는 열쇠였다.
// 미들웨어든 BFF든, 내 origin 응답에 쿠키를 심을 수 있다
response.cookies.set('SESSION', sessionId, {
path: '/',
httpOnly: true,
sameSite: 'lax',
})
// → 이 쿠키는 내 도메인(localhost) 소유가 된다이 한 줄이 다음 사건의 핵심이다.
같은 로그인인데 한 앱만 401이 떴다
모노레포에 두 앱이 있었다. 둘 다 같은 백엔드 api.example.com을 쓴다.
- A 앱: 브라우저가
api.example.com을 직접 호출 (BFF 없음) - B 앱: 브라우저가 자기 서버의
/api/*(BFF)를 호출 → 서버가 upstream 중계
login.example.com에서 로그인하면 A 앱은 로그인 상태가 공유되는데, B 앱은 계속 401이 떴다.
범인은 쿠키의 도메인이었다
로그인 시 발급되는 세션 쿠키의 실제 속성은 이랬다.
Set-Cookie: SESSION=...; Domain=example.com; SameSite=None; Secure핵심은 이 쿠키가 페이지 주소(origin)가 아니라 Domain=example.com에 묶인다는 점이다. 그리고 브라우저는 요청을 보낼 때 "목적지 도메인"에 매칭되는 쿠키를 붙인다 — 페이지가 어느 origin인지와 무관하게.
SameSite=None; Secure 덕분에 A 앱은 cross-site 요청에서도 .example.com 쿠키를 보낼 수 있어 로그인이 공유됐다. 반면 B 앱의 브라우저는 localhost(BFF)만 부르니, 브라우저는 localhost 소유 쿠키만 보낸다. .example.com 쿠키는 절대 localhost로 안 간다. 그래서 BFF는 세션을 못 보고 401을 돌려줬다.
BFF는 "내 도메인만 부르게" 만드는 패턴이다. 그 자체가 장점(시크릿 은닉·CORS 회피)인데, 동시에 "다른 도메인에 사는 공유 쿠키"와는 단절된다는 함정이 됐다.
B 앱을 고친 방법
세 가지 선택지가 있었다.
-
A 앱처럼 API를 직접 호출 (BFF 제거,
credentials: 'include') →.example.com공유 쿠키를 그대로 활용. 가장 단순하지만 BFF 장점을 포기해야 한다. -
BFF/미들웨어에서 세션을 "내 도메인 쿠키"로 복사 → 서버사이드에서 로그인/세션 교환을 한 뒤
response.cookies.set('SESSION', ...)로 localhost 쿠키를 새로 발급. 검문소(미들웨어)에서 도장을 찍어주는 셈이다.// middleware.ts — 세션 없으면 서버에서 교환 후 localhost 쿠키로 심기 export async function middleware(request: NextRequest) { const res = NextResponse.next() if (request.cookies.has('SESSION')) return res const sessionId = await exchangeSessionServerSide() // upstream 호출 if (sessionId) { res.cookies.set('SESSION', sessionId, { path: '/', httpOnly: true }) } return res } -
쿠키 도메인을 공유 가능하게 맞추기 (배포 환경에서 같은 상위 도메인 사용 등).
여기서 미들웨어의 진가가 드러났다. 미들웨어는 모든 요청 길목에서 자동으로 돌고, 내 도메인 응답에 쿠키를 심을 수 있으니, "다른 도메인 세션 → 내 도메인 쿠키"로 바꿔주는 브리지 역할에 딱 맞았다.
패턴 선택이 곧 인증 동작을 바꾼다
BFF는 프론트가 호출하는 데이터 창구다. 시크릿 은닉·중계·CORS 회피에 쓴다. 미들웨어는 모든 요청이 지나는 검문소다. 게이팅·리다이렉트·쿠키 주입에 쓴다. 개념은 다르지만 둘 다 "내 도메인 서버사이드"라, 쿠키를 내 도메인에 심을 수 있다는 공통점이 있다.
쿠키는 페이지 origin이 아니라 목적지 도메인을 따라 붙는다. BFF로 바꾸면 브라우저가 "내 도메인"만 부르게 되어, 다른 도메인에 사는 공유 쿠키와 단절될 수 있다. 이게 "같은 로그인인데 한 앱만 401"의 정체였다. 서버사이드 계층을 도입할 땐 "이 계층이 어떤 도메인의 쿠키를 보게 되는가"를 항상 같이 따져야 했다.