Table of contents
원문: Dominik Dorfmeister, "Please Stop Using Barrel Files"
사진: Vince Veras
어떻게 해야 '올바르게' 파일을 정리할 수 있는지는 프론트엔드 개발자들 사이에서 논란의 여지가 많은 주제입니다. 파일을 이리저리 움직이며 "감"이 올 때까지 시도해보라는 말은 흔히 하는 농담이지만 이 말을 한 Dan Abramov는 농담이 아니라고도 말하는데요. 아마 혼자 작업할 때는 이래도 괜찮겠지만 여럿이 모이게 되면 각자가 옳다고 생각하는 부분이 다 다를 것입니다. 때문에 이 조언은 상당히 주관적이며 전문적인 환경에서는 별 도움이 되지 않을 겁니다.
제 경험상 대부분의 개발자는 프로젝트에서 이미 사용 중인 구조에 적응하는 데 크게 불만이 없습니다. 개인적으로 동의하지 않는 구조라 해도 파악하기 쉬운 단일 코드베이스를 따르는 것이 모두가 하고싶은 대로 하는 것보다는 낫다는 말이죠.
저에게도 저만의 선호하는 방식이 있고 (아마) 여러분도 그럴 겁니다. 하지만 저는 팀의 일관성을 유지해야 합니다. 사소한 세부 사항 논의를 최대한 피하기 위해 가능한 한 많은 것을 변동 사항 없이 강제하고 싶죠. ESLint 플러그인 중 unicorn/filename-case
규칙이 매우 좋은 예시 중 하나입니다. 대소문자 규칙 중 하나를 골라 모든 곳에 적용하면 그걸로 끝이거든요.
하지만 제가 최근에 가장 강조하고 있는 것 중 하나는 무슨 일이 있더라도 배럴 파일을 사용하지 않는 것입니다.
배럴 파일이란 무엇일까요?
배럴 파일이란 다른 파일에서 가져온 것들을 다시 export하는 것 외에는 아무 것도 하지 않는 파일을 말합니다. 보통 index.js
또는 index.ts
라는 이름으로 작성되죠. 이런 파일들은 라이브러리를 가져다 쓰는 사람들로 하여금 디렉터리의 내부 구조를 알 수 없도록 숨기기 위해 존재합니다. 예를 들어 저장소가 아래와 같은 디렉터리 구조라고 생각해 봅시다.
- tab
- tab-list.tsx
- tab-panel.tsx
이제 각 파일이 하나의 컴포넌트만을 export한다고 가정해보죠. 이는 곧 다음과 같이 해당 모듈을 개별적으로 import할 수 있다는 뜻입니다.
import { TabList } from '@/tab/tab-list'
import { TabPanel } from '@/tab/tab-panel'
개발자는 종종 탭과 관련된 모든 것을 가져올 수 있는 단일 인터페이스를 제공하여 코드 구성을 개선하고 싶어합니다. 일반적으로 이들을 다시 export하는 배럴 파일을 작성하죠.
export { TabList } from './tab-list'
export { TabPanel } from './tab-panel'
index
라는 이름을 가진 파일은 import할 때 생략할 수 있기 때문에 다음과 같이 정리된 듯한 모습을 얻게 됩니다.
import { TabList, TabPanel } from '@/tab'
이는 훨씬 더 깔끔해 보일 뿐만 아니라 가져다쓰는 측에서 코드를 갱신할 필요 없이 내부 파일을 이동할 수 있다는 것을 의미합니다. 그렇다면 문제는 어디에 있을까요?
순환 참조
디렉터리에 배럴 파일이 있으면 현재 파일 위치에 따라 import 방식이 바뀝니다. use-tab-status.ts
유틸리티 파일을 추가해 예시를 확장해보죠. 해당 파일은 TabPanel
에서 사용할 커스텀 훅을 담고 있습니다. 이 커스텀 훅을 모두가 사용할 수 있도록 하기 위해 배럴 파일에 추가하였습니다.
export { TabList } from './tab-list'
export { TabPanel } from './tab-panel'
export { useTabState } from './use-tab-state'
지금까지는 다 좋습니다. 가져다쓰는 측은 배럴 파일에서 useTabState
를 직접 가져올 수 있죠. 하지만 TabPanel
안에서 useTabState
를 import하게 되면 어떻게 될까요? 이는 우리가 어떻게 import문을 쓰느냐에 따라 정해집니다.
// ✅ 좋습니다: use-tab-state 모듈에서 import하고 있습니다
import { useTabState } from './use-tab-state'
// ❌ 좋지 않습니다: 아래 둘 모두 배럴 파일에서 import하고 있습니다
import { useTabState } from '@/tab'
import { useTabState } from './'
항상 하던 대로 배럴 파일에서 가져오게 되면 순환 참조 문제가 생깁니다. 예를 들어 tab-panel.ts
가 index.ts
를 import하는 동시에 index.ts
가 tab-panel.ts
를 다시 export한다면 이 두 파일 사이에 순환 참조가 발생합니다.
자바스크립트야 순환 참조에 꽤 너그러운 편입니다만 특정 상황에서는 번들러가 알 수 없는 오류 메시지와 함께 충돌할 수 있습니다. 자동 import 기능을 사용할 때 이런 실수가 주로 일어나죠. 솔직해지자고요. 보통 에디터나 코파일럿이 정해주는 대로 가져오고 그대로 내버려두는 경우가 많잖아요. 적어도 저는 직접 import문을 적어본 지 꽤 오래 됐습니다.
import/no-cycle
정적 분석 규칙이 꽤 많은 순환 참조를 잡아줄 수 있으니 활성화하시는 것을 추천드립니다.
개발 속도
배럴 파일의 두 번째 문제는 배럴 파일을 가져올 때 내부적으로 어떤 일이 벌어질 지 생각하면 알 수 있습니다. import { useTabState } from '@/tab'
와 같이 작성하면 자바스크립트는 index.ts
파일을 순회하며 안에 있는 모든 모듈을 동기적으로 불러옵니다. 배럴 파일이 포함하는 파일이 3개밖에 없다면 아마 별 문제가 되지 않겠지만 상황은 쉽게 복잡해질 수 있습니다. 예를 들어 가져올 필요가 없는 다른 것들을 포함한 배럴 파일이나 엄청 많은 모듈을 포함하는 서드파티 라이브러리 등을 포함하는 경우 말이죠.
저희 Next.js 프로젝트에는 11,000개가 넘는 모듈을 불러오기 위해 페이지를 준비하는 시간이 5~10초나 걸리는 페이지들도 있었습니다. 대부분의 내부 배럴 파일을 제거한 후에는 3500개 정도로 68%나 줄어들었죠. 배럴 파일을 통해 수많은 것들을 내보내는 공유 패키지 형태는 그 중 하나의 모듈만 필요하다면 별로 좋지 않다는 것이 밝혀진 셈입니다. 😅
Next.js 팀도 배럴 파일이 개발 모드에 미치는 문제를 깨달았으며 배럴 파일에서 가져오는 형태를 실제 모듈 경로로 자동 변환해주는 optimizePackageImports
기능을 제공하게 되었습니다. Shu Ding이 작성한 Next.js에서 패키지 가져오기를 최적화하기라는 블로그 글에서 이 기능이 어떻게 동작하는지 자세하게 설명합니다. 가장 신기한 점은 이 최적화가 가져오기한 것을 내보내기만 하는 "실제" 배럴 파일인 경우에만 적용된다는 것입니다. 아래처럼 다시 내보내기하지 않는 코드를 한 줄만 작성해도 잠재적인 부작용 때문에 최적화할 수 없게 됩니다.
export { TabList } from './tab-list'
export { TabPanel } from './tab-panel'
export { useTabState } from './use-tab-state'
// ❌ 좋지 않습니다: 이 줄이 전체 파일을 최적화할 수 없게 만듭니다
export const foo = 5
다시 말하지만 가장 좋은 것은 배럴 파일을 피하는 것입니다.
배럴 파일이 좋은 경우
개인적인 생각으로는 배럴 파일은 응용 프로그램 제품의 디렉터리 내용을 그룹화하기 위해 만들어진 것이 아닙니다. 더 엄격한 정적 분석 규칙을 적용하지 않는 한 다른 개발자가 배럴 파일에서만 가져오기하도록 강제할 수는 없거든요. 배럴 파일을 이용해 특정 모듈을 "비공개"로 만들 수는 없다는 말입니다.
배럴 파일이 필요한 것은 라이브러리를 작성할 때입니다. @tanstack/react-query
같은 라이브러리들은 package.json
의 main
필드에 넣을 단일 진입점 파일이 필요합니다. 가져다쓰는 측이 사용할 수 있는 공개 인터페이스 말이죠. 저에게 배럴 파일이 의미 있는 유일한 경우입니다. 하지만 응용 프로그램 코드를 작성하는 경우 아무 디렉터리에나 index.ts
를 넣게 되면 삶이 더 힘들어질 겁니다. 그러니 제발 배럴 파일 사용을 멈춰주세요.