[BFF/모노레포] 모노레포 환경에서의 BFF 구조와 pnpm

2026. 4. 3. 15:28 · Dev/Backend

BFF (Backend For Frontend) 란?

프론트엔드를 위한 전용 백엔드 서버다.

일반적으로 프론트엔드는 백엔드 API를 직접 호출하지만, 프로젝트가 커지면 몇 가지 문제가 생긴다.

  • 하나의 화면을 그리기 위해 API를 3~4개씩 호출해야 한다
  • 백엔드 응답에 프론트가 쓰지 않는 필드가 잔뜩 들어있다
  • CORS, 인증 토큰 처리를 프론트에서 직접 해야 한다

 

BFF는 이 사이에 끼어서 프론트가 편하게 쓸 수 있는 API를 만들어주는 중간 서버다.

# 일반 구조
브라우저 → 백엔드 API

# BFF 구조
브라우저 → BFF 서버 → 백엔드 API

 

 

BFF가 하는 일

역할 설명
API 조합 여러 백엔드 API를 호출해서 하나로 합쳐 내려줌
데이터 가공 프론트에 필요한 필드만 골라서 응답
프록시 CORS 우회, 인증 헤더 자동 부착
인증 처리 로그인/토큰 갱신 등을 BFF에서 관리

 

 

간단 예시

프론트에서 대시보드 화면을 그리려면 유저 정보 + 알림 목록 + 통계 데이터가 필요하다고 가정하자.

BFF 없이:

// 프론트에서 3번 호출
const user = await fetch('/api/user/me')
const alerts = await fetch('/api/alerts')
const stats = await fetch('/api/dashboard/stats')

BFF 있으면:

// 프론트에서 1번 호출
const dashboard = await fetch('/bff/dashboard')
// BFF 서버 (Node.js)
app.get('/bff/dashboard', async (req, res) => {
  const [user, alerts, stats] = await Promise.all([
    fetchFromBackend('/api/user/me'),
    fetchFromBackend('/api/alerts'),
    fetchFromBackend('/api/dashboard/stats'),
  ])

  res.json({
    userName: user.name,
    alertCount: alerts.length,
    recentAlerts: alerts.slice(0, 5),
    totalCount: stats.total,
  })
})

프론트 입장에서는 호출 한 번이면 끝이고, 필요한 데이터만 깔끔하게 내려온다.

 

 


 

 

 

모노레포에서 BFF는 어떻게 쓰일까?

모노레포(Monorepo)는 여러 프로젝트를 하나의 저장소에서 관리하는 방식이다. 프론트엔드 앱이 여러 개 있을 때, 각 앱마다 API 호출 로직을 따로 만들면 중복이 심해진다.

일반적인 모노레포 + BFF 구조

monorepo/
├── apps/
│   ├── console/          # 메인 콘솔 앱 (Vue)
│   ├── auth/             # 인증 앱 (로그인/회원가입)
│   └── admin/            # 관리자 앱
├── packages/
│   ├── shared-ui/        # 공통 컴포넌트
│   ├── api-client/       # API 호출 함수 모음
│   └── types/            # 공통 타입 정의
├── bff/
│   └── server/           # BFF 서버 (Express/Fastify)
├── pnpm-workspace.yaml
└── package.json

 

 

API 호출 흐름

Vue 컴포넌트
  → packages/api-client의 request 함수
    → BFF 서버 (프록시)
      → 실제 백엔드 API

각 단계를 예시로 보면:

1단계: 컴포넌트에서 호출

<script setup>
import { getDeviceList } from '@packages/api-client'

const devices = ref([])

onMounted(async () => {
  devices.value = await getDeviceList()
})
</script>

2단계: api-client 패키지

// packages/api-client/device.js
import { request } from './request'

export const getDeviceList = () => {
  return request.get('/api/devices')
}

3단계: BFF 프록시

// bff/server/routes/devices.js
router.get('/api/devices', async (req, res) => {
  const token = req.headers.authorization

  const response = await axios.get(`${BACKEND_URL}/devices`, {
    headers: { Authorization: token },
  })

  // 프론트에 필요한 필드만 가공해서 내려줌
  const devices = response.data.map((d) => ({
    id: d.device_id,
    name: d.device_name,
    status: d.is_active ? 'active' : 'inactive',
  }))

  res.json(devices)
})

 

 

 

인증 처리 흐름 (IAM 리다이렉트 방식)

모노레포에서 인증 앱이 분리되어 있는 경우, 보통 이런 흐름을 따른다:

  1. 사용자가 콘솔 앱 접속
  2. 토큰이 없으면 → 인증 앱(IAM)으로 리다이렉트
  3. 인증 앱에서 로그인
  4. 토큰 발급 후 → 콘솔 앱으로 리다이렉트 (토큰 포함)
  5. 콘솔 앱이 토큰 저장 → 이후 BFF 요청 시 헤더에 포함
// 간단한 인증 가드 예시
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('access_token')

  if (!token && to.meta.requiresAuth) {
    // 인증 앱으로 리다이렉트
    window.location.href = `${IAM_URL}/login?redirect=${window.location.href}`
    return
  }

  next()
})

 

 

 

 

 

pnpm vs npm — 뭐가 다를까?

모노레포를 쓰면서 pnpm을 처음 접했는데, npm과 꽤 다르다.

핵심 차이

  npm pnpm
node_modules 구조 플랫(flat) — 모든 패키지가 최상위에 깔림 심볼릭 링크 — 실제 파일은 글로벌 store에 한 벌만 저장
디스크 사용량 프로젝트마다 중복 설치 하드링크로 공유, 디스크 절약
유령 의존성 발생함 (직접 설치 안 한 패키지도 import 가능) 차단됨 (직접 선언한 것만 접근 가능)
모노레포 지원 workspaces 있지만 기본적 네이티브 workspace 지원이 강력
속도 보통 빠름 (캐시 + 하드링크)

 

유령 의존성(Phantom Dependency)이 뭔데?

npm의 가장 큰 문제 중 하나다.

// package.json에 axios만 설치했다고 가정
{
  "dependencies": {
    "axios": "^1.0.0"
  }
}

axios가 내부적으로 follow-redirects를 사용한다. npm은 node_modules를 flat하게 깔기 때문에:

node_modules/
├── axios/
└── follow-redirects/    ← axios의 의존성인데 최상위에 깔림

그래서 내가 follow-redirects를 직접 설치하지 않았는데도 import할 수 있다. 이게 유령 의존성이다. axios가 버전업하면서 이 패키지를 안 쓰게 되면? 내 코드가 갑자기 깨진다.

pnpm은 이걸 구조적으로 막는다:

node_modules/
├── .pnpm/               ← 실제 패키지는 여기에
│   ├── axios@1.0.0/
│   └── follow-redirects@1.15.0/
└── axios -> .pnpm/axios@1.0.0   ← 심볼릭 링크 (내가 설치한 것만 접근 가능)

follow-redirects는 최상위에 링크가 없으므로 import 자체가 안 된다.

 

 

pnpm workspace 설정

모노레포에서 pnpm을 쓸 때는 루트에 pnpm-workspace.yaml을 만든다:

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
  - 'bff/*'

패키지 간 참조도 간단하다:

// apps/console/package.json
{
  "dependencies": {
    "@packages/api-client": "workspace:*",
    "@packages/shared-ui": "workspace:*"
  }
}

workspace:*는 "이 모노레포 안에 있는 패키지를 쓰겠다"는 뜻이다. npm registry에서 받는 게 아니라 로컬 패키지를 직접 참조한다.

 


 

정리

  • BFF는 프론트 전용 백엔드. API 조합, 데이터 가공, 인증 처리를 중간에서 해준다.
  • 모노레포에서 BFF를 쓰면 여러 앱이 하나의 BFF를 공유하면서 API 호출 로직 중복을 줄일 수 있다.
  • pnpm은 npm의 유령 의존성 문제를 해결하고, 디스크도 아끼고, 모노레포 지원도 강력하다.
저작자표시 비영리 변경금지 (새창열림)

'Dev > Backend' 카테고리의 다른 글

[BFF] BFF란? express로도 가능한거야?  (0) 2026.04.03
EJS 템플릿 엔진이란?  (0) 2026.03.03
'Dev/Backend' 카테고리의 다른 글
  • [BFF] BFF란? express로도 가능한거야?
  • EJS 템플릿 엔진이란?
ming0o
ming0o
Jr. 프론트엔드 개발자의 성장(개발)일지입니다. 이전 : https://velog.io/@ming0o
  • ming0o
    주토피아
    ming0o
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • Goals
        • 2026 목표 및 월간 회고
      • 개발자로 살아남기
        • 트러블슈팅
        • 코드 리뷰
        • 기술 정리
      • Dev N
        • Frontend
        • Backend
        • Infra & DevOps N
      • Challenge
        • KDT-핀테크 인턴십
        • 한이음 드림업
      • Learning
        • JavaScript
        • Algorithm
        • CS
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Router
    타입에러
    Vue
    지피티
    트러블슈팅
    try-catch문
    replace
    이기고만다
    수동배포
    Push
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
ming0o
[BFF/모노레포] 모노레포 환경에서의 BFF 구조와 pnpm
상단으로

티스토리툴바