이벤트 페이지 성능최적화 (LCP 37-> 2.5, CLS 0.219→0.006)

2025. 8. 18. 21:52·⛲ 프로젝트

 

이벤트 목록을 보러 들어온 사용자가 첫 화면을 보기까지 30초 이상 기다린다면, 이미 절반은 떠났다고 봐야 한다..

스크롤도, 탭 전환도, 버튼 클릭도 먹지 않는 “멈춤”의 순간이 길어질수록 신뢰는 빠르게 무너지는 경험을 해봤는데,이런 체감 지연을 어떻게 개선했는지,  그리고 왜 그런 문제가 생겼는지부터 적어보자

 

문제 상황

0. 페이지 전체가 Client Component

  • 기존에는 페이지 전체가 use client로 감싸진 클라이언트 전용 렌더링(CSR) 구조였고, 서버는 사실상 빈 HTML을 내려보낸 뒤 브라우저가 모든 컴포넌트를 다운받아 하이드레이션을 마칠 때까지 화면에 보여줄 게 거의 없었다.
  •  반대로 서버 렌더링(SSR) 을 쓰면 서버가 곧바로 HTML을 만들어 보내기 때문에 사용자는 즉시 UI의 뼈대를 확인할 수 있다
  • “서버에서 최대한 빨리 그려주고, 상호작용은 뒤따라 붙인다”를 적용한 것이다.

1. 무거운 컴포넌트 즉시 실행

  • 초기 진입과 동시에 event-tabs 같은 큰 컴포넌트가 즉시 평가·실행되면서 메인 스레드를 오래 점유했고, 그 결과 TBT(Total Blocking Time) 이 커지고 입력 반응성은 무뎌졌다.
  • 탭 전환도 router.replace() 같은 클라이언트 라우팅에만 의존해 초기 로드 시 불필요한 자바스크립트가 더 실행되었고 여기에 로딩 상태에서 스켈레톤도, 레이아웃 고정도 없는 공백 화면까지 겹치면서 “느리고 비어 있는” 첫인상이 되었다.

2. 라우팅 로직의 불필요한 JS 의존

  • router.replace() 방식으로 탭 전환을 구현.
  • 네이티브 네비게이션이 아닌 클라이언트 사이드 라우팅에만 의존 → 초기 로딩 시 불필요한 JS 실행.

3. 데이터 로딩 중 UI 공백

  • 로딩 중 스켈레톤이나 레이아웃 고정이 없어, 시각적으로 빈 페이지가 보임.
  • 체감 속도와 신뢰도 저하.

 

 

서버에서 뼈대 UI를 즉시 그려주고, 무거운 로직은 뒤로 미루자

로딩 중엔 스켈레톤으로 레이아웃을 고정해 사용자의 체감 속도를 높이자 

 

 

🔖개선 과정 — 무엇을, 어떻게, 왜 바꿨나

1) 페이지 구조: Client → Server Component 전환

  • 기존에는 페이지 전체가 use client로 시작해 모든 UI가 클라이언트 하이드레이션 이후에만 보였다.
  • 서버 컴포넌트로 전환해, 서버가 처음부터 HTML을 만들어 내려주게 했다.

How

  • useRouter, useSearchParams 대신  searchParams를 사용해 탭 상태를 읽는다.
  • 상호작용이 필요한 부분만 클라이언트 컴포넌트로 분리
// ✅ 서버에서 바로 HTML 생성
export default function EventPage({ searchParams }: { searchParams: { tab?: string } }) {
  const tabParam = searchParams.tab ?? "apply";
  return (
    <>
      {/* 서버에서 그려지는 영역 (즉시 픽셀) */}
      <EventBannerSection />
      {/* 상호작용만 클라이언트에서 */}
      <EventTabsClient tab={tabParam} />
    </>
  );
}

 

Why

  • 서버 렌더링(SSR) 은 브라우저가 JS 받기 전에도 즉시 그림(First Paint) 이 가능 → “바로 뜨는 느낌”.
  • 하이드레이션해야 할 범위를 줄여 JS 실행량↓, hydration 비용↓ → 입력 반응성 개선.
  • 위상(상단 폴드) 뼈대가 서버에서 찍히므로 눈에 보이는 공백 시간이 사라짐.

 

2) 배너요소 SSR 적용 (LCP 최적화)

  • LCP(Largest Contentful Paint)로 잡히는 배너 섹션을 CSR에서 SSR로 전환.

How

  • 기존: dynamic(() => import(...), { ssr: false })
  • 변경: ssr: true로 설정해 서버에서 렌더되도록 전환
const EventBannerSection = dynamic(
  () => import("./_components/event-banner-section"),
  { ssr: true, loading: () => null }
);

 

Why

  • LCP 후보(배너)가 HTML로 바로 내려와 37초 → 2.4초로 단축
  • 사용자는 “아무것도 없는 빈 화면” 대신 즉시 주요 콘텐츠(배너)를 보게 되어 신뢰 증가
  • 서버 스트리밍/프리렌더의 이점을 활용해 초반 네트워크 워터폴을 짧게 만듦.

 

3) 라우팅 최적화 router.replace() → <Link>

  • 탭 전환을 클라이언트 라우터 API로만 처리하던 것을,
  • 브라우저가 최적화 가능한 네이티브 네비게이션 경로인 <Link> 기반으로 변경

How

  • 버튼 클릭 핸들러 제거(불필요 JS 제거)
  • <Link href="..."> 사용 + replace, scroll={false}, prefetch={false}로 초기 비용 최소화
