배달의민족 가게목록은 광고배너·큐레이션·가게리스트 등 한 화면에 최대 12개 구좌가 들어가는 통합 전시 지면으로 커졌다. 클라이언트가 구좌마다 담당 서버를 직접 호출하던 구조를 BFF 한 곳으로 모았더니 이번엔 응답이 1.7MB로 비대해지고 가장 느린 구좌(300ms)에 전체가 묶이는 하향 평준화가 발생했다. SSE로 완료된 영역부터 흘려보내 렌더링 시작 시점을 300ms에서 15ms로 당긴 과정을 다룬다.
핵심 포인트- 1지면 1API(BFF)로 클라이언트 통신은 단순해졌지만, 단일 응답이라 데이터 비대화와 지연 하향 평준화라는 새 병목이 생겼다.
- REST·ResponseBodyEmitter·WebSocket·SSE 넷을 비교해 SSE 채택 — 표준 메시지 포맷이라 클라이언트 파싱이 쉽고 양방향이 불필요했다.
- 클라이언트는 EventSource 대신 fetch + ReadableStream을 골랐다. 커스텀 헤더(인증)와 AbortSignal 취소를 기존 REST 스택과 통일하기 위해서다.
- 렌더링 시작까지 300ms → 15ms. 가장 느린 구좌 도착 시각은 그대로지만 빈 화면 구간이 사라졌다.
- "SSE엔 nginx proxy_buffering off가 필수"라는 통설을 직접 측정해 반증했다 — 280KB까지 버퍼링 ON/OFF 차이가 없었다.
상세 정리- 문제의 출발점: 클라이언트가 편성 API를 부른 뒤 각 구좌 서버에 병렬 요청하는 구조였다. 구좌가 늘 때마다 통신·에러·타임아웃 코드가 함께 늘고, 서버는 편성만 제시할 뿐 실행을 클라이언트가 쥐고 있어 특정 구좌 장애 시 대체 편성을 할 수 없었다.
- 1단계 BFF 도입: 전시 서버가 모든 구좌를 조회·조립해 하나의 API로 내려준다. 클라이언트 호출이 하나로 줄고 fallback 편성이 가능해졌다.
- 통합의 대가 ①: 가게목록 약 280KB에 큐레이션 5개 약 1.4MB가 붙어 응답이 1.7MB를 넘겼다. 클라이언트는 마지막 바이트까지 받아야 파싱을 시작한다.
- 통합의 대가 ②: 서브카테고리는 15ms에 끝나는데 가장 무거운 구좌가 300ms라, 전체 응답이 300ms로 결정됐다. 빠른 데이터가 느린 데이터를 기다리는 하향 평준화다.
- 프로토콜 선택: REST는 응답 시간을 못 줄여 제외, ResponseBodyEmitter는 메시지 경계를 직접 정의해야 해 웹 생산성이 떨어져 제외, WebSocket은 양방향이 불필요해 제외. "가벼운 모델링보다 전송 안정성과 클라이언트 생산성"을 기준으로 SSE를 골랐다.
- 서버 이벤트 설계: DISPLAY_GROUP(편성 정보)을 먼저 보내 뼈대를 확정하고, SUBCATEGORY·CURATION·SHOP_LIST·DA_BANNER를 완료 순서대로 전송한 뒤 DONE으로 닫는다.
- 클라이언트 선택: EventSource는 커스텀 헤더를 못 실어 Authorization을 보낼 수 없고 취소·선검증도 제한적이었다. fetch + ReadableStream으로 가면 파싱을 한 겹 직접 짜야 하지만 인증·취소·에러 처리가 REST와 같은 규칙을 공유한다.
- 캐시 통합: SSE 이벤트가 도착하면 React Query의 setQueryData로 캐시에 밀어 넣고, 컴포넌트는 평소처럼 캐시를 구독만 한다. 통신 복잡성이 SSE 초기화 훅 한 곳에 갇힌다.
- 렌더링 전략: 편성 정보로 스켈레톤을 먼저 그리고 도착 순서대로 채우며, 한 구좌가 실패하면 그 자리만 숨기고 나머지는 정상 노출한다.
- 재방문 최적화: 이미 받은 카테고리는 SSE 연결 자체를 생략하고 캐시를 즉시 렌더하며, PTR 새로고침에서만 캐시를 비우고 재연결한다.
- nginx 버퍼링 실측: 200B/1.2KB/20KB/280KB 네 구간에서 직접 연결·버퍼링 ON·OFF를 비교했는데 차이가 없었다. 다만 text/event-stream에 gzip을 켜면 버퍼링 ON에서 지연이 생겨 OFF가 필요했다.
- 통설의 조건: 버퍼링 끄기는 알림·실시간 로그처럼 끝나지 않는 스트림에서 의미가 있고, 300ms에 끝나는 가게목록에는 해당하지 않는다는 결론이다.
- 적용 범위 제한: 구좌 하나만 내려주는 단순 지면에는 SSE를 넣지 않았다. 나눠 보낼 이유가 없으면 스트리밍은 복잡도만 늘린다.