본문 바로가기
(Frontend) 프론트엔드

(Next.js) BFF 프록시 패턴 2탄 - Next.js + React 멀티 앱 인증 공유와 서브도메인 쿠키 전략

by 공부가싫다가도좋아 2026. 6. 20.
반응형

포스팅 목차

  1. 이 포스팅에서 다루는 것
  2. 문제 상황 - Next.js와 React 앱이 공존할 때
  3. 쿠키 도메인 공유 원리
  4. 시나리오별 비교 - 어떤 도메인 구조를 선택할까?
  5. 실무에서의 설계 기준 - 왜 서브도메인이 업계 표준인가
  6. 인증(Authentication) vs 인가(Authorization) - 개념 분리
  7. 실제 구현 코드
  8. 공부하면서 생겼던 의문점 (Q&A)
  9. 참고 자료

1. 이 포스팅에서 다루는 것

지난 포스팅에서 Next.js App Router에서 BFF 프록시 패턴으로 httpOnly 쿠키 인증을 구현하는 방법을 다뤘습니다.이번엔 거기서 한 발 더 들어간, 실무에서 진짜 부딪히는 문제를 다룹니다.

"사용자단은 Next.js, 관리자단은 React로 구현했는데, 관리자가 사용자단에서 로그인하면 관리자단에서도 자동으로 로그인되게 할 수 있을까?"

프로덕트가 커지다 보면 이런 멀티 앱 구조를 심심찮게 만납니다. 그 해법인 쿠키 도메인 공유 전략과, 현업에서 실제로 쓰는 서브도메인 기반 인증 설계를 정리해 봤습니다.

- 1탄 내용(BFF 프록시 패턴)을 먼저 읽고 오시면 이해하기 더 수월합니다.
- Next.js App Router + 별도 React 앱(관리자 등)이 공존하는 구조를 가정합니다.
- TypeScript 기준으로 작성되었습니다.


2. 문제 상황 - Next.js와 React 앱이 공존할 때

많은 서비스가 아래와 같은 구조로 운영됩니다.

사용자단 (Next.js + BFF)   →  localhost:3000  또는  www.example.com
관리자단 (React + Vite)    →  localhost:4000  또는  admin.example.com
백엔드   (Spring / NestJS) →  localhost:8080  또는  api.example.com

이 구조에서 백엔드 인증 API는 하나를 공통으로 씁니다. 그런데 1탄에서 만든 BFF 방식은 Next.js 앱의 /api/auth/login Route Handler에서 쿠키를 심는 구조였죠.

그렇다면 사용자단에서 로그인해서 생긴 쿠키를, 관리자단 React 앱에서도 쓸 수 있을까요?

답은 "배포 도메인 구조에 따라 다르다" 입니다.


3. 쿠키 도메인 공유 원리

1탄에서 쿠키가 도메인에 묶여 있다는 건 프록시를 둬야 하는 제약으로 나왔는데, 이번엔 그 성질을 거꾸로 활용해서 앱끼리 로그인을 공유합니다.

3-1. 쿠키는 도메인에 묶여 있습니다

1탄에서 다뤘듯이 쿠키는 설정된 도메인에서만 브라우저가 자동으로 전송합니다. 그리고 쿠키를 설정할 때 domain 옵션을 어떻게 지정하느냐에 따라 공유 범위가 달라집니다.

domain 옵션 설정값 쿠키가 전송되는 범위 예시
미설정 (기본값) 설정한 정확한 도메인만 www.example.com
www.example.com 해당 도메인만 www.example.com
.example.com (앞에 . 붙임) 모든 서브도메인 포함 www., admin., api. 모두

 

3-2. 핵심 포인트

domain.example.com 처럼 앞에 점(.)을 붙여서 루트 도메인으로 설정하면, www.example.com에서 설정한 쿠키가 admin.example.com에서도 자동으로 전송됩니다.

멀티 앱 인증 공유는 결국 여기서 출발합니다.

