dev

배럴(Barrel) 파일의 배신

배럴(Barrel) 파일에서 export * from 사용할 때 조심하자.

배럴(Barrel) 파일이란?

여러 모듈을 하나로 묶는 index.ts 같은 걸 배럴 파일이라고 합니다.
배럴 파일은 모듈을 내보내는 역할만 하며, 코드 자체는 포함하지 않습니다.

// feature/index.js
export * from "./foo";
export * from "./bar";
export * from "./baz";

배럴(Barrel) 파일을 사용했던 이유

저는 이전에 다니던 회사에서 배럴 파일을 사용하는 패턴을 익혔고, 습관처럼 사용해왔습니다.
개인적으로 진행한 사이드 프로젝트나 과제에서도 배럴 파일을 사용했습니다.
배럴 파일 패턴에는 몇 가지 장점이 있습니다.
우선, import 구문을 간결하게 작성할 수 있습니다. 자동 import 기능이 있더라도, 코드의 길이를 줄일 수 있다는 점이 특히 매력적이었습니다. 또한, index 파일을 통해 외부로 내보낼 코드와 내부에서만 사용할 코드를 명확히 구분할 수 있다는 점도 장점이라고 생각했습니다.

배럴 파일 사용 전

import { foo } from "../foo";
import { bar } from "../bar";
import { baz } from "../baz";

배럴 파일 사용 후

import { foo, bar, baz } from "../feature"

ESModules(ESM)의 특징

배럴 파일의 문제점을 이해하기 위해서, ESModules(ESM)의 import 동작 방식을 먼저 간단히 살펴보겠습니다.

정적 구조

  • import는 최상단에서만 사용할 수 있고, 조건문에서는 사용할 수 없습니다.
  • 따라서 빌드 전에 모든 의존성 그래프를 분석할 수 있습니다.
  • 덕분에 VScode 같은 툴에서 import 자동완성을 잘 사용할 수 있습니다.
  • 여기서 중요한 점은 코드가 실행되기 전에 전체 import 그래프가 만들어진다는 점입니다.

모듈 캐싱

  • 하나의 모듈은 처음 로드될 때만 실행됩니다.
  • 이후 동일 모듈은 캐시된 결과를 사용합니다.
// a.js
console.log('loaded');

// b.js
import './a.js';
import './a.js'; // 다시 실행 안 됨

스코프 분리

ESM 파일은 자동으로 strict mode가 적용되고 전역 변수를 직접 건드릴 수 없습니다.

// a.mjs
foo = 1; // ❌ ReferenceError

브라우저, Node.js 사용

브라우저에서는 <script type="module"> 태그를 사용합니다.
Node.js에서는 .mjs 확장자를 사용하거나 package.json 파일에 "type": "module" 설정을 추가해야 합니다.

배럴(Barrel) 파일의 문제점

import { doSomething } from './utils'

doSomething()을 호출하지 않아도 import는 실행됩니다. 즉, 실제로 코드를 사용하는지와 관계없이 import 여부만으로 로드 대상이 결정됩니다.

export * from './B' // B도 로드됨
export * from './C' // C도 로드됨
export * from './D' // D도 로드됨

이러한 방식으로 배럴 파일을 사용하면, 하나의 import만 해도 불필요한 모듈까지 import 그래프에 포함될 가능성이 커집니다.
이러한 문제점은 특히 대규모 프로젝트에서 성능 저하로 이어질 수 있습니다.
따라서 배럴 파일을 무조건 사용하기보다는, 기능 단위로 모듈을 엄격하게 분리하는 것이 좋습니다.

Webpack이 알아서 해결해주지 않을까?

이러한 배럴 파일의 문제를 Webpack과 같은 번들러 도구가 트리 쉐이킹(Tree Shaking)을 통해 자동으로 해결해주지 않을까 기대할 수도 있습니다.

// utils/math.ts
export const add = (a, b) => a + b
export const sub = (a, b) => a - b

// main.ts
import { add } from './utils'

위 코드에서 sub 함수는 사용되지 않으므로, 빌드 결과에서 제거됩니다.

// utils/index.ts
export * from './math'
export * from './string'

// main.ts
import { add } from './utils'

그러나 위 코드에서는 math.tsstring.ts의 모든 내용이 번들에 포함될 가능성이 높습니다. 특히 export * 구문은 정적 분석을 어렵게 만들어, 번들러가 사용하지 않는 코드를 효과적으로 제거하지 못할 수 있습니다.

배럴 파일이 성능에 미치는 영향은 이곳에서 더 자세히 알아볼 수 있습니다.


Reference