이벤트 목록을 보러 들어온 사용자가 첫 화면을 보기까지 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 |