// domain 미설정 → www.example.com 에서만 쿠키 전송
response.cookies.set('accessToken', token, {
  httpOnly: true,
  // domain 없음 → 현재 도메인에만 묶임
});

// domain 설정 → example.com의 모든 서브도메인에서 쿠키 전송
response.cookies.set('accessToken', token, {
  httpOnly: true,
  domain: '.example.com', // 앞에 . 붙이는 것이 핵심!
});

4. 시나리오별 비교 - 어떤 도메인 구조를 선택할까?

구분 같은 도메인
(경로로 구분)
서브도메인
(권장)
완전 다른 도메인
예시 example.com
example.com/admin
www.example.com
admin.example.com
example.com
example-admin.com
쿠키 공유 자동 공유 domain 옵션으로 공유 불가능
추가 로그인 필요 여부 불필요 불필요 필요 (각각 로그인)
배포 독립성 낮음 높음 높음
보안 분리 어려움 가능 완전 분리
팀 독립 운영 어려움 가능 가능
구현 복잡도 낮음 낮음 높음 (SSO 필요)
추천 상황 소규모 MVP 일반적인 실무 서비스 앱이 3개 이상 + 대규모

같은 도메인에 경로로만 나누는 방식은 구현은 간단하지만, 사용자단과 관리자단이 한 Next.js 프로젝트에 묶입니다. 배포가 한꺼번에 나가니 관리자단을 배포할 때 사용자단까지 영향을 받고, 팀이 갈려 있으면 코드 충돌도 잦아집니다.

도메인을 아예 다르게 가져가면 쿠키를 공유할 수 없어서, 로그인까지 묶으려면 Keycloak이나 Auth0 같은 별도 SSO(Single Sign-On) 서버를 둬야 합니다. 들이는 품이 만만치 않습니다.

결국 대부분의 실무 서비스에서는 서브도메인 구조가 가장 현실적인 선택입니다.


5. 실무에서의 설계 기준 - 왜 서브도메인이 업계 표준인가

서브도메인 구조가 업계 표준이 된 건 단지 쿠키 공유가 편해서가 아닙니다. 실무에서 무겁게 보는 이유가 네 가지 있습니다.

① 배포와 스케일링의 독립성

사용자단에 트래픽이 폭주해도 admin.example.com은 영향을 받지 않습니다. 각각 독립적으로 배포하고, 서버 사양도 트래픽에 맞게 다르게 설정할 수 있습니다. 관리자단은 트래픽이 적으니 작은 인프라로 충분합니다.

 

② 보안 정책의 분리

CSP(Content Security Policy), CORS 정책을 앱별로 다르게 설정할 수 있습니다. 관리자단에는 IP 허용 목록 제한이나 사내 VPN 강제 접속 같은 추가 보안 레이어를 독립적으로 적용할 수 있습니다. 사용자단이 보안 취약점에 노출되더라도 관리자단은 별도 방어선이 있습니다.

 

③ 팀의 독립적 운영

사용자단 팀과 관리자단 팀이 서로 코드 충돌 없이 작업합니다. 기술 스택도 팀마다 자유롭게 고르면 됩니다. Next.js와 React를 각각 쓰는 것처럼, 서비스 특성에 맞는 기술을 앱마다 독립적으로 정할 수 있습니다.

 

④ 구현 비용이 낮습니다

서브도메인 구조를 선택하면, 쿠키 공유를 위해 수정해야 할 코드는 단 한 줄입니다. 별도 SSO(Single Sign-On, 통합 인증) 서버를 구축할 필요가 없습니다.

언제 SSO 서버를 도입하나요?
앱이 3~4개 이상이 되거나, 완전히 다른 도메인의 서비스끼리 인증을 공유해야 할 때 Keycloak 같은 별도 인증 서버를 고려합니다. 그 전에 도입하는 것은 오버엔지니어링입니다.


6. 인증(Authentication) vs 인가(Authorization) - 개념 분리

