FCP 85% 개선, 데이터 스케일링이 초래한 렌더링 병목 해결하기
FCP 85%, DCL 89.5% 개선하기
CPR cloud 리뉴얼 업데이트 이후 데이터가 쌓이면서 전체 데이터를 확인할 수 있는 어드민 계정에게 치명적인 성능 저하가 발생했어요. 특히 대시보드 페이지에서는 약 10개의 차트를 그리기 위한 데이터를 쿼리해야 했기 때문에 페이지 로드가 최대 20초 가량 걸리는 상황이 발생했어요. 이는 단순한 버그가 아니라, 데이터 스케일이 커짐에 따라 아키텍처가 한계에 다다랐다는 신호였어요.
빠른 해결 전략: 프론트엔드 렌더링 최적화
가장 시급한 문제는 사용자가 20초 동안 아무것도 없는 빈 화면을 봐야한다는 점이었어요. 근본 원인을 파악하기 전에 사용자 경험을 최소한으로 보호하기 위해 프론트엔드 단에서 응급 처치를 진행했어요.
Streaming SSR 도입
서버에서 모든 데이터 조회가 끝날 때까지 페이지 전체 렌더링이 지연되는 것을 방지하기 위해 React의 <Suspense />를 활용한 Streaming SSR을 도입했어요. 데이터 쿼리가 오래 걸리는 각 Widget을 Suspense로 감싸 로딩 중에는 실제 UI와 동일한 스켈레톤 UI을 보여줄 수 있게끔 긴급하게 업데이트했어요.
import { Suspense } from "react";
import { LongLoadingWidget } from "./LongLoadingWidget";
import { LongLoadingWidgetSkeleton } from "./LongLoadingWidgetSkeleton";
<Suspense fallback={<LongLoadingWidgetSkeleton />}>
<LongLoadingWidget data={longLoadingDataFromServerSide} />
</Suspense>;이를 통해 사용자는 더이상 빈 화면을 기다릴 필요 없이 준비된 컴포넌트부터 순차적으로 확인할 수 있게 되었어요. 이는 FCP를 일부 개선하고 CLS를 방지하는 효과를 가져왔어요. 다만 여전히 Chrome DevTools Network 탭에서 확인한 Server Timing이나 Performance 탭의 DCL은 평균 약 18.2초나 걸렸어요.
기타 프론트엔드 최적화
Streaming SSR 이외에도 비동기 로직 병렬 처리를 통해 여러 데이터 요청을 동시에 처리할 수 있도록 하거나 코드 스플리팅을 활용해 무거운 차트 라이브러리를 분리하여 First Load JavaScript를 줄였지만 여전히 필드 데이터(RUM)인 TTFB, 실험실 데이터인 Server Timing 지표는 거의 개선되지 않았어요.
문제 재정의: 근본적인 렌더링 지연 원인 파악
프론트엔드 최적화를 통해 약간이나마 FCP를 개선하기는 했으나 여전히 필드 데이터(RUM)인 TTFB, 실험실 데이터인 Server Timing 지표는 거의 개선되지 않았다는 점은 서버에서 첫 번째 데이터를 응답하는 데 걸리는 시간 자체가 근본적인 원인이라 판단했어요.
데이터를 심층 분석한 결과, 사용자가 가진 데이터의 양에 따라 경험이 극명하게 갈리는 '성능 절벽' 현상을 발견했어요.
- 일반 사용자: 자신이 속한 소규모 그룹의 데이터만 보므로, 성능 저하를 거의 체감하지 못함.(1~2초 내외 로딩)
- 파워 유저(팀 관리자 등): 수십 개 이상의 디바이스 데이터를 조회해야 하는 경우, 로딩 시간이 최대 10초에 근접하며 서비스 사용에 불편을 겪기 시작.
- 어드민(내부 계정): 서비스 전체의 모든 디바이스와 사용자 데이터를 한 번에 집계해야 하는 특수한 요구사항으로 인해, 20초 이상의 로딩 시간이 소요.
결국 이 성능 문제는 초기 아키텍처가 현재의 데이터 규모를 감당하지 못해 발생한 구조적인 문제였어요. 특히, 모든 데이터를 실시간으로 JOIN하여 집계하는 방식은 데이터가 많아질수록 기하급수적으로 부하가 증가하여 어드민 계정에서 한계에 도달한 거에요.
근본 원인 해결을 위한 데이터 집계 방식 개선
가장 큰 병목을 유발하는 데이터 조회 및 집계 방식을 개선하는 데 집중했어요.
Read 성능에 최적화된 데이터 모델 도입
핵심 전략은 기존의 정규화된 테이블과는 별개로, 조회 및 분석에 최적화된 새로운 데이터 모델, 즉 '롤업 테이블(Roll-up Table)'을 도입하는 것이에요. 이 방식은 '실시간성'을 15분 단위로 타협하는 대신, 예측 가능하고 안정적인 성능을 얻는 기술적 트레이드오프 방식이에요. 15분마다 스케줄러가 미리 복잡한 JOIN과 집계 연산을 수행해 이 테이블에 결과를 저장하게 돼요.
그 결과 사용자가 요청할 때마다 실행되던 무겁고 복잡한 실시간 쿼리는, 미리 계산된 롤업 테이블을 조회하는 단순하고 빠른 SELECT 쿼리로 대체됐어요. 평균 쿼리 속도의 개선률은 다음과 같아요.
| 기존 로직(ms) | 3,085.95 | 25.84 | 1,661.68 | 2,276.82 | 6,449.01 | 1,300.99 | 1,419.30 | 1,521.16 | 96.26 | 5,528.92 |
|---|---|---|---|---|---|---|---|---|---|---|
| 개선 로직(ms) | 29.37 | 3.56 | 52.56 | 88.67 | 48.67 | 51.59 | 70.41 | 75.52 | 46.52 | 27.52 |
| 개선률(%) | 99.05% | 86.24% | 96.84% | 96.11% | 99.25% | 96.03% | 95.04% | 95.04% | 51.68% | 99.50% |
결과: 기술적 성과를 넘어 비즈니스 가치로
최적화 배포 후 사용자 행동 데이터를 분석한 결과 기술적 지표 개선을 넘어 의미 있는 비즈니스 임팩트를 확인할 수 있었어요.
| 상황 | 측정 기준 | FCP(RUM) | LCP(RUM) | DCL(LAB) | ST(LAB) |
|---|---|---|---|---|---|
| 이전 | P75 | 20136ms | 20136ms | 37299ms | 37290ms |
| 이후 | P75 | 3080ms | 3080ms | 3909ms | 3660ms |
| 개선률 | P75(%) | 84.7% | 84.7% | 89.5% | 90.2% |
다음 단계: 지속 가능한 성능 문화를 향하여
이번 최적화는 프론트엔드의 응급 처치에서 시작하여, 데이터 분석을 통해 근본 원인을 찾아내고 백엔드의 데이터 집계 구조를 개선하기까지의 전 과정을 돌아봤어요.
이 경험을 통해 성능이 '사후 처리'가 아닌, 새로운 기능을 설계하는 단계부터 함께 고민해야 하는 '아키텍처의 영역'임을 배웠어요. 앞으로 성능 예산(Performance Budget)을 도입하여 모니터링을 자동화하고, 이번에 얻은 최적화 경험을 서비스 전체로 점차 확산해 나갈 계획이에요.
성능 최적화는 일회성 이벤트가 아닌, 끊임없이 지속되어야 하는 여정이에요. 이번 최적화 프로젝트는 그 여정의 중요한 첫걸음이었어요.