초록
Core Web Vitals가 검색 순위와 사용자 경험에 미치는 영향을 아는 것과, 지금 이 레포의 초기 JavaScript 페이로드에서 무엇을 먼저 잘라야 하는지를 아는 것은 서로 다른 능력입니다. 전자는 개론이고, 후자는 진단입니다. 감각에 의존해 import 문을 옮기다 보면, 서버 전용 코드까지 불필요하게 클라이언트 경계로 쪼개져 유지 비용만 늘어나는 경우가 흔합니다. @next/bundle-analyzer(또는 동등한 프로덕션 번들 시각화 도구)는 최적화 그 자체가 아니라 우선순위를 결정하기 위한 지도입니다. 이 글은 30분 안에 “범인 후보 세 개”를 확정하는 절차, 그 절차가 반복적으로 드러내는 세 가지 패턴, 그리고 번들 축소가 실제로 어떤 지표(성능, 비용)와 연결되고 어떤 지표와는 약하게만 연결되는지를 분석합니다.
서론
Next.js App Router 기반 프로젝트가 성숙해지면서, 초기 로드 시 클라이언트가 다운로드해야 하는 JavaScript의 양은 자연스럽게 늘어나는 경향이 있습니다. 새로운 기능이 추가될 때마다 새로운 의존성이 따라오고, 개별 개발자는 “이 라이브러리 하나 추가”가 전체 번들에 미치는 영향을 직관적으로 가늠하기 어렵습니다. 이 문제는 어느 한 커밋의 실수가 아니라, 여러 시점에 걸친 작은 결정들이 누적된 결과이기 때문에, 사후에 원인을 추적하려면 측정 도구가 필요합니다. 각 커밋이 추가하는 KB는 개별적으로는 무시할 만한 수준이지만, 수십 개의 커밋이 누적되면 그 합이 사용자가 체감할 수 있는 지연으로 전환됩니다.
이 글의 목적은 “번들을 줄여야 한다”는 당위를 반복하는 것이 아니라, 한정된 시간(30분) 안에서 무엇을 측정하고, 그 측정 결과를 어떤 기준으로 분류하고, 어떤 순서로 손을 대야 하는지를 구체적인 절차로 제시하는 것입니다. 이 절차는 일종의 미니 실험 설계입니다—측정하고, 가설을 세우고, 한 가지 변경을 적용하고, 다시 측정합니다. 성능 작업이 감으로 흘러가는 것을 막는 가장 확실한 방법은, 이 절차를 반복 가능한 루틴으로 고정하는 것입니다. 루틴으로 고정된 절차는 개인의 숙련도에 의존하지 않고 팀 전체가 같은 방식으로 진단할 수 있게 하며, 이는 담당자가 바뀌어도 진단의 품질이 유지되는 이점으로 이어집니다.
또한 이 글은 번들 축소를 다른 두 축과 연결합니다. 하나는 이미 이 블로그에서 다룬 Core Web Vitals 최적화라는 지표 축이고, 다른 하나는 사이드 프로젝트 FinOps라는 비용 축입니다. 번들 축소가 이 두 축 모두에 항상 긍정적으로 작용한다는 가정은 성립하지 않으며, 이 글의 논의 섹션에서 그 이유를 다룹니다. 이 두 축을 미리 언급하는 이유는, 번들 최적화의 성과를 보고할 때 그 성과가 정확히 어느 축에 기여했는지를 명확히 구분하지 않으면 팀 내부에서 서로 다른 기대를 갖게 되기 때문입니다. 성과 보고서에 “번들을 20% 줄였다”는 문장만 남기면, 그 20%가 실제로 사용자 체감 속도에 얼마나 기여했는지, 혹은 비용 절감에 얼마나 기여했는지는 여전히 불명확한 채로 남습니다.
배경
개발 모드 번들을 신뢰할 수 없는 이유
개발 서버가 제공하는 모듈 그래프는 Hot Module Replacement(HMR)를 위한 추가 코드, 소스맵, 압축되지 않은 모듈 경로가 섞여 있어, 실제 사용자가 프로덕션 환경에서 받는 바이트 수와 근본적으로 다릅니다. 개발 모드에서 “이 페이지가 3MB나 된다”는 관찰은, 프로덕션 빌드에서의 실제 페이로드와 무관할 수 있습니다. 따라서 번들 분석은 반드시 프로덕션 빌드 산출물을 대상으로 해야 합니다. 이는 사소해 보이지만 실무에서 가장 자주 생략되는 전제 조건입니다.
서버 컴포넌트와 클라이언트 청크의 구분
App Router 아키텍처에서는 서버 컴포넌트 번들과 클라이언트로 전송되는 JavaScript 청크를 혼동하기 쉽습니다. 서버 컴포넌트는 서버에서만 실행되고 결과(HTML)만 클라이언트로 전송되므로, 서버 컴포넌트가 얼마나 많은 의존성을 가져오든 원칙적으로는 클라이언트 다운로드 크기에 영향을 주지 않습니다. 반면 'use client' 지시어가 붙은 모듈과 그 하위 의존성은 클라이언트 청크에 포함되어 실제로 브라우저가 다운로드·파싱·실행해야 합니다.
TTI(Time to Interactive)와 상호작용 반응성에 직접적으로 영향을 주는 것은 대개 브라우저가 실행해야 하는 클라이언트 JS입니다. 서버 번들이 커도 사용자 체감 성능과 1:1로 대응하지 않을 수 있다는 점을 이해하지 못하면, 분석 도구가 보여주는 큰 노드 중 실제로는 무해한 서버 전용 코드를 잘못된 우선순위로 다루게 됩니다.
이 구분이 실무에서 자주 혼동되는 이유는, 하나의 컴포넌트 파일 안에 서버와 클라이언트 로직이 섞여 있을 때 개발자가 그 경계를 육안으로 파악하기 어렵기 때문입니다. 'use client' 지시어가 파일 최상단에 있으면 그 파일과 그 파일이 import하는 모든 하위 모듈이 클라이언트 번들에 포함되는데, 만약 그 파일이 실수로 무거운 서버 전용 유틸(예: 파일 시스템 접근 함수를 포함한 모듈)을 함께 import하고 있다면, 그 유틸의 의존성까지 클라이언트로 새어 나갈 수 있습니다. 번들 분석기의 트리맵에서 예상치 못한 Node.js 전용 모듈(예: fs, path의 폴리필)이 클라이언트 청크에 나타난다면, 이는 거의 항상 이런 경계 위반의 신호입니다.
번들 분석 도구의 역할과 한계
@next/bundle-analyzer는 웹팩(또는 Next.js가 사용하는 번들러)의 통계 출력을 트리맵으로 시각화하며, 이 시각화 자체는 몇 줄의 설정만으로 기존 빌드 파이프라인에 추가할 수 있습니다. 이 시각화는 “어떤 모듈이 어느 청크에 얼마나 큰 면적을 차지하는가”를 보여주지만, 그 모듈이 실제로 첫 화면에 필요한지, 상호작용 이후에나 필요한지, 아니면 전혀 쓰이지 않는지는 판단해 주지 않습니다. 이 판단은 도구가 아니라 사람이 해야 하는 부분이며, 이 글이 제시하는 30분 절차의 핵심은 바로 이 판단을 체계적으로 수행하는 방법입니다. 도구가 제공하는 것은 후보 목록이고, 그 후보에 대한 최종 판단과 대응 전략의 선택은 여전히 사람의 몫으로 남습니다.
트리맵을 읽을 때 흔히 빠지는 착시
트리맵 시각화는 면적으로 크기를 표현하기 때문에, 시각적으로 눈에 띄는 큰 사각형에 시선이 먼저 가는 것이 자연스럽습니다. 그런데 이 직관이 항상 올바른 우선순위로 이어지지는 않습니다. 첫째, 트리맵의 면적은 압축 전(raw) 크기를 기준으로 할 때가 많은데, 실제 사용자가 다운로드하는 바이트는 gzip 또는 brotli 압축 이후의 크기입니다. 텍스트 반복이 많은 모듈(예: 아이콘 데이터, 지역화 문자열)은 압축률이 높아, raw 크기로는 커 보여도 압축 후에는 상대적으로 작은 비중을 차지하는 경우가 있습니다. 둘째, 트리맵은 정적인 크기만 보여줄 뿐 실행 비용(파싱, 컴파일, 실행 시간)을 반영하지 않습니다. 크기가 작아도 초기화 시점에 무거운 연산을 수행하는 모듈은 트리맵에서 작게 보이더라도 TTI에 큰 영향을 줄 수 있습니다. 이 두 착시를 피하려면, 트리맵의 면적을 1차 스크리닝 도구로만 쓰고, 최종 판단은 gzip 크기와 실제 실행 프로파일(브라우저 개발자 도구의 퍼포먼스 탭)을 함께 참고해서 내려야 합니다. 이 이중 확인 절차는 30분이라는 시간 제약 안에서도 상위 세 후보에 한정해서만 수행하면 되므로, 전체 트리맵을 다시 훑는 것보다 훨씬 적은 시간이 듭니다.
분석틀 — 면적, 위치, 필요 시점의 3차원 분류
이 글은 번들 분석 결과를 세 가지 축으로 분류합니다.
- 면적(Size): 트리맵에서 해당 모듈이 차지하는 절대적인 KB(또는 gzip 이후 KB).
- 위치(Location): 진입 청크(entry chunk, 첫 로드 시 항상 다운로드됨)인지, 공유 청크(shared chunk)인지, 특정 라우트에만 귀속된 청크인지.
- 필요 시점(Necessity Timing): 첫 화면 렌더링에 필수인지, 사용자 상호작용 이후에 필요한지, 사실상 사용되지 않는지.
세 축의 교차점이 우선순위를 결정합니다. 면적이 크고, 진입 청크에 위치하며, 필요 시점이 “상호작용 이후” 또는 “미사용”인 모듈이 가장 먼저 손을 대야 할 대상입니다. 반대로 면적이 작거나, 이미 지연 로드되고 있거나, 첫 화면에 실제로 필수인 모듈은 우선순위가 낮습니다. 이 3차원 분류는 뒤에 나오는 30분 절차의 3단계에서 실제로 사용하는 라벨링 기준이 됩니다.
이 세 축을 표로 교차시키면, 총 열두 개의 조합이 나오지만 실무적으로 의미 있는 판단은 다음 네 가지 대표 조합으로 압축됩니다.
| 면적 | 위치 | 필요 시점 | 우선순위 | 전형적인 대응 |
|---|---|---|---|---|
| 큼 | 진입 청크 | 상호작용 이후 | 최고 | 동적 import로 전환 |
| 큼 | 진입 청크 | 첫 화면 필수 | 높음 | 직접 import, 경량 대체재 검토 |
| 큼 | 라우트별 청크 | 상호작용 이후 | 중간 | 이미 분리되어 있다면 낮은 우선순위로 재확인만 |
| 작음 | 어디든 | 어떤 시점이든 | 낮음 | 무시하거나 유틸 중복만 확인 |
이 표가 강조하는 것은, “면적이 크다”는 사실 하나만으로 우선순위를 정하면 안 된다는 점입니다. 면적이 크더라도 위치가 라우트별 청크로 이미 잘 분리되어 있다면, 그 모듈은 해당 라우트를 방문하는 사용자에게만 영향을 주므로 진입 청크의 문제보다 급하지 않습니다. 반대로 면적이 작아도 그 모듈이 진입 청크에 있고 첫 화면에 필수라면, 여러 개의 작은 모듈이 누적되어 상당한 크기를 차지할 수 있으므로 유틸리티 중복 여부를 별도로 확인할 필요가 있습니다.
본론
1. 30분 절차 — 측정 → 가설 → 한 방 변경 → 재측정
1단계: 프로덕션 빌드에서 analyzer를 실행한다. 개발 모드가 아니라 실제 배포에 쓰이는 빌드 설정으로 한 번 빌드하고, 그 결과의 통계 파일을 시각화합니다. 이 단계에서 시간을 아끼려는 유혹이 있지만, 잘못된 전제(개발 모드 데이터) 위에서 내리는 판단은 이후 단계 전체를 무효화합니다.
2단계: 클라이언트 진입 청크와 공유 청크에서 면적이 큰 노드를 세 개만 적는다. 트리맵 전체를 분석하려 하지 않고, 상위 세 개로 범위를 제한하는 것이 핵심입니다. 경험적으로 자주 등장하는 후보는 아이콘 라이브러리의 배럴 import, 차트 라이브러리, 리치 텍스트 에디터, 애니메이션 라이브러리, 그리고 여러 곳에서 중복으로 로드되는 날짜/유틸 함수 모음입니다.
3단계: 각 후보에 필요 시점 라벨을 붙인다. 앞서 정의한 분석틀에 따라 “첫 화면에 필수”, “상호작용 이후 필요”, “사실상 미사용” 중 하나로 분류합니다. 이 분류는 코드를 읽거나, 실제 페이지를 열어 해당 컴포넌트가 언제 렌더링되는지 확인함으로써 이루어집니다.
4단계: 라벨에 따라 대응 전략을 매칭한다. 첫 화면 필수로 분류되었다면 트리셰이킹 개선, 직접 import로 전환, 또는 더 가벼운 대체 라이브러리를 검토합니다. 상호작용 이후 필요로 분류되었다면 next/dynamic을 통한 지연 로드나 조건부 import를 적용합니다. 사실상 미사용으로 분류되었다면 삭제를 검토합니다.
5단계: 한 갈래만 적용한 뒤 다시 빌드해 KB(또는 gzip 이후 KB) 차이를 기록한다. 이 마지막 단계가 절차 전체를 실험으로 만드는 지점입니다. 한 번에 다섯 곳을 동시에 바꾸면, 어떤 변경이 실제로 효과가 있었는지, 혹은 어떤 변경이 오히려 역효과를 냈는지 인과 관계를 추적할 수 없습니다.
2. 왜 “한 방씩”이 중요한가 — 성능 작업의 실험 설계
성능 최적화 작업이 일반적인 기능 개발과 다른 점은, 효과의 크기를 사후에 측정해야만 검증이 완성된다는 점입니다. 기능 개발은 “동작하는가”라는 이진 검증으로 충분한 경우가 많지만, 성능 개선은 “얼마나 개선되었는가”라는 양적 검증이 필요합니다. 이 양적 검증이 유효하려면, 측정 시점 사이에 하나의 변수만 바뀌어야 한다는 실험 설계의 기본 원칙이 지켜져야 합니다.
여러 최적화를 한 PR에 묶으면, 리뷰어와 작성자 모두 “이 PR로 몇 KB가 줄었다”는 총합만 알게 되고, 다섯 개의 변경 중 어느 것이 실제 기여의 대부분을 차지했는지 알 수 없습니다. 이는 다음 최적화 작업의 우선순위를 정할 때도 손해입니다—효과가 컸던 패턴을 다시 찾아 반복 적용하려 해도, 어떤 패턴이 효과가 컸는지에 대한 데이터가 없기 때문입니다. 따라서 이 절차는 속도보다 재현 가능한 지식의 축적을 우선시합니다.
3. 반복되는 세 패턴과 그 이유
30분 절차를 여러 프로젝트에 반복 적용하면, 상위 세 후보 자리에 놓이는 모듈의 유형이 상당히 수렴하는 경향이 있습니다. 이는 우연이 아니라, 각 유형이 발생하는 구조적 이유가 있기 때문입니다.
아이콘·컴포넌트 배럴 import. from 'lucide-react'나 from '@mui/material'처럼 진입점 파일이 수천 개의 재-export를 담고 있는 패키지는, 번들러의 트리셰이킹 설정과 패키지의 sideEffects 선언 방식에 따라 사용하지 않는 모듈까지 해석 비용이 발생할 수 있습니다. Next.js의 optimizePackageImports 설정이나, 아이콘 단위로 직접 경로를 지정하는 import는 이 문제에 대한 직접적인 대응입니다. “번들러가 알아서 트리셰이킹해 주겠지”라는 가정은, 패키지가 외부(external)로 취급되거나 트랜스파일 경계 밖에 있을 때 자주 배신당합니다.
무거운 위젯의 성급한 상단 배치. 리치 텍스트 에디터, 지도, 차트, 대용량 애니메이션은 제품 가치가 명확히 크지만, 랜딩 페이지의 첫 페인트 시점에는 없는 것이 오히려 나은 경우가 많습니다. dynamic(() => import(...), { ssr: false })는 이 문제를 해결하는 도구이지만 만능은 아닙니다. 언제 로드를 시작할지—사용자가 관련 버튼에 호버했을 때 미리 로드를 시작하는 프리로드 전략, 모달이 열리는 순간에야 로드하는 지연 전략—까지 함께 설계해야 실제 체감 개선으로 이어집니다. 단순히 dynamic으로 감싸기만 하면, 로드 시점이 “사용자가 클릭한 직후”가 되어 오히려 상호작용 지연을 사용자가 직접 체감하게 되는 경우도 있습니다.
유틸리티 중복. 같은 날짜 처리 라이브러리나 lodash 계열 함수가 서로 다른 청크에 각각 포함되어 있다면, 공유 청크 전략(웹팩의 splitChunks 설정 등)이 제대로 작동하지 않거나, 직접 import 방식이 일관되지 않은 것을 의심해야 합니다. 이런 중복은 “작은 헬퍼 함수 하나 추가”가 여러 팀원에 의해 여러 곳에서 독립적으로 반복된 결과일 때가 많습니다. 유틸리티를 한 곳에 모으고 공유 모듈로 승격시키는 것이 근본적인 해법입니다.
4. 대응 전략의 우선순위 — 삭제, 지연, 직접 import
세 패턴에 대한 대응은 각각 다른 도구를 씁니다.
- 직접 import: 배럴 파일을 거치지 않고 실제로 필요한 모듈만 명시적으로 가져옵니다. 번들 크기뿐 아니라 개발 서버의 콜드 스타트 시간, 빌드 시간에도 긍정적인 영향을 줍니다.
- 동적 import(지연 로드): 첫 화면에 필요 없는 모듈을 사용자 의도나 상호작용 시점에 맞춰 나중에 불러옵니다. 이 전략은 총 다운로드 바이트를 줄이는 것이 아니라, 첫 화면에 필요한 바이트만 먼저 다운로드하도록 시점을 재배치하는 것입니다.
- 삭제: 실제로 어디에서도 참조되지 않는 코드입니다. 커버리지 도구(브라우저의 Coverage 탭, 또는 정적 분석)로 확인하지 않은 채 “아마 안 쓰일 것 같다”는 추측만으로 삭제하면, 런타임 피처 플래그로만 활성화되는 경로를 실수로 제거할 위험이 있습니다.
세 전략 중 어느 것을 먼저 적용할지는 앞서 정의한 필요 시점 라벨에 직접 대응합니다. 다만 실무에서는 “첫 화면 필수”로 분류된 항목에 대한 최적화(직접 import, 대체 라이브러리 검토)가 가장 큰 KB 감소를 만들어내는 경향이 있습니다. 지연 로드는 총 바이트를 줄이는 것이 아니라 체감 속도를 개선하는 것이므로, 두 지표(총 번들 크기 vs. 첫 화면 체감 속도)를 혼동하지 않아야 합니다.
4-1. 자주 등장하는 후보와 그 대응의 예시 표
30분 절차를 반복하며 관찰된 대표적인 후보와, 각 후보에 실무적으로 적용되었던 대응을 정리하면 다음과 같습니다. 이 표는 “정답”을 제시하는 것이 아니라, 필요 시점 라벨을 어떻게 판단했는지에 대한 예시를 보여주기 위한 것입니다.
| 후보 유형 | 필요 시점 라벨 | 적용된 대응 | 비고 |
|---|---|---|---|
| 아이콘 라이브러리 배럴 import | 첫 화면 필수 | 개별 아이콘 경로로 직접 import 전환 | optimizePackageImports로도 상당 부분 대체 가능 |
| 리치 텍스트 에디터 | 상호작용 이후(글쓰기 버튼 클릭 시) | next/dynamic + 호버 시 프리로드 | ssr: false 필수, 서버에서 렌더링 시도 시 오류 발생 |
| 차트 라이브러리 | 스크롤 이후(대시보드 하단) | Intersection Observer 기반 지연 로드 | 뷰포트 진입 시점에 로드를 시작하도록 트리거 분리 |
| 날짜 처리 유틸 중복 | 첫 화면 필수 | 공유 모듈로 통합, 배럴 import 제거 | 여러 팀원이 각자 추가한 흔적 |
| 사용되지 않는 레거시 위젯 | 미사용 | 커버리지 확인 후 삭제 | 피처 플래그 경로 여부를 먼저 확인 |
이 표에서 주목할 점은, 같은 유형의 라이브러리(예: 차트)라도 그것이 페이지의 어느 위치에 있는지에 따라 필요 시점 라벨이 달라진다는 것입니다. 대시보드 상단에 있는 핵심 차트는 “첫 화면 필수”로 분류되어야 하고, 스크롤을 내려야 보이는 보조 차트는 “상호작용 이후” 또는 “스크롤 이후”로 분류될 수 있습니다. 같은 라이브러리라도 배치 위치에 따라 다른 전략이 필요하다는 것이, 이 3차원 분류가 라이브러리 이름만으로 우선순위를 정하는 방식보다 정교한 이유입니다. 이 표를 팀 내부 위키에 누적해 두면, 새로운 페이지를 설계할 때부터 “이 위젯은 어느 라벨에 속할 것인가”를 사전에 고려하는 습관으로 이어질 수 있습니다.
5. 대체 라이브러리 검토의 판단 기준
“더 가벼운 라이브러리로 바꾸자”는 제안은 매력적으로 들리지만, 실행 비용이 낮지 않은 결정입니다. 라이브러리 교체는 API 표면이 달라지는 경우가 많아, 호출부 코드를 전부 수정해야 하고, 테스트 스위트가 새 라이브러리의 동작을 다시 검증해야 합니다. 이 비용을 감수할 가치가 있는지 판단하려면 다음 세 가지를 함께 확인해야 합니다.
- 실제 사용 API의 비율: 현재 라이브러리가 제공하는 기능 중 실제로 사용하는 비율이 낮다면(예: 날짜 라이브러리의 국제화 기능 전체 중 포맷팅 함수 두 개만 사용), 더 작은 대체 라이브러리나 네이티브 API로 전환할 여지가 큽니다.
- 번들 크기 차이의 절대값: 상대적으로 “70% 더 작다”는 홍보 문구보다, 실제 KB 차이가 몇 KB인지가 중요합니다. 원본이 이미 5KB였다면 70% 감소도 3.5KB 절감에 불과해 투자 대비 효과가 낮습니다.
- 유지보수 생태계: 대체 라이브러리가 작은 크기를 위해 타입 정의, 문서, 커뮤니티 지원을 희생한 경우, 장기적인 유지보수 비용이 초기 번들 절감보다 커질 수 있습니다.
이 세 기준을 모두 통과하는 경우는 생각보다 많지 않습니다. 실무에서는 라이브러리 교체보다 “같은 라이브러리를 더 가볍게 쓰는 방법”(배럴 import 제거, 서브 모듈만 import)이 투자 대비 효과가 더 큰 경우가 흔합니다. 교체를 결정하기 전에, 먼저 지금 쓰고 있는 라이브러리를 더 가볍게 쓰는 방법을 모두 시도해 보고, 그래도 남는 차이가 클 때만 교체를 검토하는 순서가 낭비를 줄입니다.
6. 30분 절차를 반복 적용했을 때의 수확 감소
30분 절차를 처음 적용하면 대개 눈에 띄는 KB 감소를 얻습니다. 그러나 같은 절차를 반복 적용할수록 새로 발견되는 후보의 크기는 점점 작아지는 경향이 있습니다. 이는 수확 감소(diminishing returns)의 전형적인 패턴이며, 이 패턴을 이해하지 못하면 “이번에는 효과가 별로 없었다”는 이유로 절차 자체의 가치를 과소평가하게 됩니다.
수확 감소 곡선을 이해하고 나면, 절차를 언제 멈춰야 하는지에 대한 판단 기준도 세울 수 있습니다. 한 번의 30분 세션에서 발견된 상위 세 후보의 절감 예상치를 합산했을 때, 그 값이 전체 초기 로드 페이로드의 1~2% 미만이라면, 추가적인 번들 최적화보다 다른 성능 레버(이미지 최적화, 서버 응답 시간 개선)로 관심을 옮기는 것이 투자 대비 효과가 높습니다. 반대로 상위 후보의 절감 예상치가 여전히 두 자릿수 퍼센트에 달한다면, 절차를 몇 차례 더 반복할 가치가 있습니다.
7. 팀 차원의 반복 — 온보딩과 회고
한 사람이 30분 절차를 숙달했다고 해서 팀 전체가 같은 감각을 갖는 것은 아닙니다. 새로 합류한 개발자가 이 절차를 처음 실행할 때는, 트리맵에서 어떤 노드가 “정상적인 크기”이고 어떤 노드가 “의심스러운 크기”인지에 대한 감각이 부족합니다. 이 감각의 차이를 줄이는 방법은, 과거에 발견되었던 상위 후보와 그 대응 결과를 짧은 회고 문서로 남겨 두는 것입니다. 이 문서는 길게 쓸 필요가 없으며, 후보 이름과 절감된 KB, 적용된 대응 방식 세 가지만 표로 남겨도 충분합니다. “이 라이브러리는 예전에 몇 KB였고, 이런 조치로 몇 KB로 줄었다”는 기록이 누적되면, 새 팀원은 트리맵을 보는 순간 “이 크기는 과거 기록과 비교했을 때 비정상적으로 크다”는 판단을 더 빠르게 내릴 수 있습니다. 이 회고 문서는 성능 최적화팀만의 자산이 아니라, 새로운 의존성을 추가하려는 모든 개발자가 참고할 수 있는 팀 전체의 참조 자료로 기능합니다.
8. 하지 말아야 할 최적화
30분 절차가 반복되면서 얻는 또 하나의 교훈은, 하지 않아야 할 최적화의 목록이 최적화 목록만큼 중요하다는 것입니다.
- 서버에서만 실행되는 코드를 억지로 클라이언트 경계로 끌어와 쪼개는 것. 이는 서버 컴포넌트와 클라이언트 청크의 구분을 무시한 결과이며, 오히려 클라이언트 번들을 늘립니다.
- LCP 이미지, 웹 폰트, TTFB(Time to First Byte) 문제를 JavaScript 번들 그래프만으로 해결하려는 것. 이 지표들은 각각 이미지 최적화, 폰트 로딩 전략, 서버 응답 시간이라는 별도의 축에 속하며, 번들 분석 도구가 보여주는 그래프와 직접적인 인과 관계가 없습니다.
- 커버리지 도구 없이 “아마 안 쓰이는 것 같다”는 추측으로 코드를 삭제하는 것. 런타임에 조건부로만 실행되는 피처 플래그 경로가 삭제되면, 프로덕션에서만 발견되는 회귀가 발생할 수 있습니다.
- 미시 최적화에 매몰되어 PR만 비대해지고, 실제 제품 가설 검증이 미뤄지는 것. 번들 축소는 목적이 아니라 수단이며, 그 자체가 로드맵의 최우선 과제가 되어서는 안 됩니다.
논의
번들 축소와 Core Web Vitals의 관계
Core Web Vitals 최적화 논의에서 다루는 지표 중, 번들 축소가 가장 직접적으로 영향을 주는 것은 INP(Interaction to Next Paint)와 초기 로드 시의 파싱·실행 비용입니다. 반면 LCP는 이미지 최적화나 서버 응답 시간과 더 밀접하고, CLS(Cumulative Layout Shift)는 레이아웃 안정성과 관련되어 번들 크기와는 거의 무관합니다. 이 구분을 하지 않으면, “번들을 줄였는데 왜 LCP가 그대로인가”라는 잘못된 기대가 생기고, 결과적으로 번들 최적화 작업의 가치를 과소평가하거나 과대평가하게 됩니다. 번들 축소는 Core Web Vitals 전체를 개선하는 만능 열쇠가 아니라, 특정 지표에 대한 특정 레버로 이해해야 정확합니다.
번들 축소와 클라우드 비용의 관계
사이드 프로젝트 FinOps의 관점에서 보면, 번들 축소는 클라우드 비용 청구서와 약한 상관만 가질 때가 많습니다. 클라이언트 JS 크기는 사용자의 대역폭 소비에는 영향을 주지만, 서버 측 함수 실행 시간이나 데이터베이스 I/O, 로그 저장 비용과는 직접적인 관계가 없습니다. 만약 청구서의 대부분이 데이터베이스 쿼리, 과도한 로그 보관, 또는 봇 트래픽에서 발생한다면, 번들을 아무리 줄여도 그 청구 라인은 줄어들지 않습니다. 이는 번들 최적화가 무의미하다는 뜻이 아니라, 어느 청구 라인과 연결되는 최적화인지를 먼저 확인해야 한다는 뜻입니다. 성능 지표 개선과 비용 절감을 같은 작업으로 묶어서 보고하면, 두 축 중 실제로 개선되지 않은 축에 대한 기대가 왜곡됩니다.
번들 크기와 개발자 경험의 관계
번들 분석 절차를 논의할 때 자주 간과되는 축은, 클라이언트 번들 크기가 최종 사용자 경험뿐 아니라 개발 서버의 콜드 스타트와 HMR 속도에도 영향을 준다는 점입니다. 배럴 import를 직접 import로 바꾸는 조치가 프로덕션 번들 크기를 줄이는 것과 별개로, 개발 서버가 매번 다시 해석해야 하는 모듈 그래프의 크기도 함께 줄여 줍니다. 이는 팀의 일상적인 개발 루프—코드를 고치고 저장한 뒤 브라우저에서 결과를 확인하는 주기—를 단축시키는 효과로 이어지며, 이 효과는 사용자에게는 보이지 않지만 팀의 생산성에는 직접적으로 반영됩니다. 즉 번들 최적화의 투자 대비 효과를 평가할 때는 최종 사용자 지표만이 아니라, 이 개발자 경험 축도 함께 고려하는 것이 완전한 평가에 가깝습니다.
프레임워크 마이그레이션과 절차의 재적용 시점
번들러나 프레임워크 메이저 버전을 마이그레이션한 직후는, 이 30분 절차를 다시 실행해야 하는 특별한 시점입니다. 마이그레이션 과정에서 트리셰이킹 동작이나 코드 분할 전략의 기본값이 바뀌는 경우가 흔하고, 이전에 이미 최적화되어 있던 모듈이 새 환경에서는 다시 큰 노드로 나타나거나, 반대로 이전에 문제였던 모듈이 새 기본 설정으로 인해 자동으로 해결되는 경우도 있습니다. 마이그레이션 PR에 “번들 분석 재실행 결과”를 포함시키는 습관을 두면, 마이그레이션으로 인한 회귀를 배포 전에 발견할 수 있습니다. 이 습관이 없으면, 마이그레이션 자체는 성공적으로 끝났지만 몇 주 뒤 사용자 경험 지표가 나빠진 이유를 뒤늦게 추적해야 하는 상황에 놓일 수 있습니다. 마이그레이션 직후의 재측정은 “문제가 있을 것”이라는 가정에서 시작하는 것이 아니라, 단지 기본값이 바뀌었을 가능성을 배제하기 위한 확인 절차로 이해하는 것이 적절합니다.
지속적 모니터링으로의 확장
30분 절차는 일회성 진단으로 소개했지만, 실무에서 가장 큰 가치를 내는 방식은 이를 정기적으로 반복하는 루틴으로 전환하는 것입니다. 주요 배포 전이나 정기 스프린트 종료 시점에 동일한 절차를 반복하면, 번들 크기가 시간에 따라 어떻게 변화하는지 추세선을 얻을 수 있습니다. 일부 팀은 CI에 번들 크기 임계값을 넣어, 특정 크기를 초과하는 PR에 대해 경고를 표시하는 방식으로 이 절차를 자동화합니다. 다만 자동화된 임계값 경고가 앞서 설명한 “필요 시점 분류”까지 대신해 줄 수는 없으며, 여전히 사람이 각 후보에 라벨을 붙이는 판단 단계는 남아 있습니다.
한계
이 글에서 제시한 30분 절차는 특정 규모(개인 또는 소규모 팀이 운영하는 Next.js 프로젝트)에서 반복적으로 유효했던 경험적 절차이며, 대규모 모노레포나 마이크로 프론트엔드 아키텍처처럼 번들 경계가 여러 팀에 걸쳐 분산된 환경에서는 30분이라는 시간 제약이 충분하지 않을 수 있습니다. 또한 이 글은 웹팩 기반 분석을 전제로 설명했으나, 번들러가 Turbopack이나 다른 대안으로 전환되면 시각화 도구와 통계 출력 형식이 달라질 수 있어 구체적인 도구 사용법은 별도로 확인이 필요합니다. 마지막으로, 이 글이 제시한 세 가지 반복 패턴(아이콘 배럴, 무거운 위젯, 유틸리티 중복)은 관찰에 기반한 경험적 분류이며, 모든 프로젝트에서 동일한 순위로 나타난다고 보장할 수 없습니다. 프로젝트의 도메인(예: 데이터 시각화 중심 서비스)에 따라 다른 패턴이 우세할 수 있습니다.
또한 이 글이 제시한 “한 방씩 변경하고 재측정한다”는 원칙은 이론적으로는 명확하지만, 실무에서는 시간 압박 때문에 여러 최적화를 한 PR에 묶어 배포하고 싶은 유혹이 항상 존재합니다. 이 글은 그 유혹을 이겨내야 한다는 규범적 주장을 제시했을 뿐, 조직 차원에서 이 규범을 어떻게 강제할 것인지(예: PR 템플릿에 벤치마크 수치를 필수 항목으로 넣는 것)에 대한 구체적 메커니즘은 다루지 않았습니다. 이 메커니즘의 설계는 팀의 코드 리뷰 문화와 밀접하게 연관되어 있어, 일반화된 처방을 제시하기보다 각 팀이 자신의 리뷰 프로세스에 맞게 조정해야 하는 부분으로 남겨둡니다.
이 글이 다루지 않은 또 다른 축은, 번들 크기 최적화와 런타임 성능 최적화(예: 불필요한 재렌더링 방지, 메모이제이션) 사이의 상호작용입니다. 두 최적화 영역은 서로 독립적으로 보이지만, 실제로는 같은 컴포넌트를 다른 관점에서 건드리는 경우가 많아, 번들 최적화 작업 중 런타임 성능에 영향을 주는 부수 효과가 생기거나 그 반대의 경우가 생길 수 있습니다. 이 상호작용을 체계적으로 다루려면 별도의 분석틀이 필요하며, 이는 이 글의 범위를 넘어섭니다.
결론
@next/bundle-analyzer가 제공하는 실질적 가치는 시각적으로 정교한 트리맵이 아니라, 30분 안에 실험 가능한 가설 세 개를 남기는 절차에 있습니다. 프로덕션 클라이언트 청크를 면적·위치·필요 시점이라는 세 축으로 분류하고, 각 후보를 직접 import·지연 로드·삭제 중 하나로 대응한 뒤, 한 갈래씩 재측정하여 KB 차이를 기록하는 것이 핵심 절차입니다. 이 수치가 PR에 남아야, 팀의 성능 문화가 “감으로 판단”에서 “증거로 판단”하는 방향으로 이동합니다. 동시에, 번들 축소가 Core Web Vitals의 모든 지표나 클라우드 비용의 모든 라인과 균일하게 연결되지 않는다는 점을 인지하는 것이, 이 작업의 가치를 정확히 평가하고 다음 우선순위를 정하는 데 필요한 균형 감각입니다.
이 절차를 처음 도입하는 팀에게 권할 만한 시작점은, 크고 복잡한 최적화 계획을 세우기보다 지금 당장 프로덕션 빌드를 한 번 실행하고 트리맵을 열어보는 것입니다. 이 단순한 행동 하나가, 그동안 감으로만 짐작했던 “이 페이지가 무거운 것 같다”는 인상을 구체적인 KB 숫자와 파일명으로 전환시켜 줍니다. 그 숫자가 크지 않다면 다른 성능 레버로 넘어가도 되고, 크다면 이 글이 제시한 5단계를 따라가면 됩니다. 어느 쪽이든, 측정 없이 최적화를 시작하는 것보다 훨씬 적은 시간에 더 정확한 우선순위를 얻을 수 있습니다. 결국 이 30분은 최적화 자체의 시간이 아니라, 이후에 쓸 시간을 어디에 배분할지 결정하는 진단의 시간이며, 그 진단의 정확도가 이후 모든 최적화 작업의 투자 대비 효과를 좌우합니다.