0. 들어가며…
웹 애플리케이션을 개발하다 보면 아이콘, 폰트 등 다양한 에셋(assets)을 다루게 된다.
에셋은 정적 파일로 관리할 수도 있지만, 때로는 동적으로 로드(import) 해야 하는 경우가 있다.
예를 들어, 사용자의 행동에 따라 필요한 이미지를 지연 로딩하거나, 특정 조건에 맞춰 서로 다른 이미지를 선택적으로 로드할 때 동적 import 방식을 활용하곤 한다.
하지만 동적 import를 사용하면 정적 import보다 빌드 크기가 커지는 문제가 발생한다.
즉, 실제로 사용되지 않는 이미지까지 번들에 포함되어 최종 빌드 파일의 용량이 불필요하게 늘어나는 것이다.
이번 시간에서는 왜 이런 현상이 발생하는지, 그리고 번들 크기를 최적화할 수 있는 방법에 대해 살펴보겠다.
목차
0. 들어가며
1. 에셋 동적 로드와 빌드 최적화
1) 에셋 동적 로드시 발생하는 현상은?
(1) 가능한 모든 이미지를 번들에 포함한다.
(2) 이미지 의존성이 커진다.
2) 에셋 동적 import시 빌드 크기 줄이는 방법
(1) 라우터 동적 import
(2) 이미지 이름(패턴) 분리
(3) 폴더 구조로 구분
2. 마치며
1. 에셋 동적 로드와 빌드 최적화
이번 시간에 설명하는 내용들은 이 레포에서 테스트한 결과를 바탕으로 작성하였다.
1) 에셋 동적 로드시 발생하는 현상은?
(1) 가능한 모든 이미지를 번들에 포함한다.
- 동적 import의 번들 크기 증가 현상은 빌드 도구(예: Vite, Webpack)가 코드를 정적 분석할 때 발생한다.
- 빌드 도구는 문자열 패턴을 감지하면 "이 경로 아래의 모든 파일이 필요할 수도 있다"고 판단한다.
- 그 결과, 불필요한 파일까지 전부 번들에 포함시킨다.
- 우선 정적 import 방식을 살펴보자. 아래 예시에서는 총 3개의 이미지를 정적 import 방식으로 로드했다.
<script setup lang="ts">
import ImageFile1 from "@/assets/file-1.png";
import ImageFile2 from "@/assets/file-2.png";
import ImageFile3 from "@/assets/file-3.png";
</script>
<template>
<img :src="ImageFile1" alt="file" />
<img :src="ImageFile2" alt="file" />
<img :src="ImageFile3" alt="file" />
</template>
- 그 결과, 빌드 도구가 import된 파일명을 명확히 파악할 수 있어, 코드에서 참조된 파일만 번들에 포함했다.
- 하지만 동적 import를 사용하면 상황이 달라진다. 이번에는 파일명을 동적으로 설정해보았다.
// 동적 로드
<script setup>
const files = Array.from(
{ length: 3 },
(_, i) => new URL(`../assets/file-${i + 30}.png`, import.meta.url).href,
);
</script>
<template>
<ul>
<li v-for="file in files" :key="file">
<img :src="file" alt="file" />
</li>
</ul>
</template>
- 동적 import에서
"../assets/file-${i + 30}.png"
처럼 변수로 파일명을 설정하면,i
의 값이 런타임에서 결정되므로 - 빌드 도구는 이 파일이 정확히 무엇인지 알 수 없다.
- 결과적으로, 해당 폴더 내의 모든
.png
파일을 로드할 가능성이 있다고 판단해 사용되지 않는 이미지까지 포함된 채 번들링된다. - 빌드 결과(아래 이미지)를 보면, 불필요한 이미지들이 번들에 포함된 걸 확인할 수 있다.
왜 이런 현상이 발생하는 걸까?
- 빌드 도구는 코드를 정적 분석할 때
"../assets/file-${i}.png"
같은 문자열 패턴을 감지한다. "file-${i}.png"
가 어떤 값을 가질지 알 수 없어, 폴더 내 모든.png
파일이 필요할 수 있다고 판단한다.- 결국, 특정 폴더 아래에 있는 모든
.png
파일을 한꺼번에 번들에 포함하게 된다.
(2) 이미지 의존성이 커진다.
- 동적 import를 사용할 때 또 하나의 문제는 이미지 의존성이 커진다는 점이다.
- Rollup Visualizer 도구로 번들 구조를 살펴보면, assets 폴더 내의 모든 이미지가 단일 메인 청크(index.js 등)에 의존한다.
- 특히, Base64 인코딩 방식으로 처리될 경우 이미지 데이터가 번들 파일 내부에 포함되어 청크 크기가 증가할 수 있다.
2) 에셋 동적 import시 빌드 크기 줄이는 방법
동적 import를 사용하면서도 필요한 에셋만 번들에 포함시키려면 코드 구조, 파일 네이밍, 폴더 구성 등 관리가 필요하다.
아래 세 가지 방법으로, 번들 크기를 줄여보자!
(1) 방법1: 동적 라우터 방식을 사용해, 페이지 단위로 이미지 번들링하기
- Vue, React 등의 프레임워크는 라우터를 구성할 때 정적, 동적 import 2가지 방법이 있다.
- 이 중 정적 import 방식을 사용하면 모든 페이지의 코드와 해당 페이지에서 사용하는 이미지가 묶여서 메인 청크(index.js 등)에 포함된다.
- 아래와 같이 정적 import 방식으로 라우터를 구성하면…
import Home from "../views/HomePage.vue";
import About from "../views/AboutPage.vue";
const routes = [
{ path: "/", redirect: "/Home" },
{ path: "/Home", component: Home },
{ path: "/About", component: About },
];
- 빌드 파일이 페이지별이 아니라, 하나의 메인 파일만 생긴다.
- Rollup Visualizer로 번들 구조를 보면, index.js에 의존성이 몰려있는 걸 알 수 있다.
하지만, 동적 import 형식으로 라우터를 구성하면 파일 구조가 달라진다.
- 이번에는 동적 import 방식으로 라우터를 구성해보자.
const routes = [
{ path: "/", redirect: "/Home" },
{ path: "/Home", component: () => import("../views/HomePage.vue") },
{ path: "/About", component: () => import("../views/AboutPage.vue") },
];
- 그러면, 정적 import 방식과 달리 라우터별로 번들 파일이 생성된다.(HomePage, AboutPage, index)
- 페이지별로 코드를 분할하면, 해당 페이지에서 사용하는 이미지들도 그 청크에 국한되어 포함된다.
- 이 경우 불필요한 이미지가 전체 번들에 포함되는 문제를 줄일 수 있고,
- 필요한 리소스만 로드되므로 초기 페이지 로딩 속도를 개선할 수 있다.
- 하지만, 라우트 분할이 완벽한 해결책이 아니다.
- 여러 페이지에서 동일한 방식으로 동적 import를 사용하면, 빌드 도구가 이를 감지하고 모든 이미지를 하나의 청크로 묶어버릴 수 있다.
- 즉, 특정 페이지에서만 필요한 이미지도, 다른 페이지와 함께 묶여서 로드될 수 있다.
(2) 방법2: 이미지 이름(패턴) 분리
- 동적 import 시 파일 경로를 변수(예:
"${i}"
등)로 구성하면, 빌드 도구는 런타임에 어떤 값이 들어올지 알 수 없어 경로 내의 모든 파일을 번들에 포함시킨다. - 이를 방지하기 위해, 각 페이지에서 사용하는 이미지의 파일 이름 패턴을 구분하는 방법이 있다.
- 예를 들어, Home 페이지용 이미지는
home-file-XXX.png
, About 페이지용 이미지는about-file-XXX.png
처럼 명확하게 이름을 지정하는 것이다.
assets
├─ about-file-1.png
├─ about-file-2.png
├─ about-file-3.png
│
├─ home-file-1.png
├─ home-file-2.png
└─ home-file-2.png
- 그러면 AboutPage 페이지에는, 패턴에 해당하는 파일만 포함되어 번들링된다.
- 단, 이미지를 로드하는 기능을 별도의 외부 함수로 분리할 경우, 함수 내부에 파일 패턴을 명확히 지정해야 한다.
export function getETCFolderImage(name: string) {
/**
* 동적 import시, 빌드 시스템에서 청크 파일을 만들 때,
* ${name}값에 무엇이 들어갈지 알 수 없어 "/assets/etc/" 내부에 있는 모든 파일을 가져옴
* */
const imageUrl = new URL(`../assets/etc/${name}.png`, import.meta.url).href;
return imageUrl;
}
export function getETCFileImage(name: string) {
/**
* 동적 import시, 빌드 시스템에서 청크 파일을 만들 때,
* 특정 패턴에 해당하는 파일만 가져오게 하려면 "폴더 경로 + 파일의 특정 패턴"을 사용해야 함
* */
const imageUrl = new URL(`../assets/etc/file-${name}.png`, import.meta.url).href;
return imageUrl;
}
(3) 세 번째 단계: 이미지를 용도에 맞게, 폴더로 구분하기
- 이미지 파일의 수가 많아지고 여러 페이지에서 사용되다 보면, 이름 패턴 만으로 관리하기 어려워진다.
- 이럴 땐 폴더 단위로 이미지를 구분하는 방법이 유용하다.
- 폴더 이름도 파일 패턴처럼 번들링 대상에 포함되기에, 동일한 파일 이름을 사용해도 문제가 발생하지 않다.
assets
├─ home
│ ├─ file-1.png
│ ├─ file-2.png
│ └─ file-3.png
├─ about
│ ├─ file-1.png
│ ├─ file-2.png
│ └─ file-3.png
└─ etc
├─ file-1.png
└─ file-2.png
- 폴더 단위로 이미지를 구분하면 파일 네이밍에 의한 혼선 없이, 각 페이지별로 필요한 파일만 번들에 포함할 수 있다.
2. 마치며…
이번 시간에는 에셋을 동적으로 로드할 때 빌드 크기가 증가하는 문제와 이를 해결하는 방법을 살펴보았다.
웹 애플리케이션에서 동적 import는 유용하지만, 빌드 도구가 런타임의 변수 값을 알 수 없어 사용되지 않는 파일까지 포함되었다.
그래서 이미지 로드 방식에 따라 불필요한 리소스가 번들에 포함될 수 있으며, 번들 크기가 커지고 성능이 저하될 수 있다.
동적 import로 인해 발생하는 문제를 해결하기 위해 다음과 같은 방법을 적용할 수 있다.
- 라우터를 동적 import 방식으로 변경하여 페이지 단위로 청크를 분리한다.
- 이미지 파일 이름 또는 폴더 구조를 명확히 구분하여 특정 파일만 포함되도록 한다.
- Vite, Webpack 등의 빌드 도구에서
code splitting
이나import.meta.glob
등의 기능을 활용하여 불필요한 파일이 포함되지 않도록 조정한다.
정리하자면,
- 동적 import는 런타임 변수를 포함할 경우, 모든 가능성을 고려해 불필요한 파일까지 번들링할 수 있다.
- 페이지 단위로 코드 스플리팅을 적용하고, 파일 패턴을 명확히 설정하면 불필요한 이미지 포함을 방지할 수 있다.
- 빌드 도구의 설정을 조정하면, 더 세밀한 제어가 가능하다.
프로젝트에서 에셋을 동적으로 로드할 때 위 내용을 참고하면, 필요한 리소스만 로드하는 환경을 구축할 수 있을 것이다.
'개발 기술 > 사소하지만 놓치기 쉬운 개발 지식' 카테고리의 다른 글
Base64 인코딩과 Path 방식의 장단점 및 성능 비교(feat.에셋 로드 방식) (0) | 2025.02.09 |
---|---|
[JS] Array 빌트인 함수는 정말 성능이 나쁠까? (forEach, map 등) (0) | 2025.01.24 |
[JS] setTimeout: 0ms delay에도 지연이 발생하는 이유 (4) | 2025.01.14 |
[JS] 내장 프로토타입에 커스텀 함수를 등록하면 안되는 이유 (4) | 2024.12.20 |
[React] 반응형 데이터 연속 연산시 주의할 점(feat. Vue, React의 차이) (3) | 2024.12.09 |
댓글