"어? 이 함수 어디서 쓰는 거였지?" - 3개월 전 코드를 보는 나
요즘 프론트에서 FSD 많이 쓰지 않나요?? (맹신론자 아님)
FSD가 아니어도 저는 함수형 프로그래밍을 할 때 보통 디렉토리 단위 모듈을 Barrel-File로 관리하는 방식을 선호합니다 🦀
Barrel-File이 뭐임?
디렉토리에 존재하는index.(ts,js,tsx...etc)는/블라블라~/{디렉토리이름}경로로 import가 가능하기에 디렉토리에 있는 다양한 기능들을 index에서 import하고 다시 export하는 방식을 말합니다.
(장점보단 단점이 많다고 하는... Reddit에 올렸다 대차게 욕먹기도 했습니다..하하)
이러한 방식은 모듈의 원작자의 의도와 다르게 은근슬쩍 내부 기능들이 멋대로 사용되는 현상이 종종 발생합니다.
// domains/cart/components/CartItem.tsx
import { calculateDiscount } from '../../user/utils/priceHelper';
import { formatCurrency } from '../../payment/formatters/currency';
"어...? 장바구니가 왜 유저 모듈 내부까지 들어가서 가져와?"
2줄의 import가 말해주는 것
→ "장바구니가 유저, 결제 모듈의 내부 구현에 의존"
그리고 이 순간 깨달았죠.
"
priceHelper는 모듈 내부에서만 사용하는 기능인데 이거 나중에priceHelper파일명 바꾸면 장바구니도 터지겠네?"
"아니 애초에 왜 다른 모듈 내부를 마음대로 뒤지고 있어?"
// domains/user/index.ts (게이트웨이)
export { UserProfile } from './components/UserProfile';
export { useUser } from './hooks/useUser';
// "이것만 공개하고 싶었는데..."
// -------------------------------------------------------------------------
// pages/블라블라~.tsx (사용처)
import { internalHelper } from '../user/utils/priceHelper'; // 😱
// domains/user 경로만 공개하고 싶었는데..
// 실제로는 이렇게 직접 참조
왜 이렇게 되는가?
물론 나쁜 방법은 아니지만, Barrel-File을 이용하여 class의 private, public과 같이 모듈 내부의 기능을 공개하거나 고립하는 방식에는 적합하지 않았습니다.
// 처음엔 이렇게 시작
import { temp } from '../user/utils/temp';
// 3개월 후
import { temp } from '../user/utils/temp';
import { helper } from '../user/services/helper';
import { validate } from '../user/validators/check';
// ... 15개 파일에서 사용 중
깨진 유리창 이론이 여기서도 적용됩니다.
결국 "이 함수 어디서 쓰는지 모르겠어요"라는 말이 나오게 됩니다.
// TypeScript: "경로만 맞으면 OK!"
import { anything } from '../../../../anywhere/deep/inside/secret';
너무나도 당연하게도 타입스크립트는 경로만 맞으면 import를 허용합니다.
(모듈 경계? 그런 건 없어요.)
TypeScript의 역할
결국 아키텍처는 우리가 직접 강제해야 합니다.
핵심 아이디어
특정 디렉토리는 배럴 파일을 통해서만 접근 가능
(내부 파일에 직접 접근하면 ESLint 에러)
왜 ESLint 플러그인으로 만들었는가?
// ❌ 이런 식으로 내부 침투
import { calculateDiscount } from '../user/utils/priceHelper';
// ESLint Error: Please import from '../user'.
// Direct access to '../user/utils/priceHelper' is not allowed.
// You must use the barrel pattern and only consume APIs exposed externally.
// This is to ensure encapsulation of internal logic and maintain module boundaries.
import { InternalComponent } from '../cart/components/internal/Secret';
// ESLint Error: Please import from '../cart'.
// Direct access to '../cart/components/internal/Secret' is not allowed.
// ✅ 배럴 파일을 통해서만 접근
import { calculateDiscount } from '../user';
import { PublicAPI } from '../cart';
{
"barrel-rules/enforce-barrel-pattern": ["error", {
paths: ["src/domains/*"], // 이 디렉토리들은 배럴을 통해서만!
baseDir: __dirname
}]
}
이제 src/domains/user/utils/priceHelper.ts에 직접 접근하려고 하면:
❌ ESLint Error: Direct import from barrel path is not allowed.
Use 'import from "../user"' instead.
이렇게 하면 무엇이 좋아지는가?
priceHelper.ts 파일명 변경 → user/index.ts만 수정하면 끝"배럴 파일 느리지 않음?? 추적도 어렵고.."
맞습니다. Wildcard import는 여러 문제를 만듭니다:
// ❌ 개발자의 디버깅을 어렵게 하는 "*"(와일드카드)
import * as User from '../user/utils/priceHelper';
User.calculateDiscount();
// 문제: calculateDiscount가 User 어디서 왔는지 모호
{
"barrel-rules/no-wildcard": ["error"]
}
import * as User from '../user'; // ❌ wildcard 금지!
export * from './utils'; // ❌ 이것도 금지!
// ✅ 명시적으로만
import { calculateDiscount, formatPrice } from '../user';
export { CartItem, useCart } from './components';
그래서 이게 왜 좋아????
1. Tree-shaking 향상
사용하는 것만 번들에 포함 → 번들 사이즈 감소
2. 코드 추적 용이
calculateDiscount가 어디서 왔는지 명확 → IDE의 "Go to Definition" 정확도 향상
3. 리팩토링 안전
사용하지 않는 export 제거 시 영향 범위 파악 쉬움
"나만의 Alias를 사용하면 못 쓰지 않나??"
안심하세요. 자동으로 인식합니다:
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@domains/*": ["src/domains/*"],
"@shared/*": ["src/shared/*"]
}
}
}
import { Button } from '@shared/components/Button'; // ✅ alias 지원
import { internal } from '@domains/user/utils/secret'; // ❌ 내부 접근 차단
어떻게 동작하는가?
tsconfig.json을 읽어서 alias를 실제 경로로 변환npm i eslint-plugin-barrel-rules --save-dev
// eslint.config.js
import barrelRules from 'eslint-plugin-barrel-rules';
export default [{
plugins: {
'barrel-rules': barrelRules
},
rules: {
'barrel-rules/enforce-barrel-pattern': ['error', {
paths: ['src/domains/*'],
baseDir: __dirname
}],
'barrel-rules/no-wildcard': ['error']
}
}];
// .eslintrc.js
module.exports = {
plugins: ['barrel-rules'],
rules: {
'barrel-rules/enforce-barrel-pattern': ['error', {
paths: ['src/domains/*'],
baseDir: __dirname
}],
'barrel-rules/no-wildcard': ['error']
}
};
사실 처음엔 isolated: true 모드를 만들었습니다.
"아예 다른 모듈은 쳐다도 못 보게 하자!(정말 순수한 모듈 제작 가능)"
하지만 실제로 사용하면서 문제를 발견했습니다:
allowedImportPaths를 계속 추가해야 함결국 "배럴 파일로만 접근"이라는 핵심 기능에 집중하기로 했습니다.
(Issue가 쌓이면 다시 추가할 수도...?)
After 2025.10.22
isolate-barrel-file규칙을 추가했습니다.
https://github.com/racgoo/eslint-plugin-barrel-rules
처음엔 모든 기능을 넣으려 했지만, 결국 핵심 기능만 남겼습니다.
친절한 에러 메시지가 긴 문서보다 효과적입니다.
❌ Direct import not allowed
✅ Please import from '../user'. Direct access to '../user/utils/priceHelper' is not allowed.
You must use the barrel pattern and only consume APIs exposed externally.
처음엔 "또 규칙이야?" 하지만,
나중엔 "이거 없으면 어떻게 관리하지?" 하게 됩니다.
"코드는 짧게, 의존성은 더 짧게"
eslint-plugin-barrel-rules는:
더 이상 "이거 어디서 쓰는지 모르겠어요"라는 말은 하지 않아도 됩니다.
🔗 자세한 설명은 레포지토리의 README를 참고해주세요~
P.S. Barrel 파일은 Gateway입니다. 문으로 들어오세요, 담 넘지 마시고...