--- id: backend-bff-pattern title: Backend for Frontend — Per-client API category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [backend, bff, vibe-coding] tech_stack: { language: "TS", applicable_to: ["Backend"] } applied_in: [] aliases: [BFF, backend for frontend, edge BFF, aggregation API, gateway pattern] --- # BFF (Backend for Frontend) > Frontend 별 backend layer. **Web BFF, iOS BFF, Android BFF**. Aggregation + transformation + 인증. Microservice + 다양 client 의 sweet spot. ## 📖 핵심 개념 - BFF: 한 frontend = 한 BFF. - Aggregation: 여러 service 호출 → 한 응답. - Tailoring: 그 client 가 필요한 데이터만. - Edge BFF: 사용자 가까이. ## 💻 코드 패턴 ### Architecture ``` [Web] → [Web BFF] → Service A [iOS] → [iOS BFF] → Service B [Android] → [Android BFF] → Service C [Admin] → [Admin BFF] ``` → 각 BFF 가 그 client 의 needs 에 맞춰. ### Why per-client? ``` Web: 큰 페이로드 OK, 빠른 fetch, web-specific UI 데이터. iOS: 작은 (data plan), iOS-specific format (e.g. SF symbols). Android: 작은, 전력 절약. Admin: 풍부한 데이터, 권한 다름. → 한 API 가 모두 만족 X. ``` ### Web BFF 예 (Hono / Next API) ```ts // /api/dashboard app.get('/api/dashboard', authRequired, async (c) => { const userId = c.get('userId'); // 여러 service 동시 호출 const [user, recentOrders, recommendations, notifications] = await Promise.all([ fetch(`${USERS_SVC}/users/${userId}`).then(r => r.json()), fetch(`${ORDERS_SVC}/orders?userId=${userId}&limit=5`).then(r => r.json()), fetch(`${RECS_SVC}/for/${userId}`).then(r => r.json()), fetch(`${NOTIF_SVC}/${userId}/unread`).then(r => r.json()), ]); // Web 의 needs 에 맞춰 합치기 return c.json({ user: { id: user.id, name: user.name, avatar: user.avatar }, recentOrders: recentOrders.map((o: any) => ({ id: o.id, status: o.status, total: o.total, itemCount: o.items.length, })), recommendations: recommendations.slice(0, 6), unreadCount: notifications.length, }); }); ``` → Web 이 한 번의 fetch. ### iOS BFF 예 (작은 페이로드) ```ts // /api/dashboard (iOS) app.get('/api/dashboard', authRequired, async (c) => { const userId = c.get('userId'); const [user, recentOrders] = await Promise.all([ fetch(`${USERS_SVC}/users/${userId}`).then(r => r.json()), fetch(`${ORDERS_SVC}/orders?userId=${userId}&limit=3`).then(r => r.json()), // 작게 ]); return c.json({ user: { name: user.name, avatar: user.avatar }, // id 안 필요 orders: recentOrders.map((o: any) => ({ id: o.id, status: o.status, // total, itemCount 만 — 적은 byte })), }); }); ``` ### Edge BFF (Cloudflare / Vercel) ```ts // CF Worker / Vercel Edge export default { async fetch(req: Request, env: Env) { const userId = await getUserId(req, env); if (!userId) return new Response('Unauthorized', { status: 401 }); // Cache 적극 const cacheKey = `dashboard:${userId}`; const cached = await env.CACHE.get(cacheKey, { type: 'json' }); if (cached) return Response.json(cached); const data = await aggregateData(userId, env); await env.CACHE.put(cacheKey, JSON.stringify(data), { expirationTtl: 30 }); return Response.json(data); }, }; ``` → 사용자 가까이 = 빠름. ### Authentication 한 곳 ```ts // BFF 가 JWT verify, 백엔드 service 호출 시 trusted async function callService(url: string, userId: string) { return fetch(url, { headers: { 'X-User-ID': userId, // BFF 가 verify 한 user 'X-Internal-Auth': INTERNAL_TOKEN, // service-to-service }, }); } ``` → BFF 가 user 인증 + service 호출. ### Caching strategy ```ts // Per-user cache const userCache = `user:${userId}:dashboard`; // Common cache const productsCache = `products:trending`; // 다른 TTL - Personal data: 30s - Common (products): 5 min - Static (categories): 1 hour ``` ### Error 통합 ```ts async function safeCall(fn: () => Promise, fallback: T): Promise { try { return await fn(); } catch (e) { log.error({ err: e }); return fallback; } } const data = { user: await safeCall(() => fetchUser(), null), orders: await safeCall(() => fetchOrders(), []), recommendations: await safeCall(() => fetchRecs(), []), }; // 일부 service 실패 = partial response ``` → Resilient — 한 service 다운 = 다른 데이터 표시. ### Header forwarding ```ts const FORWARD_HEADERS = ['x-request-id', 'traceparent', 'tracestate', 'x-locale']; async function callService(url: string, req: Request) { const headers = new Headers(); for (const h of FORWARD_HEADERS) { const v = req.headers.get(h); if (v) headers.set(h, v); } return fetch(url, { headers }); } ``` → Tracing 보존. ### Type-safe (tRPC / Hono RPC) ```ts // BFF 가 tRPC server const bffRouter = router({ dashboard: publicProcedure.query(async ({ ctx }) => { return aggregateDashboard(ctx.userId); }), }); // Client (Web) const client = createTRPCReact(); const dashboard = client.dashboard.useQuery(); ``` → Type-safe end-to-end. ### GraphQL BFF ```ts // 단일 GraphQL endpoint per client type Query { webDashboard(userId: ID!): WebDashboard iosDashboard(userId: ID!): IosDashboard } # Web 이 자기 query 만 보냄 → 정확 데이터. ``` → Pothos / Yoga. ### Aggregation patterns ```ts // 1. Parallel const [a, b, c] = await Promise.all([...]); // 2. Sequential (의존) const user = await fetchUser(); const orders = await fetchOrders(user.id); // 3. Conditional const user = await fetchUser(); if (user.tier === 'pro') { data.proFeatures = await fetchProFeatures(); } // 4. Stream / pipe async function* streamData() { yield await fetchA(); yield await fetchB(); } ``` ### Rate limit (BFF level) ```ts // Per user / per IP const rate = await rateLimiter.check(userId); if (!rate.allowed) return c.text('Rate limited', 429); ``` ### Failure isolation ```ts // Circuit breaker per service const userBreaker = new CircuitBreaker(fetchUser, { timeout: 5000 }); if (userBreaker.isOpen()) { return Response.json({ user: cached, degraded: true }); } ``` → Service 다운 = degraded mode. ### Observability ```ts // 매 service call 추적 import { trace } from '@opentelemetry/api'; const tracer = trace.getTracer('bff'); await tracer.startActiveSpan('fetch-user', async (span) => { span.setAttributes({ userId }); try { return await fetchUser(); } finally { span.end(); } }); ``` ### Web push notification ```ts // BFF 가 SSE / WebSocket 처리 app.get('/api/events', async (c) => { return new Response( new ReadableStream({ async start(controller) { const sub = pubsub.subscribe(c.get('userId')); for await (const event of sub) { controller.enqueue(`data: ${JSON.stringify(event)}\n\n`); } }, }), { headers: { 'Content-Type': 'text/event-stream' } } ); }); ``` ### vs API Gateway ``` API Gateway: - Generic — 어떤 client 도 가능 - 큰 organization (한 Gateway, 많은 client) - Auth / rate limit / routing BFF: - Per-client — Web BFF, iOS BFF - 작은 organization (each team owns BFF) - 비즈니스 logic (aggregation) → Gateway = horizontal. BFF = vertical (client-specific). 둘 다 같이 사용 가능. ``` ### Fan-out + cache ``` 1 BFF call = 5 service calls. Cache: - BFF response cache (per-user 30s) - Service response cache (Redis) - DB query cache (Redis) → 첫 call slow, 후속 fast. ``` ### Mobile-specific BFF ```ts // iOS BFF - 작은 페이로드 (data plan) - iOS HIG-friendly format (SF symbol name 같은) - App version 별 다른 응답 - Push token 등록 endpoint // Android BFF - 작은 + 전력 절약 - Material symbol name - App version 별 ``` ### Versioning (per BFF) ``` /api/v1/dashboard /api/v2/dashboard → App version 별 BFF version pin. ``` ### Team ownership ``` Web 팀: Web BFF + Web frontend iOS 팀: iOS BFF + iOS app → Frontend 팀 가 BFF 소유. 빠른 iteration. ``` ### CDN integration ``` Static + edge BFF: - 정적 = CDN - 동적 = edge BFF - 사용자 = 가까운 region 자동 ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | 다양 client | BFF per client | | Single client | Direct API 충분 | | 마이크로서비스 + Web | Web BFF 가 aggregation | | Public API | Direct (다양 dev) | | Mobile + 작은 페이로드 | Mobile BFF 강력 | | Edge user 가까이 | Edge BFF | ## ❌ 안티패턴 - **BFF 가 비즈니스 logic 모두**: service layer 의 책임. - **BFF 가 내부 API expose 그대로**: tailoring 의미 X. - **모든 client 한 BFF**: per-client 의 가치 잃음. - **Cache 무**: 매 fetch 가 N service. - **Auth 매 service 마다**: BFF 만. - **Header forward 무**: tracing 깨짐. - **Failure isolation 무**: 한 service down = BFF down. ## 🤖 LLM 활용 힌트 - BFF = aggregation + tailoring + 인증. - Edge BFF (CF / Vercel) 가 가까운 user. - Type-safe = tRPC / Hono RPC. - Failure isolation + cache 항상. ## 🔗 관련 문서 - [[Backend_API_Gateway_BFF]] - [[Backend_Edge_Functions]] - [[Backend_Hono_Modern]]