번들링 최적화를 통한 import cost 줄이기

InseoYangInseoYang
5 min read

JavaScript 애플리케이션을 개발할 때 번들 크기는 성능에 직접적인 영향을 미치는 중요한 요소이다. 특히 웹 애플리케이션에서는 초기 로딩 시간과 직결되어 사용자 경험을 좌우한다고 생각한다. 이번 글에서는 번들링 최적화를 통해 import cost를 줄이는 방법과 흔히 사용되는 배럴 파일의 문제점에 대해 알아보고자 한다.

배럴 파일의 함정

배럴 파일(Barrel Files)은 여러 모듈의 내보내기를 하나의 파일로 통합하여 가져오기를 단순화하는 패턴으로, 일반적으로 index.js 또는 index.ts 파일을 통해 구현된다.

예를 들어, 다음과 같은 구조의 컴포넌트가 있다고 가정해 보면:

/components
└── /Modal
    ├── Modal.js
    ├── ModalHeader.js
    ├── ModalContent.js
    └── ModalFooter.js

배럴 파일은 ./components/Modal/index.js에 다음과 같이 정의된다:

export { Modal } from "./Modal";
export { ModalHeader } from "./ModalHeader";
export { ModalContent } from "./ModalContent";
export { ModalFooter } from "./ModalFooter";

이렇게 하면 다음과 같이 단일 import 문으로 여러 컴포넌트를 가져올 수 있다:

import { Modal, ModalHeader, ModalContent, ModalFooter } from "./Modal";

언뜻 보기에는 코드 구성을 개선하고 import를 깔끔하게 만드는 좋은 방법처럼 보이지만, 배럴 파일에는 몇 가지 심각한 단점이 있다.

1. 번들 크기 증가

트리 쉐이킹(tree-shaking)이 활성화되지 않은 환경에서는 배럴 파일에서 가져온 모든 파일이 번들에 포함된다. 즉, 실제로 사용하지 않는 컴포넌트도 번들에 포함되어 불필요한 코드가 증가한다.

예를 들어, Material UI의 배럴 파일을 통해 Button 컴포넌트만 가져오는 경우와 직접 가져오는 경우의 번들 크기 차이는 상당하다고 볼 수 있다:

// 배럴 파일 사용 (번들 크기: 151.47 kB)
import { Button } from "@mui/material";

// 직접 가져오기 (번들 크기가 크게 감소)
import Button from "@mui/material/Button";

2. 빌드 시간 증가

배럴 파일은 대규모 프로젝트에서 도구 속도가 느려지는 주요 원인 중 하나다. 모든 모듈이 배럴 파일을 로드하면 각 모듈이 다른 모듈에 의존하는 복잡한 의존성 그래프가 생성된다.

배럴 파일을 사용할 때와 직접 가져올 때의 빌드 시간 차이:

  • 배럴 파일 사용: 10초

  • 직접 가져오기: 7초

3. 린트 시간 증가

배럴 파일은 린팅 성능에도 영향을 미친다. eslint-plugin-importimport/no-cycle 규칙과 같은 도구를 사용할 때, 배럴 파일의 모든 내보내기를 해결해야 하므로 린트 시간이 길어진다.

4. 개발자 경험 저하

대부분의 IDE는 자동 완성 및 IntelliSense 기능을 제공하므로 함수 이름을 입력하면 자동으로 올바른 import를 가져올 수 있다. 배럴 파일이 있으면 코드 탐색이 어려워진다. CMD + click을 사용하면 실제 모듈 정의가 아닌 배럴 파일로 이동하는 것을 확인할 수 있다.

번들링 최적화 전략

이제 import cost를 줄이기 위한 번들링 최적화 전략을 생각해보자면,

1. 패키지 타입을 모듈로 설정

package.json에서 type 필드를 module로 설정하여 ESM(ECMAScript Modules)을 기본으로 사용할 수 있다:

{
  "type": "module"
}

ESM과 CJS의 주요 차이점

문법 차이

  • ESM(ECMAScript Modules): importexport 키워드 사용

  • CJS(CommonJS): require()module.exports 사용

로딩 방식

  • ESM: 비동기적으로 모듈 로드

  • CJS: 동기적으로 모듈 로드

구조적 특성

  • ESM: 정적 구조를 가지며, 컴파일 타임에 모듈 의존성을 분석 가능

  • CJS: 동적 구조를 가지며, 런타임에 의존성을 분석

최적화 가능성

  • ESM: 정적 분석이 가능해 트리 쉐이킹과 같은 최적화 기법을 사용할 수 있음

  • CJS: 동적 로딩을 지원하지만, 정적 분석이 어려움

호환성

  • ESM: 브라우저와 Node.js 모두에서 사용 가능

  • CJS: 주로 Node.js 환경에서 사용됨

2. ESM 지원 및 진입점 분리

다양한 환경에서의 호환성을 위해 여러 진입점 설정하기:

