
Next.js에서 FSD 아키텍처를 적용시키던 중 배럴 익스포트(Barrel Export)의 필요성을 느꼈다.
But, 누군가는 배럴 파일의 위험성에 대해서도 소개하고 있길래, 사용할까말까 고민해봤다.
배럴 익스포트란, 다른 파일들을 재수출(re-export)하는 파일을 만드는 패턴이다.
간단히말해, 여러 파일을 한 곳에서 모아서 내보내는 방식이며, 이때 사용하는 파일(보통 index.ts 혹은 index.js)을 배럴 파일이라고 한다.
배럴 익스포트(
index.ts)의 유무에 따른 차이점을 확인해보자.
BlogList에서 useBlogList와 BlogCard로부터 UI및 기능들을 import 해야하는 상황이다.
📂blog-list
├─📂model
│ └─useBlogList.ts
└─📂ui
└─BlogCard.tsx
📂widgets
└─BlogList.tsx
배럴 파일 없을 때는 직접 하나하나 import 해야한다.
// widgets/BlogList.tsx
import { useBlogList } from '@/features/blog-list/model/useBlogList';
import { BlogCard } from '@/features/blog-list/ui/BlogCard';
📂blog-list
├─📂model
│ └─useBlogList.ts
├─📂ui
│ └─BlogCard.tsx
│
└─index.ts (배럴파일 생성)
📂widgets
└─BlogList.tsx
배럴파일 생성 ( 배럴파일에서 미리 이것저것 export 준비)
// features/blog-list/index.ts (배럴파일) 생성
export { useBlogList } from './model/useBlogList';
export { BlogCard } from './ui/BlogCard';
사용할 때는 개별 하나하나 import할필요없이 배럴파일만 import하면된다.
// widgets/BlogList.tsx
import { BlogCard, useBlogList } from '@/features/blog-list';
// Before
import { BlogData } from '@/entities/blogs/model/types';
import { blogApi } from '@/entities/blogs/api/blogApi';
... 길어지는 import들
// After
import { BlogData, blogApi ...한줄컷 } from '@/entities/blogs';
어떤 것들이 외부에 노출되는지 명확하게 정의할 수 있음.
// entities/blogs/index.ts - 블로그 entities의 Public API
export type { BlogData, BlogListResponse } from './model/types';
export { blogApi } from './api/blogApi';
// 내부 구현 상세는 숨길수있다
내부 폴더 구조가 바뀌어도 배럴파일만 수정하면 됨. 사용하는 쪽 코드는 건드리지 않아도 됨.
배럴 익스포트가 문제가 되는 케이스들을 살펴보자.
// shared/ui/index.ts
export * from './Button';
export * from './Modal';
export * from './Table';
export * from './Chart';
export * from './Calendar';
// ... 그외 여러 컴포넌트
// 실사용하는 컴포넌트에서 Button만 필요하다면?
import { Button } from '@/shared/ui';
// 모든 컴포넌트가 번들에 포함되며 성능이슈를 발생시킨다.
배럴 파일이 많아지면 import 체인이 거미줄처럼 복잡해짐.
// feature/index.js
export * from "./foo";
export * from "./bar";
export * from "./baz";
// 사용하는 곳
import { foo, bar, baz } from "../feature";
이런 패턴이 코드베이스 전체에 퍼지면서 모든 폴더에 index.js 파일이 생김. 결국 하나의 모듈을 import하면 배럴 파일 → 또 다른 배럴 파일 → 또 다른 배럴 파일 순으로 연쇄적으로 import하게 됨.
최종적으로는 거미줄처럼 얽힌 import문을 통해 프로젝트의 모든 파일을 import하게 되는 상황 발생.
Jest 같은 테스트 러너는 각 테스트 파일을 별도 프로세스에서 실행함. 이는 모든 테스트 파일이 모듈 그래프를 처음부터 다시 구성해야 한다는 뜻임.
블로그에서 제시한 계산:
이 시간 동안에는 실제 테스트나 다른 코드가 실행되지 않음. 순전히 엔진이 소스 코드를 준비하는 시간임.
// 이런 건 문제없음
export { Button } from './Button';
export { Input } from './Input';
// 이런 게 문제 (무지성 export)
export * from './components'; // 10000개 컴포넌트
export * from './utils'; // 10000개 유틸함수
그럼 어떻게 해야 할까? 프로젝트 규모와 상황에 맞는 전략을 세워보자.
소규모 프로젝트
- 선택적 사용: 핵심 모듈들만 배럴 익스포트 적용 (성능 이슈 거의 없음)
// 현재 나의 프로젝트 수준
entities/blogs/index.ts
features/blog-list/index.ts
// 이정도만 사용하자
중간규모 프로젝트
- 신중한 적용: 정말 필요한 곳에만 사용하며, 번들크기와 빌드시간을 체크해보자
대규모 프로젝트
- 할수있다리: 나중에 꼭 경험해보자
명시적 export 사용
// ✅ 필요한 모듈만 export해서 명시하자
export type { BlogData, BlogListResponse } from './model/types';
export { blogApi } from './api/blogApi';
// ❌ 지양하자
export * from './model/types';
export * from './api/blogApi';
작은 단위로 분할해서 사용하자
// entities/blogs/model/index.ts (분할 1번)
export type { BlogData, BlogListResponse } from './types';
// entities/blogs/api/index.ts (분할 2번)
export { blogApi } from './blogApi';
// entities/blogs/index.ts (2개로분할된걸 최상위에서 export)
export * from './model';
export * from './api';
조건부 배럴 익스포트
자주 함께 사용되는 것들만 묶고, 무거운 모듈은 별도로 import하도록 함.
// shared/ui/index.ts
// 기본적인 것들만 등록해두기
export { Button } from './Button';
export { Input } from './Input';
// 무거운 컴포넌트는 별도로 import해서 사용해버리기
// import { Chart } from '@/shared/ui/Chart';
TypeScript 환경에서 FSD아키텍처를 사용하고 있으며, 배럴 파일을 적용해보려한다.
- Entities 계층 배럴파일 생성
// entities/blogs/index.ts
export type { BlogData, BlogListResponse } from './model/types';
export { blogApi } from './api/blogApi';
// features/blog-list/index.ts
export { useBlogList } from './model/useBlogList';
export { BlogCard } from './ui/BlogCard';
'use client';
import { BlogCard, useBlogList } from '@/features/blog-list';
import TempBlogSection from '@/widgets/blog-temp-section';
export default function BlogSection() {
const { blogs, error, loading, handleBlogClick, refetch } = useBlogList();
if (error) {
return <TempBlogSection />;
}
if (loading) {
<div>
<h1>Loading blogs...</h1>
<button onClick={refetch}>새로고침</button>
</div>;
}
return (
<div className="w-full px-4 py-12 ">
<div className="max-w-6xl mx-auto">
<h2 className="font-paperlogy text-4xl md:text-5xl font-bold text-foreground mb-12">Blog</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 font-paperlogy tracking-wider">
{/* 여기에 블로그 입력 */}
{blogs.length === 0 ? <p className="text-foreground">블로그를 준비중입니다.</p> : blogs.map((blog) => <BlogCard key={blog.id} blog={blog} onClick={handleBlogClick} />)}
</div>
</div>
</div>
);
}
어떻게 마무리 해야할지 모르겠다.
배럴 익스포트의 문제점을 유의미하게 격어보지는 못했기 때문에 아직은 DX적인 부분을 좀더 고려하여 개발을 진행해보려 한다.
이후 규모가 큰 프로젝트를 만났을 때, 직접 경험해보며 느껴보고 싶다.
마지막으로 참고했던 블로그 글 namgigi!