Skip to content

[Perf] LCP 개선을 위한 Font/Icon/CardTag 컴포넌트 렌더링 최적화 (2.3s → 1.0s)#396

Merged
Chiman2937 merged 4 commits intomainfrom
chiyoung-perf/lcp
Mar 24, 2026
Merged

[Perf] LCP 개선을 위한 Font/Icon/CardTag 컴포넌트 렌더링 최적화 (2.3s → 1.0s)#396
Chiman2937 merged 4 commits intomainfrom
chiyoung-perf/lcp

Conversation

@Chiman2937
Copy link
Member

@Chiman2937 Chiman2937 commented Mar 24, 2026

📝 변경 사항

화면 렌더링 지연 문제 개선 변경사항

브랜치: chiyoung-fix/card-tag
커밋 범위: e2c4832 ~ 919f2df (3개 커밋)
변경 파일: 7개 (154 additions, 311 deletions)


커밋 히스토리

커밋 메시지
e2c4832 fix: pretendard font CDN Dynamic Subset 방식으로 전환
dcff104 fix: card-tag가 useLayoutEffect에 의해 display갯수가 정해지던 방식에서 css overflow 방식으로 전환
919f2df fix: svg sprite 방식에서 SVGR 방식으로 변경

1. Pretendard 폰트 CDN Dynamic Subset으로 전환

문제 원인

next/font/local로 Variable 폰트 전체(~2MB)를 로드하고 있어, 폰트 다운로드가 완료될 때까지 텍스트가 흔들리는 현상(FOUT)이 발생했음. 이로 인해 Lighthouse 측정 시 LCP가 Mobile 기준 13.82초, Desktop 기준 2.4초까지 늘어남.


수정 1. src/lib/fonts.ts 삭제

// Before - next/font/local로 Variable 폰트 전체 로드
import localFont from 'next/font/local';

export const pretendard = localFont({
  src: [{ path: '../assets/fonts/PretendardVariable.woff2', weight: '45 920' }],
  variable: '--font-pretendard',
  display: 'swap',
});

수정 2. src/app/layout.tsx - pretendard className 제거

// Before
<body className={`${pretendard.className} ${pretendard.variable} antialiased`}>

// After
<body className='antialiased'>

수정 3. src/styles/globals.css - CDN Dynamic Subset import 추가

/* Before - 별도 폰트 import 없음 */
@import 'tailwindcss';

/* After - CDN Dynamic Subset 방식으로 전환 */
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
@import 'tailwindcss';

body {
  font-family:
    'Pretendard Variable',
    Pretendard,
    -apple-system,
    BlinkMacSystemFont,
    system-ui,
    sans-serif;
}

Dynamic Subset은 페이지에서 실제로 사용되는 글자만 포함된 서브셋 파일을 동적으로 조합하여 로드하므로, Variable 폰트 전체(~2MB)를 받는 대신 필요한 글자만 요청하여 약 263KB로 87% 감소.


2. card-tag CSS overflow 방식으로 전환

문제 원인

useLayoutEffect + ResizeObserver로 컨테이너 너비를 측정하여 보이는 태그 수를 JS로 계산하는 방식이었음. SSR 시점에는 DOM이 없으므로 클라이언트에서 hydration 이후에야 태그 표시 여부가 결정되어 렌더링 지연 현상이 발생했음.

// Before - 'use client' + useLayoutEffect 기반 JS 너비 계산
'use client';

useLayoutEffect(() => {
  updateVisibleTags(); // DOM 측정 후 setState → 리렌더링 발생
  const resizeObserver = new ResizeObserver(() => updateVisibleTags());
  resizeObserver.observe(container);
  return () => resizeObserver.disconnect();
}, [updateVisibleTags]);

return (
  <div ref={containerRef} className='mt-1 flex min-h-5 gap-1'>
    {tags?.map((tag, index) => {
      const isVisible = lastVisibleIndex !== null && index <= lastVisibleIndex;
      return (
        <span className={isVisible ? BASE_TAG_CLASSES : HIDDEN_TAG_CLASSES}>
          {tag.label}
        </span>
      );
    })}
  </div>
);
// After - CSS overflow: hidden으로 SSR 시점부터 처리
return (
  <div className='mt-1 flex max-h-4.5 flex-wrap gap-1 gap-y-0 overflow-hidden'>
    {tags?.map((tag) => (
      <span className='bg-mint-100 text-text-2xs-medium text-mint-700 inline-flex shrink-0 items-center rounded-full px-2 py-0.5'>
        {tag.label}
      </span>
    ))}
  </div>
);

'use client' 제거로 서버 컴포넌트로 전환. SSR 시점부터 모든 마크업이 HTML에 포함되며, CSS overflow: hidden으로 넘치는 태그를 잘라냄. JS로 레이아웃을 계산하던 방식에서 브라우저의 CSS 렌더링에 위임하는 방식으로 변경하여 클라이언트 렌더링 지연이 사라짐.


3. 아이콘 SVG sprite → SVGR 방식으로 전환

문제 원인

flexisvg 기반 SVG sprite 방식은 <use> 태그를 사용하여 아이콘을 렌더링함. <use> 태그는 브라우저의 SVG 렌더링 엔진이 sprite 파일을 fetch한 이후에야 아이콘이 그려지는 방식으로, SSR 시점 HTML에 아이콘 마크업이 포함되지 않음. 이로 인해 초기 로드 시 아이콘 및 버튼이 제대로 보이지 않고 깜빡이는 현상이 발생했음.

// Before - flexisvg SVG sprite 방식 (런타임 HTTP 요청 발생)
import type { IconMetadata } from 'flexisvg';