{
  "main": "dist/index.js", // cjs
  "module": "dist/esm/index.js", // ems
  "types": "dist/index.d.ts",
  "files": [
    "dist",
    "dist/esm"
  ]
}

3. Tree Shaking 지원 강화

Tree Shaking을 최대한 활용하기 위해 sideEffects 필드를 false로 설정한다:

{
  "sideEffects": false
}

이 설정은 번들러에게 패키지의 모든 파일이 부작용이 없으며 사용되지 않는 코드를 안전하게 제거할 수 있음을 알려준다.

sideEffects 필드

sideEffects 필드는 package.json에 명시하는 속성으로, 패키지의 어떤 파일이 사이드 이펙트를 가지고 있는지 번들러에게 알려주는 역할을 한다.

사이드 이펙트란?

모듈이 import될 때 전역 스코프에 영향을 미치는 코드를 의미하며, 예를 들어 전역 객체를 수정하거나, 프로토타입을 확장하는 등의 작업이 이에 해당한다.

설정 방법

  1. Boolean 값 설정:

     {
       "sideEffects": false
     }
    

    false로 설정하면 해당 패키지의 모든 파일이 사이드 이펙트가 없다고 선언하는 것이다.

  2. 배열로 설정:

     {
       "sideEffects": [
         "*.css",
         "./src/some-side-effect.js"
       ]
     }
    

    특정 파일이나 패턴만 사이드 이펙트가 있다는 식으로도 표시할 수 있다.

장점

  • 번들 크기 감소: 사용되지 않는 코드를 안전하게 제거할 수 있어 번들 크기가 줄어듦

  • 빌드 속도 향상: 번들러가 코드를 분석하는 과정이 줄어들어 빌드 속도가 향상됨

  • 트리 쉐이킹 최적화: 번들러가 사용되지 않는 export를 효과적으로 제거할 수 있음

주의사항

  • CSS 파일이나 폴리필과 같은 사이드 이펙트가 있는 파일은 반드시 sideEffects 배열에 포함시켜야 함

  • 잘못 설정할 경우 필요한 코드가 제거될 수 있으므로 주의해야 함

4. Rollup 설정 최적화

Rollup 설정에서 treeshake 옵션을 활성화하고 preserveModules를 사용하여 모듈 구조 유지하기

import pkg from "./package.json"

export default {
  input: ['src/index.ts', 'src/colors.ts'],
  output: [
    {
      dir: pkg.files[0],
      format: 'cjs',
      preserveModules: true,
    },
    {
      dir: pkg.files[1],
      format: 'esm',
      preserveModules: true,
    }
  ],
  treeshake: true
}

5. CJS 지원을 위한 설정

Node.js 환경에서의 호환성을 위해 CommonJS(CJS) 형식도 지원해야 한다. CJS 형식의 파일은 .cjs 확장자를 사용하여 명확히 구분하면 된다.

배럴 파일 대안

배럴 파일을 사용하는 대신 다음과 같은 접근 방식을 고려해볼 수 있음

  1. 직접 가져오기: 필요한 모듈만 직접 가져오기

     import Button from "@mui/material/Button";
    
  2. 별도의 진입점 제공: 자주 사용되는 기능에 대해 별도의 진입점을 제공하기

     import { http } from 'msw/http';
    
접근 방식장점단점
직접 가져오기불필요한 코드 로드를 방지하여 번들 크기 감소import 문이 길어질 수 있음
별도의 진입점 제공자주 사용하는 기능을 쉽게 접근 가능유지보수 시 추가적인 관리 필요

확인

  • 기존(cjs로 번들링)

  • esm 만 적용했을 때(type = module)

  • treeshake = true 까지 적용했을 때

결론

번들링 최적화는 애플리케이션 성능 향상에 중요한 역할이라고 생각한다. 배럴 파일은 코드 구성을 개선하는 것처럼 보이지만, 번들 크기 증가, 빌드 시간 증가, 개발자 경험 저하 등의 문제를 일으킬 수 있다.

대신 ESM 지원, 트리 쉐이킹 최적화, 다중 진입점 제공 등의 전략을 통해 import cost를 효과적으로 줄일 수 있다. 이러한 최적화는 특히 대규모 프로젝트에서 빌드 시간과 번들 크기를 크게 개선할 수 있을 것으로 보여진다.

참고자료

  1. Why you should avoid Barrel Files in JavaScript Modules?

  2. Barrel files: A case study

  3. The Benefits and Disadvantages of Using Barrel Files in JavaScript

  4. Speeding up the JavaScript ecosystem - The barrel file debacle

  5. Please Stop Using Barrel Files | TkDodo's blog

  6. Modules imports for side effects from barrel files are not included in Webpack

  7. The Hidden Costs of Barrel Files

  8. https://rollupjs.org/configuration-options/#treeshake

  9. https://vroomfan.tistory.com/11

0
Subscribe to my newsletter

Read articles from InseoYang directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

InseoYang
InseoYang