Skip to content

SSR 차단 제거와 폰트 경량화를 통한 LCP 최적화

FE_juncci edited this page Feb 23, 2026 · 1 revision

LCP 43.9초 → 8.3초 개선 회고

— SSR 차단 제거와 Pretendard 서브셋 적용을 통한 렌더 경로 정상화


1. 문제 정의: 단순 지연이 아닌 구조적 병목

초기 성능 측정에서 LCP는 43.9초로 관측되었습니다.

이 수치는 네트워크 지연이나 이미지 최적화 부족으로 설명하기 어려운 수준이었습니다. 40초가 넘는 LCP는 일반적인 “느린 페이지”가 아니라, 렌더 경로에 구조적 차단이 존재하는 상태에 가깝습니다.

개선 이후 LCP는 8.3초까지 단축되었습니다.

이는 단순 몇 초 개선이 아니라, 30초 이상이 줄어든 것으로 병목 제거에 가까운 구조 정상화였습니다.

원인은 크게 두 가지였습니다.

  1. SSR 단계에서 LCP 후보가 존재하지 않는 구조
  2. 과도한 폰트 네트워크 payload

2. 렌더링 구조 분석: LCP 후보가 왜 없었는가

기존 SplashGate 구조는 다음과 같았습니다.

// before
if (!mounted) return null;
if (showSplash && SplashScreen) return <SplashScreen />;
return <>{children}</>;

SSR 시점에서 mounted = false이기 때문에, 초기 HTML에는 children이 포함되지 않았습니다.

이를 타임라인으로 정리하면 다음과 같습니다.


🔴 Before: SSR 단계에서 LCP 후보 부재

sequenceDiagram
    participant Server
    participant Browser
    participant JS

    Server->>Browser: SSR HTML (SplashGate = null)
    Browser->>Browser: Initial Paint (LCP 후보 없음)
    Browser->>JS: JS 다운로드
    JS->>Browser: Hydration (mounted = true)
    JS->>Browser: children 렌더
    Browser->>Browser: LCP 후보 등장 (지연 기록)
Loading

핵심 문제

  • SSR HTML에 메인 콘텐츠 없음
  • LCP 후보가 초기 HTML에 존재하지 않음
  • Hydration 이후에야 LCP 후보 등장

즉,

LCP가 늦은 것이 아니라, LCP 후보 자체가 늦게 등장하는 구조였습니다.


3. 구조 개선: SSR은 유지하고 Splash는 Overlay로

해결 방식은 구조를 뒤집는 것이었습니다.

  • children은 항상 SSR로 렌더
  • Splash는 overlay로만 표시
// after
return (
  <>
    {children}
    {showSplash && SplashScreen ? (
      <div className="absolute inset-0 z-50 bg-white">
        <SplashScreen />
      </div>
    ) : null}
  </>
);

🟢 After: SSR 단계에서 LCP 후보 즉시 존재

sequenceDiagram
    participant Server
    participant Browser
    participant JS

    Server->>Browser: SSR HTML (children 포함)
    Browser->>Browser: Initial Paint (LCP 후보 존재)
    Browser->>Browser: LCP 확정
    Browser->>JS: JS 다운로드
    JS->>Browser: Splash overlay 표시
Loading

변경의 의미

  • SSR 단계에서 LCP 후보가 HTML에 포함
  • 브라우저가 즉시 LCP 감지 가능
  • Splash는 시각적으로 가릴 뿐, 렌더를 차단하지 않음

추가 조정 사항

  • fixedabsolute 변경해 app-frame 내부 경계 유지
  • bg-white 적용해 배경 일관성 확보
  • showSplash 초기값 조정해 선노출 플래시 완화

4. 네트워크 병목: Pretendard 폰트 분석

두 번째 문제는 폰트였습니다.

Pretendard를 다음과 같이 9개 weight 모두 로드하고 있었습니다.

100, 200, 300, 400, 500, 600, 700, 800, 900

한글 폰트는 구조적으로 무겁습니다.

초성 19 × 중성 21 × 종성 28 = 11,172자

완성형 한글만 해도 11,172글리프가 존재합니다. 여기에 라틴 문자, 숫자, 특수문자까지 포함되면 weight 하나당 5~7MB 수준이 됩니다.

초기 전송량은 약 7MB 수준이었습니다.

텍스트가 LCP 후보인 경우, 폰트 로딩이 지연되면 LCP 확정도 함께 지연됩니다.


5. 폰트 최적화 전략

1) 사용 weight 축소

실제 UI 사용 기준:

  • 400
  • 500
  • 600
  • 700

불필요 weight 제거.


2) Subset 적용

전체 11,172자 대신:

  • KS X 1001 완성형 (2,350자)
  • 고빈도 200여 자 보강
  • 서비스 실제 사용 문자 추가
  • 기본 라틴/숫자/기호 포함

최종 약 3,000~4,000자 수준으로 제한.


3) WOFF2 포맷 적용

  • TTF/OTF 대신 WOFF2 사용
  • Brotli 압축 기반
  • WOFF 대비 추가 압축

결과

  • 폰트 전송량: ~7MB → ~2MB
  • 초기 네트워크 payload 감소
  • 텍스트 LCP 지연 완화

6. 최종 개선 결과

항목 개선 전 개선 후
LCP 43.9초 8.3초
폰트 전송량 ~7MB ~2MB
SSR 차단 존재 제거

이번 개선은 단순 “최적화”가 아니라,

렌더 경로 정상화 + 네트워크 병목 제거

작업이었습니다.


7. 이번 작업에서 얻은 교훈

  1. LCP가 비정상적으로 길다면, 먼저 LCP 후보가 SSR 단계에 존재하는지 확인해야 한다.
  2. return null 패턴은 SSR 환경에서 치명적일 수 있다.
  3. Overlay는 렌더를 막는 구조가 아니라, 시각적 계층일 뿐이다.
  4. 한글 폰트 최적화는 weight 단위가 아니라 글리프 단위로 접근해야 한다.
  5. 렌더링과 네트워크는 분리된 문제가 아니다.

Clone this wiki locally