export type DynamicIconId = 'arrow-down' | 'arrow-up' | ...;
export type ResizableIconId = 'bell-unread' | 'congratulate' | ...;
// After - SVGR 방식 (SVG를 React 컴포넌트로 import)
import { iconComponentMap } from './registry';

// registry/index.ts에서 각 SVG를 직접 import

// Breaking Changes를 만들지 않기 위해 기존 Icon 사용 구조 유지하며 내부 구조만 변경함

import IconArrowDown from '../../../../public/icons/dynamic/icon-arrow-down.svg';
// ...
export const iconComponentMap = { 'arrow-down': IconArrowDown, ... };

SVGR로 전환하면 각 SVG가 빌드 타임에 JS 번들에 인라인으로 포함됨. 런타임 HTTP 요청이 사라지고, JS 번들은 브라우저에 캐시되므로 재방문 시 추가 네트워크 비용 없이 즉시 렌더링됨. SSR 시점 HTML에도 SVG 마크업이 직접 포함되어 아이콘 로딩 지연이 해소됨.


개선 결과

지표 개선 전 개선 후
LCP (Desktop) 2.3s 1.0s
Lighthouse 성능 점수 (Desktop) 88 99
폰트 파일 크기 ~2,058KB ~263KB (87% 감소)

변경된 파일 목록

파일 변경 유형
src/lib/fonts.ts 삭제 (next/font/local 제거)
src/app/layout.tsx 수정 (pretendard className 제거)
src/styles/globals.css 수정 (CDN Dynamic Subset import 추가)
src/components/shared/card/card-tags/index.tsx 수정 (useLayoutEffect → CSS overflow)
src/components/shared/card/card-tags/index.test.tsx 수정 (불필요 테스트 제거)
src/components/icon/index.tsx 수정 (flexisvg → SVGR 방식)
src/components/icon/registry/index.ts 추가 (SVG 컴포넌트 레지스트리)

🔗 관련 이슈

Closes #


🧪 테스트 방법

  • 수동 테스트 검증(로컬 환경)
  • 유닛 테스트 검증
  • 통합 테스트 검증

📸 스크린샷 (선택)

전/후 비교 영상(뚜렷한 차이 확인을 위해 CPU x4 감속, Slow 4G 적용

  • 변경 전
_2026_03_24_17_45_24_118.mp4
  • 변경 후
_2026_03_24_17_46_07_34.mp4

📋 체크리스트

  • 관련 문서를 업데이트했습니다 (필요한 경우)
  • 테스트를 추가/수정했습니다 (필요한 경우)
  • Breaking change가 있다면 명시했습니다

💬 추가 코멘트


CodeRabbit Review는 자동으로 실행되지 않습니다.

Review를 실행하려면 comment에 아래와 같이 작성해주세요

@coderabbitai review

@github-actions
Copy link

github-actions bot commented Mar 24, 2026

🎨 Storybook Report

Story가 변경되었습니다

Chromatic에서 비주얼 변경사항을 확인하세요.

Status Storybook Build Log Updated (UTC)
✅ Ready View Storybook View Build 2026-03-24 08:57:08

@github-actions
Copy link

github-actions bot commented Mar 24, 2026

🎭 Playwright Report

E2E Test가 성공적으로 완료되었습니다.

Test 요약 내용을 확인해주세요.

Status Build Log Updated (UTC)
✅ Ready View Build 2026-03-24 08:57:15

📊 Test Summary

  • ✅ Passed: 3
  • ❌ Failed: 0
  • ⏱️ Duration: 27.3s

📜 Test Details

✅ Passed Tests (3)
  • profile.test.ts (3)
    • [chromium] 존재하지 않는 프로필 페이지로 접속 시 404 redirect 되는 지 테스트
    • [firefox] 존재하지 않는 프로필 페이지로 접속 시 404 redirect 되는 지 테스트
    • [webkit] 존재하지 않는 프로필 페이지로 접속 시 404 redirect 되는 지 테스트

@github-actions
Copy link

github-actions bot commented Mar 24, 2026

🚀 PR Preview Report

Build가 성공적으로 완료되었습니다.

Preview에서 변경사항을 확인하세요.

Status Preview Build Log Updated (UTC)
✅ Ready Visit Preview View Logs 2026-03-24 08:57:27

@github-actions
Copy link

github-actions bot commented Mar 24, 2026

📊 Coverage Report

Status Build Log Updated (UTC)
✅ Ready View Build 2026-03-24 08:56:06

📉 #396main에 병합하면 coverage가 0.66% 감소합니다.

Coverage 요약

@@             Coverage Diff             @@
##             main     #396       +/-   ##
===========================================
- Coverage   35.24%   34.58%    -0.66%     
===========================================
  Files         264      264         0     
  Lines       12142    11985      -157     
  Branches      474      461       -13     
===========================================
- Hits         4280     4145      -135     
- Misses       7862     7840       -22     

영향받은 파일

파일 Coverage 변화
/home/runner/work/WeGo_FrontEnd/WeGo_FrontEnd/src/components/icon/registry/index.ts 100.00% (+100.00%) ⬆️
/home/runner/work/WeGo_FrontEnd/WeGo_FrontEnd/src/components/shared/card/card-tags/index.tsx 100.00% (+7.02%) ⬆️
/home/runner/work/WeGo_FrontEnd/WeGo_FrontEnd/src/mock/svg.tsx 100.00% (+100.00%) ⬆️

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 24, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 1d598028-8a35-4f73-9925-a1ff9317a0b3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chiyoung-perf/lcp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Chiman2937 Chiman2937 merged commit 53c546e into main Mar 24, 2026
6 of 7 checks passed
@Chiman2937 Chiman2937 deleted the chiyoung-perf/lcp branch March 24, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant