あるプロジェクトで循環参照(Circular Dependency)エラーに遭遇しました。

原因を追っていくと、犯人はバレルファイルでした。バレルファイルとは、フォルダ内の複数モジュールを index.ts に集約して再エクスポートするものです。
原因がバレルだと分かった後、プロジェクトからバレルパターンを取り除くことにしました。その結果得られた良い変化と、それでも一部で維持した理由を記録しておきます。
ただ import をきれいにしたかっただけ
バレルを導入した理由は単純で、import 文をすっきり見せたかったからです。
// こう書くより
import { validateEmail } from "@/shared/helpers/validation/email";
import { formatDate } from "@/shared/helpers/format/date";
import { calculateAge } from "@/shared/helpers/calculate/age";
// こうした方がきれい!
import { validateEmail, formatDate, calculateAge } from "@/shared/helpers";副作用のことなど考えていませんでした。「1 行にまとまるならその方がいいでしょ?」という軽い気持ちです。多くのプロジェクトでバレルが使われていたので、悪いパターンかもしれないと疑いもしませんでした。
そして循環参照が発生!
ある日、ついに循環参照エラーにぶつかりました。問題はこんな感じです。
// 1. shared/helpers 配下のあるファイルで
import { SnsPlatforms } from "@/shared/enums";
// 2. その SnsPlatforms の中では
// @/shared/enums/SnsPlatforms.ts
import { createI18nMap } from "@/shared/helpers";
// 問題:バレルが原因で循環参照が発生
// shared/helpers/index.ts がすべてのエクスポートを集約しているため、
// shared/helpers → shared/enums → shared/helpers というサイクルができる
// 一時対応:バレルを経由せずに直接参照する
import { createI18nMap } from "@/shared/helpers/enum";すべてのエクスポートを 1 か所に集めるせいで、相互依存するファイル間で循環参照が起こりやすくなっていました。
これをきっかけに、バレルの使用を見直しました。すでに多くの記事が問題点を指摘しています。
循環参照は一部にすぎない…
バレルファイルは、循環参照以外にも問題を抱えています。
ツリーシェイキングの阻害
最新の JS バンドラ(Vite、Webpack など)は、ツリーシェイキングで未使用コードを削除し最終成果物を最適化しますが、バレルはこの過程を妨げることがあります。
// 1. バレル経由(バンドラ:「index.ts を解析しよう」)
import { validateEmail } from "@/shared/helpers";
// 2. 直接インポート(バンドラ:「email.ts だけ見ればいい!」)
import { validateEmail } from "@/shared/helpers/validation/email";1 の場合、validateEmail だけが必要かどうかを判断するために、バンドラは @/shared/helpers(index.ts)全体を解析します。そこからさらに何十ものファイルが再エクスポートされていると、「どれかが副作用を持つかもしれないから全部バンドルした方が安全」と保守的に判断されることがあります。
結果として、本当に必要なのは validateEmail だけなのに、helpers フォルダ内のユーティリティが丸ごと最終バンドルに含まれてしまう可能性があります。
バレルを削除してみた
一度問題に遭遇してから、すべてのバレルを削除することにしました。進める中で、次のような良い変化がありました。
1. import は相変わらず十分きれい
最初に心配していた点ですが、行数は少し増えたものの、気になるほどではありません。IDE で import ブロックを折りたためば、実害はありません。
むしろ“どこから何を持ってくるのか”が明示的になり、読みやすくなりました。@/shared/helpers という曖昧な経路よりも、@/shared/helpers/format/date のような明確な経路の方が意図がはっきり伝わります。
2. 不要なフォルダ/ファイル構成が消えた
バレルを使っていると、「フォルダができたら index.ts も作らなきゃ」という暗黙の強迫観念が生まれがちでした。その結果、次のように中身が 1 ファイルだけなのに index.ts だけは置いてある…という非効率な構成も生まれていました。
features/
└── helpers
└── something.ts
└── index.tsバレルを削除すると、こうした不要な index.ts が消え、構成はより直感的でシンプルになりました。
3. 「うっかり」ヒューマンエラーが減った
バレルを使っていると、新しいユーティリティやコンポーネントを追加するたびに“2 か所”を直す必要がありました。
- 実ファイルの作成(例:
new-util.ts) - バレル(
index.ts)に export を追加
たくさんのファイルを触っていると、②を入れ忘れるミスが起きがちです。今はファイルを作れば、そのパスのままどこからでもすぐに import できます。
4. リファクタリングがずっと楽に
ファイルが index に紐づいていると、構造を変えるときに煩雑でした。index も一緒に直す必要があるし、IDE が全部を正しく追えないこともあって手作業で直すことも。今は index につながれていないので、IDE が import パスを自動更新してくれて、リファクタがとても自由になりました。
バレルが絶対悪というわけではない
すべてのバレルを消したわけではありません。次のように目的が明確なケースでは維持しています。
- Toast のような機能フォルダ。内部に複数のファイルがあっても、外側には
useToastやToastProviderといった決まったインターフェースだけを公開したい場合 - Table のように Row、Cell、Header などに分割したコンポーネント群。ただし最終的には全て import して 1 つの Table に組み上げるのが一般的な場合
components/
Table/
index.ts # バレルを維持
Table.tsx
TableRow.tsx
TableCell.tsx
TableHeader.tsx感想
どんな手法にも長所と短所があります。バレルも同じです。「常に使うべき必須」ではなく、「状況に応じて選べるオプション」と考えるのがよいでしょう。大事なのは、なぜその選択をしたのかを理解し、問題が起きたときに別のやり方を試せる柔軟さだと思います。