멀티 앱 인증을 설계할 땐 이 둘을 헷갈리지 않고 갈라 두는 게 중요합니다.

구분 인증 (Authentication) 인가 (Authorization)
핵심 질문 "너 누구야?" "너 이거 할 수 있어?"
수행 주체 공통 백엔드 (하나) 각 앱 또는 백엔드 (분산)
예시 로그인 → 토큰 발급 userType이 ADMIN이면 관리자 API 허용
공유 여부 공통 사용 앱별로 독립 설정

서브도메인 구조에서 멀티 앱 인증 설계의 핵심은 아래와 같습니다.

  • 인증은 공통 — 토큰 발급, 검증, 갱신은 하나의 백엔드에서 처리합니다.
  • 인가는 분리userTypeADMIN이면 관리자 API를 허용하고, 아니면 403을 반환합니다. 이 판단은 백엔드가 합니다.

프론트는 userType 값으로 어떤 화면을 띄울지만 정하면 됩니다. 실제 권한 검증은 언제나 백엔드가 맡아야 하고요.


7. 실제 구현 코드

앞서 설명한 서브도메인 구조를 실제로 구현할 때 수정이 필요한 부분들을 정리했습니다.

7-1. Next.js 로그인 Route Handler - domain 옵션 추가

1탄에서 작성한 로그인 Route Handler에 domain 옵션 한 줄만 추가하면 됩니다.

// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();

  const response = await fetch(`${process.env.BACKEND_URL}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });

  if (!response.ok) {
    return NextResponse.json({ message: '로그인 실패' }, { status: response.status });
  }

  const data = await response.json();
  const nextResponse = NextResponse.json({ success: true });

  nextResponse.cookies.set('accessToken', data.accessToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24,
    path: '/',
    // 이 한 줄 추가 → admin.example.com에서도 쿠키 자동 전송
    domain: process.env.NODE_ENV === 'production' ? '.example.com' : undefined,
  });

  return nextResponse;
}

로컬 개발 환경 주의
로컬에서는 localhost:3000localhost:4000이 포트만 다를 뿐 도메인은 같은 localhost입니다. 로컬에서는 domain 옵션을 설정하지 않아도 쿠키가 공유됩니다. 프로덕션 환경에서만 .example.com으로 설정하도록 분기 처리하는 것이 좋습니다.

 

7-2. React(관리자단) - credentials 설정

관리자단 React 앱에서 백엔드 API를 호출할 때, 브라우저가 쿠키를 함께 전송하도록 credentials: 'include'를 설정해야 합니다.

// 관리자단 React 앱의 API 유틸 함수
type FetchOptions = Omit<RequestInit, 'body'> & {
  body?: Record<string, unknown>;
};

export async function adminFetch<T>(
  path: string,
  options: FetchOptions = {}
): Promise<T> {
  const { body, ...restOptions } = options;

  const response = await fetch(`${import.meta.env.VITE_API_URL}${path}`, {
    ...restOptions,
    // 핵심: 다른 도메인(서브도메인 포함)으로 요청 시 쿠키를 함께 전송
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      ...(restOptions.headers as Record<string, string>),
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  if (!response.ok) {
    throw new Error(`API 요청 실패: ${response.status}`);
  }

  if (response.status === 204) {
    return undefined as T;
  }

  return response.json() as Promise<T>;
}

 

7-3. 백엔드 CORS 설정 확인 필요

credentials: 'include'를 사용하면 백엔드에서 CORS 설정도 맞춰줘야 합니다. 백엔드 팀에 아래 내용을 전달해야 합니다.

# 백엔드에서 설정해야 할 CORS 옵션

Access-Control-Allow-Origin: https://admin.example.com  # * 는 credentials와 함께 사용 불가
Access-Control-Allow-Credentials: true                  # 반드시 true 설정

주의
credentials: 'include'를 사용하면 백엔드의 Access-Control-Allow-Origin*(와일드카드)로 설정할 수 없습니다. 반드시 허용할 도메인을 명시해야 합니다.

 

7-4. 전체 구조 요약

┌─────────────────────────┐       ┌─────────────────────────┐
│  사용자단 (Next.js)      │       │  관리자단 (React)         │
│  www.example.com        │       │  admin.example.com       │
│                         │       │                         │
│  BFF 프록시 O            │       │  BFF 프록시 X            │
│  httpOnly 쿠키 설정      │       │  credentials: 'include' │
└───────────┬─────────────┘       └───────────┬─────────────┘
            │                                 │
            │  domain: '.example.com' 쿠키 공유 (accessToken)
            │                                 │
            ▼                                 ▼
┌─────────────────────────────────────────────────────────────┐
│                      Backend API                            │
│                   api.example.com                           │
│                                                             │
│  인증 (Authentication) 공통 처리                             │
│  인가 (Authorization) → userType으로 권한 분기              │
│    GENERAL / BUYER → 일반 API 허용                          │
│    ADMIN          → 관리자 API 허용, 일반 API도 허용         │
└─────────────────────────────────────────────────────────────┘

8. 공부하면서 생겼던 의문점 (Q&A)

Q. 관리자단 React 앱은 BFF가 없는데, httpOnly 쿠키를 어떻게 활용하나요?

A. React 앱에서는 httpOnly 쿠키를 JavaScript로 직접 읽을 수 없습니다. 대신 credentials: 'include'를 설정하면 브라우저가 요청 시 쿠키를 자동으로 함께 전송해줍니다. 백엔드가 쿠키에서 토큰을 읽어 인증을 처리하기 때문에, 프론트에서 토큰을 직접 다룰 필요가 없습니다.

Q. "로그인 상태인지" 여부를 React 앱에서는 어떻게 확인하나요?

A. 방법은 두 가지입니다. 첫 번째는 httpOnly: false로 설정한 별도 쿠키(예: userType)를 JavaScript에서 읽는 방법입니다. 두 번째는 앱 진입 시 /members/me 같은 내 정보 API를 호출해서 성공하면 로그인 상태, 401이 오면 비로그인 상태로 판단하는 방법입니다. 보안이 중요한 정보는 두 번째 방법이 더 안전합니다.

Q. sameSite 옵션은 어떻게 설정해야 하나요?

A. 서브도메인 간 쿠키 공유를 위해서는 sameSite: 'lax' 또는 'none'으로 설정해야 합니다. sameSite: 'strict'는 다른 사이트에서 온 요청에 쿠키를 전송하지 않기 때문에 서브도메인 간 공유가 제한될 수 있습니다. 단, 'none'을 사용하면 반드시 secure: true도 함께 설정해야 합니다.

Q. 언제 SSO 서버(Keycloak 등)를 도입해야 하나요?

A. 앱이 3~4개 이상이 되거나, 완전히 다른 도메인의 서비스끼리 인증을 공유해야 할 때입니다. 그 전에 Keycloak을 도입하는 것은 운영 비용 대비 효과가 낮은 오버엔지니어링입니다. 지금 단계에서는 서브도메인 + 쿠키 공유 구조면 충분합니다.


9. 마무리

Next.js와 React 앱이 같이 도는 구조를 실무에서 처음 봤을 땐 "앱이 둘이면 로그인도 두 번 해야 하나?" 싶었습니다. 알고 보니 쿠키 domain 옵션 한 줄과 서브도메인 구조면 끝나는 일이었죠.

이번 글을 한 줄로 추리면 이렇습니다.

서브도메인 구조 채택 → 쿠키 domain을 루트 도메인(.example.com)으로 설정 → 인증은 공통, 인가만 앱별로 분리 → React 앱에서는 credentials: 'include'로 쿠키 자동 전송

멀티 앱 인증 구조를 짜다 막혔던 분들께 조금이나마 보탬이 됐으면 합니다.


참고 자료

반응형

댓글