일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- 클린코드
- frontend
- TypeScript
- 합성 컴포넌트
- 에세이
- sharedworker
- 리액트
- Webworker
- radixui
- 아키텍처
- CRA
- vite
- react
- Function Region
- JavaScript
- MFA
- 리팩토링
- virtaullist
- 회고
- CustomHook
- provider 패턴
- MicroFrontEnd
- 오블완
- Web
- 프론트엔드
- 티스토리챌린지
- 이것저것
- 웹워커
- 자바스크립트
- context.api
- Today
- Total
Lighthouse of FE beginner
Rollup manualChunks를 활용한 브라우저 캐싱 전략 본문
들어가며
Vite는 번들러로 Rollup을 사용합니다.
Rollup에는 manualChunks라는 옵션이 존재하는데, 해당 옵션을 사용하면 chunk 파일을 전략적으로 분리할 수 있습니다.
왜 chunk파일을 분리해야 할까?
manualChunks를 사용하지 않고 빌드를 할 시 프로젝트에서 사용하는 모든 라이브러리 파일이 하나의 chunk파일로 번들링 되어 떨어집니다.

이 경우 프로젝트가 커질수록 사용자가 플랫폼에 접근할 때 리소스를 다운로드 받기 위해 기다리는 시간이 기하수급적으로 늘어납니다.
이 경우 manualChunks 기능을 활용해 라이브러리 단위로 chunk 파일을 분리하기만 해도 초기 접근 당시 필요한 파일만을 불러와 로딩 시간을 단축할 수 있어서 사용자 경험에 유리합니다.

chunk 파일을 분리하는 방법은 manualChunks 외에 한 가지 더 존재합니다.
Code Splitting 기술을 활용해 chunk 파일을 분리하는 것 입니다.
React에는 lazy라는 API를 제공합니다. 해당 API를 사용하면 Lazy Loading 이라는 Code Splitting 기술을 활용할 수 있고 분리된 chunk 파일은 사용자가 해당 컴포넌트에 접근할 경우 로딩이 됩니다.
관련 내용은 다음의 글을 확인해주세요.
Vite 프로젝트 번들링 최적화 (manualChunks)
manualChunks를 활용한 브라우저 캐싱 전략
그럼 본론으로 돌아가서 manualChunks를 활용한 브라우저 캐싱 전략에 대해서 살펴보겠습니다.
브라우저 캐싱 전략은 사용하는 브라우저 마다 다르게 존재합니다.
아래 캐싱 전략은 Chrome 브라우저를 기본으로 작성된 전략임을 참고해주시기 바랍니다.
기본적으로 브라우저는 메모리 캐시와 HTTP를 활용해 캐시 된 리소스를 사용합니다.
manualChunks를 활용해 배포 환경에서 사용중인 라이브러리 chunk 파일과, 변경되지 않은 컴포넌트 chunk 파일을 캐싱하여 사용자에게 제공하고, 변경된 컴포넌트 chunk 파일만 제공할 수 있습니다.
예제를 통해 살펴보기
간단한 예제 환경을 구성하여 살펴보도록 하겠습니다.
Vite 프로젝트를 생성하고, AWS S3 버킷에 빌드된 파일을 올려 정적 웹 사이트 호스팅을 통해 배포합니다.
개발 > 빌드 > 배포 사이클에서 실제 프로덕션과 유사한 과정으로 살펴보기 위해 Github Actions를 활용해 CI/CD 파이프라인을 간단하게 구축합니다.
먼저 최초로 소스 코드를 수정하여 master 브랜치에 push 합니다.
Github Actions에서 커밋을 감지하여 CI/CD Workflow가 작동합니다. 프로젝트를 빌드 후 S3 Bucket에 빌드된 파일을 밀어넣습니다.


빌드 후 배포된 웹 사이트를 살펴봅니다.
vendor-scheduler-[hash].js chunk 파일이 디스크에 캐시된 것을 확인할 수 있습니다.
시간이 지나 캐싱이 무효화 됐는지 다른 chunk 파일은 캐싱되지 않은 상태로 응답 받았습니다.

두 번째 상황을 확인해보기 위해 소스 코드를 수정 후 master 브랜치에 커밋합니다. 마찬가지로 CI/CD Workflow가 작동합니다.


component-one 파일의 내용만 수정했기에 chunk 파일 내용에는 변화가 없습니다. 하지만 chunk 파일의 hash 값을 살펴보면 vendor chunk는 변화가 없고 index, component-one chunk 파일의 hash 값만 변화한 것을 살펴볼 수 있습니다.

배포 환경에서 살펴봅니다. 웹 사이트에 접근하니 index, component-one chunk 파일은 새로운 리소스로 응답합니다. 반대로 vendor chunk 파일은 변화가 없기에 HTTP 캐싱 전략을 사용해 응답한 것을 확인할 수 있습니다.
마지막으로 Code Splitting까지 확인해보겠습니다.
소스 코드에 component-two 파일을 생성한 후 커밋 합니다. CI/CD Workflow가 작동하고 S3 Bucket에 빌드된 결과물이 업로드 되어 배포됩니다.

배포 환경을 살펴봅니다. click open Component2 버튼을 클릭해야 component-two 컴포넌트가 랜더링됩니다.
초기에 해당 컴포넌트를 사용하지 않으니 (Lazy Loading) chunk 파일을 요청하지 않습니다.

