📚 목차
2025 Woowacon FE 후기 - 배민 장보기/쇼핑 가게홈 성능 최적화 고군분투기
장보기/쇼핑 가게홈 성능 최적화 고군분투기: 웹 전환 과정에서의 경험과 교훈
이번 글에서는 오늘 2025 Woowacon에서 발표한 "장보기/쇼핑 가게홈 성능 최적화 고군분투기: 웹 전환 과정에서의 경험과 교훈"에 대해 다뤄보려고 한다.
당일 많은 트랙들이 있었지만 우아한형제들 장보기·쇼핑 프론트엔드팀 우희제님이 발표하신 장보기/쇼핑 서비스의 웹 전환 과정에서 겪은 성능 최적화 경험에 대한 이야기가 가장 인상이 깊어서 이 글을 통해 공유하고자 한다.

네이티브에서 웹으로: 시작의 이유
배민 장보기 쇼핑은 "30분 안에 문 앞까지" 도달하는 빠른 커머스 서비스다.
그러나 경쟁이 치열해지고 시장 변화 속도가 빨라지면서 "더 빠르게 실험하고, 더 빠르게 배포하고, 더 빠르게 피드백 받는" 환경이 필요해졌다.
하지만 네이티브 앱은
- iOS/Android 이중 개발,
- 심사 지연,
- 여러 버전의 API 운영 부담
이라는 태생적 한계를 갖고 있었다.
이 문제를 해결하기 위해 팀은 웹뷰 기반 전환을 결정했다.
단, 미션은 하나였다.
"고객이 네이티브에서 웹으로 옮겨졌다는 사실조차 눈치채지 못하도록."

웹뷰는 네이티브보다 구조적으로 불리하다.
네이티브는 로컬 자원만 읽지만, 웹뷰는 네트워크를 타야 하므로 기본적으로 한 템포 늦게 출발한다.
그래서 우아한형제들 프론트엔드팀은 "웹도 네이티브처럼 빠를 수 있을까?"라는 질문을 던지며 성능 최적화 여정을 시작했다.
SSR의 함정 — 첫 번째 시행착오

초기 웹 아키텍처는 SSR(Server-Side Rendering) 기반이었다.
서버에서 HTML을 만들어 내려주는 구조지만, 사용자 피드백은 한결같았다.
"버벅거려요."
"로딩이 너무 느려요."
당시 초기 성능 측정은 아래와 같았다.
- FCP (첫 요소 표시): 1.5초
- LCP (가장 큰 요소 표시): 2.35초
- Load (전체 렌더링 완료): 4.5초
SSR은 모든 API 응답을 기다린 뒤 HTML을 완성하기 때문에
응답이 느린 하나의 API만 있어도 전체 페이지 로딩이 지연됐다.
Finantial Times에 따르면 로드 시간 지연 1초 당 조회수가 4.6% 감소되고, 로드 시간이 0.1초 개선될 때마다 추가 매출이 1% 상승한다.

배달의 민족 개발팀에서는 크게 FCP, LCP, Load 3가지의 지표를 중심으로 성능 개선 작업을 진행했다.
CSR 전환 + Skeleton UI

위 사진을 보면 알 수 있듯이,
상단 그래프인 SSR 타임라인에서는 "서버 구간 → 필수 API → 다운로드 → 렌더 → JS 실행" 순서로 진행된다.
즉, SSR은 API 완료 전까지 HTML 생성이 불가하다. (빈 화면)
하지만 하단 그래프인 CSR 타임라인에서는 "JS 실행 → 렌더 → 필수 API" 순서로 진행된다.
즉, JS가 오자마자 스켈레톤 UI 렌더링이 가능하여 사용자에게 빠른 화면 표시가 가능하다.
FCP와 LCP를 비교해보면,
SSR에서는 FCP/LCP 모두 늦는 반면, CSR에서는 JS가 다 로드된 후에야 실제 콘텐츠를 채울 수 있기 때문에 LCP가 늦지만, 스켈레톤 UI를 사용자에게 먼저 보여줄 수 있기에 FCP를 크게 단축할 수 있다.
또한, 서비스 특성상 SEO가 불필요하고, 트래픽이 곧 서버 비용으로 이어지는 구조였다.
그래서 결론적으로 우아한형제들 FE 개발팀은 SSR의 한계를 인정하고 CSR(Client-Side Rendering) 으로 전환했다.
이 과정에서 스켈레톤 UI를 추가하여 "아무것도 안 뜨는 흰 화면" 대신, 로딩 중임을 직관적으로 보여주는 시각적 피드백을 제공했다.
결과는 어땠을까?
- FCP 39% 단축
- 초기 체감 속도 개선
- 사용자 만족도 향상
이미지 최적화 — Lazy Loading + S3 리사이저

