본문으로 건너뛰기

BFF vs 미들웨어, 쿠키 도메인과 401

·11 min read

같은 계정으로 로그인했는데, 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 앱을 고친 방법

세 가지 선택지가 있었다.

  1. A 앱처럼 API를 직접 호출 (BFF 제거, credentials: 'include') → .example.com 공유 쿠키를 그대로 활용. 가장 단순하지만 BFF 장점을 포기해야 한다.

  2. 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
    }
  3. 쿠키 도메인을 공유 가능하게 맞추기 (배포 환경에서 같은 상위 도메인 사용 등).

여기서 미들웨어의 진가가 드러났다. 미들웨어는 모든 요청 길목에서 자동으로 돌고, 내 도메인 응답에 쿠키를 심을 수 있으니, "다른 도메인 세션 → 내 도메인 쿠키"로 바꿔주는 브리지 역할에 딱 맞았다.

패턴 선택이 곧 인증 동작을 바꾼다

BFF는 프론트가 호출하는 데이터 창구다. 시크릿 은닉·중계·CORS 회피에 쓴다. 미들웨어는 모든 요청이 지나는 검문소다. 게이팅·리다이렉트·쿠키 주입에 쓴다. 개념은 다르지만 둘 다 "내 도메인 서버사이드"라, 쿠키를 내 도메인에 심을 수 있다는 공통점이 있다.

쿠키는 페이지 origin이 아니라 목적지 도메인을 따라 붙는다. BFF로 바꾸면 브라우저가 "내 도메인"만 부르게 되어, 다른 도메인에 사는 공유 쿠키와 단절될 수 있다. 이게 "같은 로그인인데 한 앱만 401"의 정체였다. 서버사이드 계층을 도입할 땐 "이 계층이 어떤 도메인의 쿠키를 보게 되는가"를 항상 같이 따져야 했다.