-
Notifications
You must be signed in to change notification settings - Fork 1
홈 초기 로딩 경로 재설계를 통한 번들 경량화 회고
페이지 로딩 속도는 사용자 행동에 직접적인 영향을 줍니다. 여러 성능 리서치 자료에서 공통적으로 확인되는 패턴이 있습니다. 페이지가 2초 이내에 로딩되면 이탈률은 약 9% 수준이지만, 로딩 시간이 5초를 넘어가면 30% 이상으로 급격히 증가합니다. 몇 초의 차이가 단순 체감 문제가 아니라 실제 사용자 유지율과 전환에 영향을 준다는 의미입니다.
웹 애플리케이션이 느려지는 원인은 다양합니다. API 응답 지연, 네트워크 환경, 이미지 최적화, 폰트 전략, 크리티컬 렌더링 패스에 포함된 대형 리소스 등 여러 요소가 복합적으로 작용합니다. 이번 작업에서는 그중에서도 JavaScript 번들에 집중했습니다.
이유는 단순합니다. 같은 100KB라도 이미지와 JavaScript는 브라우저 입장에서 처리 비용이 다릅니다.
이미지는 다운로드 후 디코딩 과정만 거치면 되지만, JavaScript는 다운로드 이후 파싱, 컴파일, 실행, 런타임 메모리 점유까지 이어집니다. 특히 초기 렌더 구간에서 JavaScript가 많으면 메인 스레드를 오래 점유하게 되고, 이는 LCP와 TTI와 같은 핵심 성능 지표에 직접적인 영향을 줍니다.
따라서 이번 작업의 목표는 “전체 번들 총량 감소”가 아니라,
초기 렌더에 필요한 JavaScript를 줄이는 것
이었습니다.
번들 최적화를 논의하기 전에, 먼저 현재 상태를 정확히 측정했습니다.
분석에는 @next/bundle-analyzer를 사용했습니다. Next 16 기본 빌드는 Turbopack 기반이지만 analyzer와의 호환 문제로 webpack 빌드를 사용해 측정했습니다.
ANALYZE=true pnpm exec next build --webpack
중요하게 본 지표는 All 총합이 아니라 Entrypoints / Initial이었습니다.
전체 번들 크기는 코드 스플리팅 이후에도 크게 변하지 않을 수 있습니다. 하지만 사용자가 홈에 처음 진입할 때 실제로 다운로드되는 JavaScript는 Initial 영역입니다. 사용자 체감 성능에 영향을 주는 것은 이 구간이기 때문에 비교 기준도 Initial로 설정했습니다.
초기 분석 결과, 홈 진입 시 다음과 같은 대형 모듈이 함께 로드되고 있었습니다.
threerapierlottie-react-
motion계열 모듈
이 라이브러리들은 모두 효과성 UI를 위한 것이었습니다. 스플래시 화면의 3D 애니메이션, 가입 성공 confetti, 채팅 요청 성공 애니메이션 등 특정 조건에서만 동작하는 기능들입니다.
문제는 이 기능들이 항상 필요한 것은 아니라는 점이었습니다. 그럼에도 불구하고 홈 초기 렌더 트리에 정적으로 연결되어 있었기 때문에 조건과 무관하게 항상 번들에 포함되고 있었습니다.
즉,
기능은 조건부인데, 번들은 조건부가 아니었습니다.
이번 작업의 전략은 명확했습니다. 기능을 제거하는 것이 아니라, 초기 로딩 경로에서 분리하는 것이었습니다.
라이브러리를 삭제하지 않았습니다. UX를 축소하지도 않았습니다. 3D 애니메이션과 모션 효과는 그대로 유지했습니다.
다만 목표는 하나였습니다.
무거운 라이브러리를 초기 엔트리에서 이탈시키는 것
초기 진입 시 반드시 필요한 코드와 특정 조건에서만 필요한 코드를 분리하는 것이 핵심이었습니다.
이를 위해 무거운 라이브러리를 직접 import하는 컴포넌트를 분리 경계로 설정했습니다.
- 정적 import 제거
- 상태 플래그가 true일 때만
import()실행 - 모듈 로드 전에는 fallback UI 제공
import Lottie from 'lottie-react';이 구조에서는 이벤트 발생 여부와 무관하게 lottie-react가 초기 번들에 포함되었습니다.
useEffect(() => {
if (!flag) return;
import('lottie-react').then((mod) => {
setLottie(() => mod.default);
});
}, []);이벤트 플래그가 없으면 import 자체가 실행되지 않도록 변경했습니다.
결과적으로 가입 성공 이벤트가 발생하지 않는 경우, lottie-react 번들은 다운로드되지 않습니다.
import { BlurText } from '@/shared/ui/blur-text';useEffect(() => {
import('@/shared/ui/blur-text').then((mod) =>
setBlurText(() => mod.BlurText)
);
}, []);초기에는 정적 텍스트 fallback을 렌더하고, 모듈이 로드된 이후에만 모션 효과를 적용하도록 변경했습니다.
이를 통해 motion 관련 대형 모듈이 초기 엔트리에서 제외되었습니다.
import SplashScreen from './SplashScreen';useEffect(() => {
if (!shouldShow) return;
import('./SplashScreen').then((mod) =>
setSplashScreen(() => mod.default)
);
}, [shouldShow]);스플래시가 필요한 경우에만 모듈을 로드하도록 변경했습니다. 스플래시가 표시되지 않는 세션에서는 관련 3D 및 물리 엔진 번들이 초기 로딩 경로에서 완전히 제외됩니다.
Initial 기준 번들 크기는 다음과 같이 감소했습니다.
- 변경 전: 206.55 KB
- 변경 후: 94.96 KB
총 111.6 KB 감소, 약 54% 경량화되었습니다.
전체 번들 총량(All)은 크게 달라지지 않았습니다. 하지만 사용자가 홈에 처음 진입할 때 다운로드하는 JavaScript는 절반 이하로 줄어들었습니다.
analyzer의 client.html 기준으로 확인했을 때, 초기 엔트리 청크 내부에서 three, rapier, lottie, motion 계열 모듈이 제거된 것을 확인할 수 있었습니다.
이번 작업은 압축이나 트리 쉐이킹 최적화가 아니라,
렌더 트리와 import 경계를 재설계한 결과
였습니다.
첫째, 번들 최적화는 단순한 용량 감소 작업이 아닙니다. “어떤 코드가 언제 로드되는가”를 설계하는 문제입니다.
둘째, 조건부 UI는 반드시 import 경계도 조건부여야 합니다. 렌더링이 조건부라고 해서 번들까지 자동으로 분리되지는 않습니다.
셋째, 분석 기준을 명확히 해야 합니다. All이 아니라 Initial을 기준으로 봤기 때문에 이번 개선의 의미를 정확히 판단할 수 있었습니다.
앞으로 신규 기능을 추가할 때도 “이 기능은 초기 렌더에 반드시 필요한가?”라는 질문을 먼저 던질 필요가 있습니다. 만약 그렇지 않다면, import 시점 또한 함께 설계해야 합니다.
이번 작업은 거창한 알고리즘 개선이 아니라, 구조를 다시 보는 작업이었습니다. 하지만 그 결과는 초기 진입 JavaScript를 절반 이하로 줄이는 명확한 개선으로 이어졌습니다.
앞으로도 성능 문제는 최적화 이전에 구조에서 시작한다는 관점으로 접근하려고 합니다.