CSR로 전환 이후에도 "로딩이 끝나지 않는다" (무한 로딩 스피너)라는 문제가 있었다.
페이지는 열리지만, 상품 이미지들이 끝없이 로딩 중인 상태가 발생했다.
이 문제의 근본 원인은 너무 많은 리소스를 한 번에 불러오는 구조에 있었다.
사용자 입장에서는 “끝없이 돌아가는 스피너”를 보는 게 가장 큰 피로였어요.
배민 특정 페이지에서 상품 이미지 수가 264개 이상, 용량은 1MB 이상인 경우도 있었기에 이러한 문제들을 해결하고자 하였다.
이를 해결하기 위해 배민 팀은 스켈레톤 UI 외에도 이미지 Lazy Loading과 S3 이미지 리사이즈 시스템을 도입하였다.
첫번째로 이미지 Lazy Loading을 적용을 살펴보면,
<img loading='lazy' /> 코드와 가이 lazy를 적용하여 보이지 않는 영역 이미지는 지연 로드하여 초기 로딩 부담 경감하였다.

또한, S3 + Lambda 기반 이미지 리사이즈 시스템 구축하였다.
→ 최초 호출 시 썸네일 생성, 이후 캐시 활용
이전에 본인도 lambda 함수를 활용하여 이미지 리사이즈 시스템을 구축을 하여 이미지 최적화를 한 경험이 있었기에 이 부분이 더욱 와닿았다.
효과는 과연 어땠을까?
- 이미지 요청 수 297 → 53개 (-70%)
- LCP 10.8% 개선
- Load 시간 86% 단축
- 평균 이미지 크기 5MB → 1MB 이하
캐시 정책 복원

웹뷰 설정이 캐시 비활성화 상태였다.
즉, 앱을 열 때마다 수 MB의 JS·이미지 번들을 매번 다시 다운로드 중이었다.
그래서 아래와 같이 조치하였다.
- Android/iOS의 캐시 정책을 LOAD_DEFAULT, LOAD_CACHE_ELSE_NETWORK로 전환
- 배민 전사 웹 페이지 영향 검증
- 캐시 초기화 기능 탑재로 사용자 보호
결과는 아래와 같이 개선되었다.
- FCP 32% 개선
- LCP 28% 개선
- Load 27% 단축
- AWS 비용 55.8% 감소 (연 3천만 원 절감)
렌더링 절약 — Server Driven UI
하단 보이지 않는 컴포넌트까지 전부 렌더링하던 코드를 수정했다.
서버에서 어떤 구성 요소를 어떤 순서로 그릴지 지시하는 Server Driven UI(SDU) 구조를 도입하여 아래가 같이 추가적으로 더 개선할 수 있었다.
- 초기 JS 실행량 감소
- LCP 17% 추가 개선
중요한 일을 먼저 하자 필수 API preload

성능을 개선하는 과정에서 우리는 또 다른 병목을 발견했다.
우리는 이것들 기억해야한다. "자바스크립트는 공짜가 아니다."
CSR은 초기 렌더링을 클라이언트가 담당하기 때문에, JS 파일을 받는 순간부터 브라우저의 파싱 → 평가 → 실행이 시작된다.
이때 JS가 너무 크면 HTML이 파싱 중단되고, 렌더링 자체가 지연된다.

각 페이지에서 반드시 필요한 **필수 API(critical API)**를
JS 실행 이전 시점에 **미리 호출(preload)**하도록 변경하였다.
JS 평가가 시작되기 전에 해당 API를 먼저 실행하고, 그 결과를 HTML 상단의 인라인 스크립트로 삽입해 두면,
React 렌더링 시점에서는 이미 그 데이터를 **전역 캐시(prefetchGlobalCache)**에서 즉시 불러올 수 있다.

이렇게 하면 JS 파싱과 렌더링이 완료되는 순간, 이미 데이터가 준비되어 있기 때문에 FCP/LCP가 자연스럽게 단축되죠.
다만 HTML 파싱을 블로킹할 수 있기 때문에, 이 인라인 스크립트의 용량은 최소화되어야 한다.
결론적으로 렌더링 시 React는 이미 캐시된 응답 데이터를 즉시 사용하게 되어 FCP/LCP 모두 30% 이상 단축됐다.
Code Splitting — 중요하지 않은 건 나중에

모달, 팝업, 배너 등 초기에 불필요한 UI 컴포넌트는 React.lazy + Suspense로 동적 import하여 처리했다.
const LazyComponent = React.lazy(() => import('./LazyComponent'));→ 번들 크기 감소
→ 초기 구동 속도 향상
추가: 리스트 가상화(Virtualization) — 메모리 관리의 마지막 퍼즐

