노트

배럴 파일과 re-export

Barrel File

프런트엔드#js#tooling · 연결된 개념 8개

쉽게 말하면

배럴 파일은 여러 파일의 내보내기를 한 파일에 모아 둔 안내 데스크예요. 쓰는 쪽은 한 곳만 알면 되고, 안쪽 파일 위치가 바뀌어도 안내 데스크만 고치면 되죠.

비유가 깨지는 곳 안내 데스크를 거치면 안 쓰는 것까지 딸려 올 수 있어요. 프로덕션 번들러는 대체로 쓰는 export만 남기지만, 부수 효과 모듈이 섞이거나 개발 서버처럼 트리 셰이킹을 안 하는 곳에선 비용이 그대로 들어요.

배럴 파일(Barrel File)은 여러 모듈의 export를 index.ts 하나에 모아 다시 내보내(re-export) 진입점을 하나로 만드는 파일이다. 쓰는 쪽은 import { Button, Input } from './components'처럼 한 경로에서 가져온다.

export { Button } from './Button'              // 이름 그대로
export { Input as TextInput } from './Input'   // 이름 바꿔서
export * from './layout'                       // 전부(단, default는 빠짐)
export * as utils from './utils'               // 네임스페이스로 묶어서
export { default as Modal } from './Modal'     // default를 이름 붙여서
  • 장점: import 경로가 짧아지고, 내부 파일 위치가 바뀌어도 배럴만 고치면 된다. 공개할 API(Application Programming Interface)의 경계 역할도 한다
  • 주의: export *는 default export를 옮기지 않는다
  • 번들 크기: 배럴을 import하면 번들러가 그 아래 모듈들을 평가해야 할 수 있다. 부수 효과가 있는 모듈이 섞여 있으면 트리 셰이킹이 깨져 안 쓰는 코드까지 딸려 온다. 개발 서버에서는 불러오는 모듈 수가 늘어 느려지기도 한다
  • 순환 참조(Circular Dependency): 배럴을 거쳐 서로를 import하다 보면 순환이 생기기 쉽다. 패키지 안쪽에서는 배럴 대신 직접 경로로 import하는 편이 안전하다

비용은 어디서 실제로 생기나

"배럴이 트리 셰이킹을 깨뜨린다"는 말은 절반만 맞다. ESM의 export { X } from './x'·export *는 정적으로 해석되므로, 프로덕션 번들러는 배럴을 거쳐도 쓰인 export만 남길 수 있다. 새는 곳은 따로 있다.

  • 부수 효과 모듈이 묶일 때: 배럴 아래 어떤 컴포넌트 파일이 import './sheet.css'나 전역 등록 코드를 갖고 있으면, 그 컴포넌트를 안 써도 번들러는 그 모듈을 버려도 되는지 확신하지 못한다. JS는 빠지고 CSS만 남는 식으로 새기도 한다. package.json의 sideEffects로 순수한 파일을 알려 줄 수 있는데, 이때 CSS처럼 import만으로 효과가 있는 파일은 "sideEffects": ["*.css"]로 예외에 넣어야 운영 빌드에서 사라지지 않는다(트리 셰이킹)
  • 배럴을 동적 import할 때: import('./features/products')처럼 배럴 자체를 분할 단위로 잡으면 그 청크에 배럴이 노출한 것이 다 들어간다(코드 스플리팅과 동적 임포트)
  • 트리 셰이킹을 하지 않는 환경: ESM에서 import { A } from './barrel'은 배럴 모듈 전체와 그 정적 import를 모두 평가한다. 트리 셰이킹은 이 위에 얹는 프로덕션 번들러의 최적화일 뿐이다. 개발 서버와 테스트 러너는 빠른 재시작을 위해 이 최적화를 생략하므로, 배럴 하나가 가리키는 모듈 수만큼 변환·실행 비용이 그대로 든다. 번들링하는 개발 서버는 컴파일할 모듈이 늘고, 번들링하지 않는 개발 서버는 브라우저 요청이 그만큼 이어진다. 체감은 첫 진입과 캐시가 깨진 뒤에 몰린다
  • 서버·클라이언트 경계가 섞일 때: App Router에서 배럴이 server-only 모듈과 클라이언트 컴포넌트를 함께 다시 내보내면, 클라이언트 컴포넌트가 배럴에서 무엇을 가져오든 서버 코드가 클라이언트 모듈 그래프로 끌려와 빌드가 깨진다('use client'와 클라이언트 경계)

절충안은 얇은 배럴이다. 배럴에는 타입·상수·API 함수만 두고, 컴포넌트와 부수 효과가 있는 모듈은 직접 경로로 import한다. Feature-Sliced Design의 공개 API처럼 경계 역할을 살리면서 위 비용을 피한다.

라이브러리 배럴과 optimizePackageImports

아이콘·UI 라이브러리는 진입점 하나에서 수천 개 모듈을 다시 내보낸다. Next.js는 13.5부터 experimental.optimizePackageImports로, 배럴 import를 실제 쓰는 모듈 경로로 바꿔 필요한 것만 불러온다. lucide-react·date-fns·lodash-es·@mui/material 등은 기본으로 적용되고, 다른 패키지는 목록에 추가한다(Next.js 16.3 기준, 아직 experimental). node_modules 패키지용이라 앱 안의 배럴에는 해당하지 않는다. Vercel은 도입 당시 lucide-react가 1,583개에서 333개 모듈로 줄었다고 밝혔다.

module.exports = {
  experimental: { optimizePackageImports: ['my-icon-lib'] },
}

모듈 문법 자체는 ES 모듈과 CommonJS, 큰 패키지 경계를 나누는 설계는 결합도와 함께 생각한다.

출처: webpack - Tree Shaking: Mark the file as side-effect-free · Next.js - optimizePackageImports · Vercel - How we optimized package imports in Next.js

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 번들 크기 줄이기

    번들 다이어트는 사용자가 내려받는 JavaScript 양을 측정하고 줄이는 작업이다. 같은 용량이라도 JS는 이미지보다 비싸다. 이미지는 다운로드와 디코딩만 하면 되지만, JS는 다운로드·파싱·컴파일·실행을 모두 거친다.

  • 번들러

    번들러(Bundler)는 모듈로 나뉜 소스 코드를 의존 관계(Dependency Graph)를 따라 묶고 변환해, 브라우저가 적은 요청으로 받을 수 있는 결과물로 만드는 빌드 도구다.

  • verbatimModuleSyntax와 import type

    verbatimModuleSyntax(TypeScript 5.0+)는 import/export를 쓴 그대로 출력에 남기라고 강제하는 컴파일러 옵션이다. 대신 타입만 가져오는 import에는 type을 직접 표시해야 한다. import type으로 표시한 것만 지워진다.

  • loading 경계와 레이아웃 끌어올리기

    Next.js App Router에서 loading.tsx는 같은 세그먼트의 page와 그 아래(하위 layout 포함)를 suspense로 감싸지만, **같은 세그먼트의 layout은 감싸지 않는다**. 그래서 데이터를 기다릴 필요가 없는 공통 UI(탭바, 필터 헤더)를 page에서 layout으로 끌어올리면 스켈레톤에 덮이지 않고 처음부터 실제 모습으로 보인다.

  • 여러 저장소 합치기

    따로 관리하던 Git 저장소를 하나로 합칠 때, 히스토리를 버리지 않고 옮기는 방법이 두 가지 있다. 저장소 루트끼리 그대로 합치거나(unrelated histories 머지), 한쪽을 다른 쪽의 하위 폴더로 넣는다(subtree). 모노레포로 옮길 때 자주 쓴다.

보기 옵션