eslint-plugin-barrel-rules: "Barrel-file에 방화벽을 달아줄까요?"

Racgoo·2025년 10월 13일

library

목록 보기
1/1
post-thumbnail

eslint-plugin-barrel-rules

"어? 이 함수 어디서 쓰는 거였지?" - 3개월 전 코드를 보는 나

오늘은 이전에 만들었던 eslint-plugin을 소개하려고 합니다.

요즘 프론트에서 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 파일명 바꾸면 장바구니도 터지겠네?"
"아니 애초에 왜 다른 모듈 내부를 마음대로 뒤지고 있어?"


왜 이런 일이 생길까?

1. 배럴 파일(index.ts)의 의미가 사라짐

// domains/user/index.ts (게이트웨이)
export { UserProfile } from './components/UserProfile';
export { useUser } from './hooks/useUser';
// "이것만 공개하고 싶었는데..."

// -------------------------------------------------------------------------

// pages/블라블라~.tsx (사용처)
import { internalHelper } from '../user/utils/priceHelper'; // 😱
// domains/user 경로만 공개하고 싶었는데..
// 실제로는 이렇게 직접 참조

왜 이렇게 되는가?

  • IDE의 자동완성이 내부 경로를 추천합니다
  • 개발자가 생각 없이 첫 번째 추천을 선택합니다
  • 시간이 지나면서 이런 참조가 수십 개로 늘어납니다

물론 나쁜 방법은 아니지만, Barrel-File을 이용하여 classprivate, public과 같이 모듈 내부의 기능을 공개하거나 고립하는 방식에는 적합하지 않았습니다.


2. "괜찮아, 나 혼자 쓰는데?"

// 처음엔 이렇게 시작
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개 파일에서 사용 중

깨진 유리창 이론이 여기서도 적용됩니다.

  • 규칙이 한 번 깨지면 → "다른 곳도 이렇게 하니까 나도..."
  • 규칙이 지켜지지 않으면 → 끊임없이 규칙이 지켜지지 않는 방향으로 코드 베이스가 쌓입니다

결국 "이 함수 어디서 쓰는지 모르겠어요"라는 말이 나오게 됩니다.


3. 타입스크립트는 막지 않음

// TypeScript: "경로만 맞으면 OK!"
import { anything } from '../../../../anywhere/deep/inside/secret';

너무나도 당연하게도 타입스크립트는 경로만 맞으면 import를 허용합니다.
(모듈 경계? 그런 건 없어요.)

TypeScript의 역할

  • ✅ 타입 체크
  • ✅ 인터페이스 검증
  • ❌ 모듈 경계 강제
  • ❌ 의존성 방향 제어

결국 아키텍처는 우리가 직접 강제해야 합니다.


그래서 만들었습니다: eslint-plugin-barrel-rules

핵심 아이디어

특정 디렉토리는 배럴 파일을 통해서만 접근 가능
(내부 파일에 직접 접근하면 ESLint 에러)

왜 ESLint 플러그인으로 만들었는가?

  • 코드 작성 즉시 피드백 (빨간 줄)
  • CI/CD에서 자동으로 체크
  • 팀 전체가 같은 규칙 적용
  • 설정만 하면 끝 (수동 리뷰 불필요)

기술적 구현

1. 배럴 패턴 강제 (enforce-barrel-pattern)

잘못된 상황

// ❌ 이런 식으로 내부 침투
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만 수정하면 끝
  • 모듈 이동/리팩토링 → 영향 범위가 명확
  • 코드 리뷰 → "이거 공개 API 맞나요?" 논의 가능

2. Wildcard Import 방지 (no-wildcard)

"배럴 파일 느리지 않음?? 추적도 어렵고.."

맞습니다. 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 제거 시 영향 범위 파악 쉬움


3. TypeScript Path Alias 지원

"나만의 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를 실제 경로로 변환
  • 변환된 경로로 barrel pattern 규칙 적용
  • 개발자는 alias를 자유롭게 사용하면서도 규칙 준수 가능

사용해보기

설치

npm i eslint-plugin-barrel-rules --save-dev

ESLint 9 (Flat Config)

// 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']
  }
}];

ESLint 8 (Legacy Config)

// .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 모드는 없나요?

사실 처음엔 isolated: true 모드를 만들었습니다.
"아예 다른 모듈은 쳐다도 못 보게 하자!(정말 순수한 모듈 제작 가능)"

하지만 실제로 사용하면서 문제를 발견했습니다:

  1. 너무 엄격함
    실무에서는 모듈 간 의존이 필요한 경우가 많음
  2. 설정 복잡도 증가
    allowedImportPaths를 계속 추가해야 함
  3. 학습 곡선
    새로운 팀원이 이해하기 어려움

결국 "배럴 파일로만 접근"이라는 핵심 기능에 집중하기로 했습니다.
(Issue가 쌓이면 다시 추가할 수도...?)

After 2025.10.22
isolate-barrel-file 규칙을 추가했습니다.
https://github.com/racgoo/eslint-plugin-barrel-rules


이 프로젝트를 통해 배운 것

1. 완벽한 도구보다 사용되는 도구

처음엔 모든 기능을 넣으려 했지만, 결국 핵심 기능만 남겼습니다.

2. 에러 메시지

친절한 에러 메시지가 긴 문서보다 효과적입니다.

❌ 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.

3. 개발자는 규칙을 싫어하지만, 좋은 규칙은 좋아한다

처음엔 "또 규칙이야?" 하지만,
나중엔 "이거 없으면 어떻게 관리하지?" 하게 됩니다.


앞으로의 계획

  • TypeScript Path Alias 지원
  • CommonJS 지원
  • ESLint 8/9 모두 지원
  • Wildcard 방지
  • Isolated 모드 재검토
  • 모듈 의존성 시각화 도구

마무리

"코드는 짧게, 의존성은 더 짧게"

eslint-plugin-barrel-rules는:

  • 모듈 경계를 강제합니다
  • 배럴 패턴을 자동화합니다
  • 의존성을 투명하게 만듭니다

더 이상 "이거 어디서 쓰는지 모르겠어요"라는 말은 하지 않아도 됩니다.

🔗 자세한 설명은 레포지토리의 README를 참고해주세요~


P.S. Barrel 파일은 Gateway입니다. 문으로 들어오세요, 담 넘지 마시고...

profile
개발자 락구

0개의 댓글