마지막으로, 웹이 앱처럼 느려지는 또 하나의 이유는 바로 긴 리스트를 전부 렌더링하기 때문이었다.
네이티브 앱은 화면에 보이는 아이템만 렌더링하지만,
웹은 기본적으로 DOM에 모든 아이템을 생성합니다.
이로 인해 메모리가 과도하게 사용되고, GC(Garbage Collection)가 자주 일어나 프레임 드랍이 발생했죠.
**"B마트 홈처럼 상품이 수천 개에 달하는 화면"**에서는 CSR로 모든 상품을 한 번에 그리면 메모리 사용량이 폭증하고,
가비지 컬렉션(GC)으로 인한 UI 프레임 드랍 또는 앱 크래시가 발생할 수 있었다.
그래서 이를 해결하기 위해 TanStack Virtual을 도입했다.
"우리는 웹이지만, 앱처럼 작동해야 했다."
새로운 바퀴를 만들 필요는 없었다.
팀은 다양한 오픈소스 중에서 TanStack Virtual을 선택했다.
선택 이유는 아래와 같다.
- 동적으로 항목 높이를 계산 가능
- 스크롤 위치 기반으로 최소 DOM 유지
- TanStack Query와의 궁합도 우수
메모리 프로파일링 결과와 아래와 같이 개선되었다.
- 가상화 적용 전: 메모리 사용량이 스크롤과 함께 지속 상승 (우상향)
- 가상화 적용 후: 메모리 사용량이 일정 수준에서 유지, GC 안정화
덕분에 긴 상품 리스트에서도 스크롤이 부드럽게 유지되고, JS heap 메모리와 렌더링 부하 모두 안정화되었다.
최종 성과 결과
| 지표 | 개선 전 | 개선 후 | 개선률 |
|---|---|---|---|
| FCP | 1.5s | 0.6s | 🔻58% |
| LCP | 2.35s | 1.5s | 🔻35% |
| Load | 4.5s | 0.4s | 🔻90% |
-
AWS 트래픽 비용
-
월간 55.8% 절감
-
연간 약 3,000만 원 절약
-
실사용자 데이터 (Sentry Sample 기반)
- FCP 대부분 300ms 이하
- LCP 대부분 1초 전후로 단축
개인적으로 들었던 궁금증
배민 장보기 서비스를 앱에서 웹으로 전환 했을 때 초기 웹 아키텍처는 Server-Side Rendering(SSR)이었지만
해당 배민 장보기 서비스에서는 여러 API를 호출하는데 SSR 서버가 모든 API 응답을 기다린 뒤 완성된 HTML을 만들어 WebView로 반환하는데 한 API라도 느리면 전체 HTML이 늦게 도착하기 때문에 CSR로 전환했다고 이해하였다.
여기서 갑자기 의문점이 생겼다.
여러 API들을 병렬처리 하는 등 백엔드를 개선할 수 있었을텐데 굳이 왜 FE 아키텍쳐를 바꿨을까? 라는 생각을 했다.
스스로 답변을 해봤을때 단순히 백엔드에서 병렬처리 등으로 잘 처리하면 좋겠지만 결국에는 SSR 구조 자체가 한 번에 모든 걸 기다려야 하는 구조라 로딩 중에는 아무것도 안 보이는 문제는 해결되지 않아서 FE 아키텍처를 바꿨을 것 같다. 라는 생각을 했습니다.
하지만 Next.js로 마이그레이션하거나 renderToPipeableStream등을 사용하여 SSR을 사용했을때 Streaming SSR을 사용하면 데이터 오기 전에 로딩 띄울 수는 없을까? 라는 생각을 했지만 트래픽이 커지면 비용적 측면에서 문제가 여전히 있을거라고 생각했다.
그래서 우아한형제 FE팀에서 SSR에서 CSR로 아키텍처를 바꾼 근본적인 이유가 무엇인지 궁금증이 생겼다.
마무리
최적화는 일회성이 아니다.
팀은 Sentry Performance + Slack 연동으로
특정 기준 이하로 성능이 떨어지면 실시간으로 리포팅되게 했다.
이 덕분에 배포 후 성능 저하를 즉시 탐지·조치하고
UX가 퇴보하는 것을 방지했다.
우희제 발표자님께서 마지막에 이렇게 말씀하셨다.
“저희가 한 건 대단한 기술이 아닙니다.
기본을 정말 철저히 지킨 것뿐입니다.”
스켈레톤, 캐시, 이미지 리사이즈, 코드 스플리팅, 리스트 가상화 등 모두 프론트엔드 개발자라면 알고 있는 개념이다.
하지만 그 기본을 끝까지 밀어붙이고 수치로 증명하는 과정이,
진짜 우아한 기술이었다.
이 세션은 단순한 기술 발표가 아니라,
"웹이 어디까지 네이티브를 대체할 수 있는가"에 대한 실험 보고서였다.
- 단순한 최적화가 아니라 사용자 경험 중심의 구조적 리디자인
- 기술 선택보다 기술 운영과 측정의 문화화
- 협업, 검증, 자동화로 이어지는 프론트엔드 팀의 체계적인 성장
웹도 네이티브처럼 빠를 수 있다.
다만 그것은, "기술"보다 "집요함"의 문제다.
이번 2025 우아콘을 통해서 우아한형제들의 프론트엔드 개발자 팀은 서비스를 지속적으로 개선하고 최적화를 하기 위해 얼마나 노력을 기울이고 있는지 생생하게 보여주었다.
우아콘을 통해서 스스로도 많이 배우고 느낄 수 있었던 값진 시간이었다. 앞으로 프론트엔드로서 성장하기 위해 이러한 기술 발표들을 자주 참가하여 새로운 기술들을 배우고 공유하는 기회를 많이 가져야겠다는 생각을 가지게 되었다.