// ✅ 네이티브 경로 활용 + 초기 비용 최소화
<Link
  href={`?tab=${tab.value}&toggle=0`}
  replace
  scroll={false}
  prefetch={false}
>
  탭 변경
</Link>

 

Why

  • 클릭마다 불필요한 JS 핸들러가 실행되지 않으므로 메인 스레드 점유 감소.
  • prefetch={false}로 초기 진입 시 프리패치 트래픽을 줄여, 첫 페인트까지의 대역폭을 핵심 리소스에 집중.
  • Next.js의 네이티브 최적화(히스토리 교체, 스크롤 관리)를 활용해 부드러운 전환과 낮은 JS 부담 동시 달성.

 

4) 탭 본문: 동적 import + 스켈레톤 (지연 로딩 & CLS 방지)

 
  • 탭 콘텐츠 컴포넌트(무거운 로직)는 첫 페인트 이후로 미루고, 로딩 중에는 스켈레톤으로 레이아웃을 고정

How

  • dynamic()로 클라이언트 전용 컴포넌트를 지연 로딩
  • loading에 스켈레톤을 넣어 레이아웃/공간을 미리 확보(CLS 방지)
const EventTab = dynamic(() => import("./_components/event-tabs"), {
  ssr: false,
  loading: () => <CardSkeleton />, // 레이아웃 고정 + 체감 로딩 개선
});

 

Why

  • 무거운 JS 청크를 초기 번들에서 제외 → JS 평가/실행 시간↓, TBT 감소 → 입력 반응 즉시성↑.
  • 로딩 중 빈 화면이 아니라 자리 고정된 스켈레톤을 보여 CLS(레이아웃 시프트) 를 방지.
  • “보여주고(SSR) → 채워 넣기(Suspense/동적 로딩)” 흐름으로 심리적 체감 속도를 극대화.

 

 

요약하자면

 

Server Components로 뼈대 UI를 먼저 그려 사용자에게 즉시 구조를 보여주고, 

Client Components는 정말 상호작용이 필요한 영역에만 최소화하여 붙였다.

무거운 위젯은 코드 스플리팅 + 지연 로딩(dynamic import) 으로 뒤로 미뤘고, 상단 폴드는 SSR로 빠르게 뿌리되, 내부 데이터는 Suspense + 스켈레톤으로 스트리밍하듯 채워 넣었다. 이렇게 하면 “보여줄 것”과 “나중에 붙일 것”이 분리되며, 사용자는 즉시 UI를 보고, 곧 이어 체감 로딩이 완료되는 경험 하도록 했다.

 

성능 변화

 
기존
개선 후

 

 

LCP (Largest Contentful Paint) 37.0 s 2.5 s −34.5 s ↓ 93.2% 14.8× 빠름
TBT (Total Blocking Time) 2050 ms 40 ms −2010 ms ↓ 98.0% 51.3× 감소
CLS (Cumulative Layout Shift) 0.219 0.006 −0.213 ↓ 97.3% 36.5× 안정

 

LCP가 37.0초에서 2.5초로 줄었고(−34.5초, 약 93%↓),

TBT는 2,050ms에서 40ms로 떨어졌다.(약 98%↓).

CLS도 0.219 → 0.006으로 안정화됐고, FCP 0.9s / Speed Index 1.1s 수준으로 첫 화면 표출과 전반 체감 속도 모두 개선됨

사용자 입장에서는 “바로 뜨고, 바로 반응하고, 화면이 안 흔들리는” 상태에 가까워졌다고 볼 수 있을 것 같다. 

 
 
 
 

 

'⛲ 프로젝트' 카테고리의 다른 글

10,000건 테스트, MSW 기반 대용량 가상+무한 스크롤 적용  (6) 2025.08.12
카카오톡에서 앱 설치 유도하기: PWA 외부 브라우저 리다이렉션 전략  (5) 2025.08.06
Home 진입 40초 → 2.5초, LCP를 93% 줄인 Next.js 최적화  (3) 2025.08.05
이미지 업로드부터 OCR 자동화까지, 세금계산서 등록 프로세스  (1) 2025.07.21
(husky) Husky와 Lint-staged를 이용한 Lint 자동화  (1) 2024.11.23
'⛲ 프로젝트' 카테고리의 다른 글
  • 10,000건 테스트, MSW 기반 대용량 가상+무한 스크롤 적용
  • 카카오톡에서 앱 설치 유도하기: PWA 외부 브라우저 리다이렉션 전략
  • Home 진입 40초 → 2.5초, LCP를 93% 줄인 Next.js 최적화
  • 이미지 업로드부터 OCR 자동화까지, 세금계산서 등록 프로세스
gprorogpfus
gprorogpfus
:- >
  • gprorogpfus
    gpfusdldks
    gprorogpfus
  • 전체
    오늘
    어제
    • 분류 전체보기 (55)
      • 🎭JavaScript (2)
      • 👚 CSS (1)
      • ⚛️ React (13)
      • 🌃 Next.js (5)
        • 🔜 next.js-study (3)
      • 🥏TypeScript (10)
      • 🏴알고리즘 (2)
      • 🌴트러블슈팅 (3)
      • ⛲ 프로젝트 (6)
        • 👖gproro-shop-app (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    JavaScript
    react
    Redux
    GIT
    TypeScript
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
gprorogpfus
이벤트 페이지 성능최적화 (LCP 37-> 2.5, CLS 0.219→0.006)
상단으로

티스토리툴바