버튼을 클릭한 후 입니다. 컴포넌트를 랜더링 하기 위해 component-two chunk 파일을 요청합니다.
이때 chunk 파일은 사용자가 배포된 후 처음 확인하는 컴포넌트임에 캐싱이 적용되지 않았음을 확인할 수 있습니다.

chunk 파일 명칭 전략
지금까지 생성한 chunk 파일은 일반적으로 파일 명 뒤 hash를 가지고 있습니다.
그 hash 값을 기반으로 다른 chunk 파일이 생성되고, 캐시가 무효화 되고 새로운 리소스를 요청하는데, 만약 hash를 부여하고 싶지 않은 경우 어떻게 해야할까요?
이때 Rollup의 chunkFileNames을 활용하는 것 입니다.
Rollup chunkFileNames
Configuration Options | Rollup
rollupjs.org
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 3001,
},
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes("node_modules")) {
const module = id?.split("node_modules/")?.pop()?.split("/")[0];
return `vendor-${module}`;
}
return null;
},
chunkFileNames: (info) => {
const isVendor = info.name?.startsWith("vendor");
const directory = "assets/[name]";
const extensionName = ".js";
const fileName = !isVendor ? "[hash]" : "";
return `${directory}${fileName}${extensionName}`;
},
},
},
},
});
위 vite.config.ts 파일의 예시를 살펴보면, vendor 파일의 경우 [hash]를 사용하지 않고 파일 명을 만들도록 설정을 수정했습니다.
빌드 결과 다음과 같이 라이브러리 청크 파일 명칭에 hash가 빠진 것을 확인하실 수 있습니다.

정말 변하지 않는 라이브러리일 경우 (버전 업그레이드를 하지 않거나 자체적으로 사용하는 라이브러리) 해당 전략을 사용해 특정 라이브러리 캐시 전략을 사용할 수 있습니다.
하지만 주의해야 할 점은, 모든 라이브러리에 위와 같이 hash 값을 제외한 chunk를 만들 경우, 라이브러리 버전 업그레이드에 따른 캐시 무효화를 사용할 수 없다는 점 입니다.
이 경우 사용자에게 캐싱된 chunk 파일이 프로젝트에서 사용하지 않는 버전이라면, 사용자는 의도하지 않은 버그를 마주칠 수 있습니다.
예를 들어봅시다. 프로젝트에서 react 18 버전을 사용 중에 있습니다. 위와 같이 라이브러리 chunk 파일에 hash를 사용하지 않을 경우 사용자는 vendor-react.js 라는 chunk파일을 내려받아 사용합니다.
프로젝트를 진행함에 따라 react 버전을 19 버전으로 올렸습니다. 프로젝트에서 사용하는 react의 버전은 올라갔지만 chunk 파일은 hash 값이 존재하지 않아 여전히 vendor-react.js 입니다.
사용자는 이미 웹 사이트에 접근한 적이 있음으로 vendor-react.js 리소스를 브라우저 디스크에 캐싱중에 있습니다. 혹은 웹 서버에서 파일의 변경점이 없다고 판단해 304 Not Modified를 응답합니다.
결국 사용자가 내려받은 컴포넌트 chunk 파일은 react 19를 요구하지만 현재 사용자가 사용하는 vendor-react.js chunk 파일은 react 18 버전 입니다. 만약 컴포넌트에서 react 19 API (eg. use hook)을 사용할 경우 웹 사이트에서 예외 상황이 발생해 사용자는 웹 사이트를 이용할 수 없게 됩니다.
hash 값은 어디서 오는 것일까?
Rollup 공식 문서에 의하면 hash 값은 최종적으로 생성된 chunk 파일을 기반으로 만들어집니다.
[hash]: A hash based only on the content of the final generated chunk, including transformations in renderChunk and any referenced file hashes. You can also set a specific hash length via e.g. [hash:10]. By default, it will create a base-64 hash. If you need a reduced character sets, see output.hashCharacters
마치며
manualChunks를 활용해 chunk 파일을 분리하면 다양한 이점을 취할 수 있습니다.
- 번들링 최적화
- 초기 로딩 감소로 인해 사용자 경험 상승
- 캐싱 전략으로 인해 사용자 경험 상승
현재 Beta 테스트 중인 Rolldown 프로젝트에서는 advancedChunks라는 옵션으로 해당 기능을 제공한다고 합니다. 관련해서는 아래 내용을 참고해주세요.
Rolldown advancedChunks
Rolldown | Rust bundler for JavaScript
RolldownFast Rust-based bundler for JavaScript with Rollup-compatible API
rolldown.rs
이 글을 통해 프로젝트의 고유한 캐시 전략을 활용해 사용자에게 더욱 좋은 경험을 제공할 수 있길 바랍니다.
감사합니다.
'[WEB] 프론트엔드' 카테고리의 다른 글
[MFA] Micro App의 라우팅 (3) | 2025.08.19 |
---|---|
Vercel 배포 시 Function Region (6) | 2025.08.08 |
React, Memoization (1) | 2025.06.18 |
[React] 제어 컴포넌트, 비제어 컴포넌트, 그리고 폼 다루기 (1) | 2025.06.01 |
Vite의 사전 번들링, HMR (1) | 2025.05.23 |