css-modulesを止めようとしている話(長々とした状況説明編)

BEMでいいじゃん話の続きその1にして, とあるアプリケーションが困っていた話.

書き味は最良ではないけれど, 設計思想を持った上で長期メンテを考えると結構いい感じだと思っているアプローチに至った. しかしながら, 放っておくと誰も試行錯誤の過程を書かないままになってしまいそうだし, それを書きそうな同僚も(多忙のあまり)書きそうな雰囲気はなさそうだし, もしかすると永遠に出てこない気さえするので書いてみる.

多分完遂の暁にはどこかで話として総括が出てくると思うけど, 中間報告ということで.

経緯

とあるアプリケーションは, 元々はCSS Moduleをcssのビルドシステムに用いてAtomic Designとして作っていた. CSS Moduleを選定した理由は自分はよく知らないが, ものの試しで使ってみようと思ったんだと思う. ここまでは別に間違ってはいない. (自分は選ばないが)styled-componentsに比べれば自分の好みではあるし, そこまでガミガミと言うつもりはない.

しかし, 肝心のコードがやらかしていた.

セレクタの黒魔術こそ殆どなかったものの, いたるところに存在するReact製のUI componentがclass属性の上書きを許容する形になっていた. この時点で開かれすぎたcomponent指向なので色々破綻しているのだが, おそらく汎用的なcomponentを作ろうとする麻疹の一種だったのだろうと推察している. そしてそれは同時に, initial release前の炎上の鉄火場のなかでダクトテープを貼り付けてデザインレイアウトとの不整合をどうにか鎮火しようとするプログラマーのメンタルモデルとハマってしまったのだろう(その時自分はその場にいないので、真相は藪の中だけど). 結果, アプリケーションサービスがリリースされて1年以上が経過した頃(自分が関わるようになった頃)には, いたることで上位のcomponentが思い思いにclass属性を上書きすることで帳尻を合わせるコードが散見されるようになっていた.

複雑なセレクタの黒魔術による上書きではなく, class属性の上書きで対処しようとした理由はわからない. だが、おそらくはCSS Moduleを使っている関係上, production buildでclass属性がmangleされてしまい, そのmangle結果もコードの成長に応じて必ずしも一定しないためにセレクタでの解決が不可能だったのではないかと推測する. とはいえセレクタを使っていても問題は後述の問題は回避できなかったと思うけど.

さて, そのような不安定なコードベースの現状に対して, アプリケーションの属するサービスは順調に成長を重ねていた. もし「実は伸びないのでcloseします」みたいなサービスであったら、ガタガタでも何の問題も無かっただろう. どうせ死ぬんだし. だが, それとは真逆でむしろ伸ばす方向にするにはどうするかを考える必要があった. そのような状況で, やばいなーと思いつつもガタガタになっているとはつゆも知らない自分(と同僚たち)はうっかりパンドラの箱を開けてしまうことになる.

発端はコードベース内のディレクトリ構造をリファクタリングしているときに起こった. そもそもディレクトリ構造をリファクタリングしていたのはコードやモジュールの責務境界を明瞭にするために実施したのだが, その過程でES Moduleのimport文の順序関係や格納されるパスのアルファベット順を変えてしまうケースがあった. 変えてもアプリケーションはちゃんと動いているし読み込む側の順序に依存するようなことはないだろうと思っていた.

読み込む側での順序を変えた結果, アプリケーション内で読み込まれる側のモジュールの依存関係に基づくDAG上でトラバースされ評価されるタイミングは変わることになる. しかもその変わった対象は変更したモジュールから遠く離れた箇所であったりもするため, changesetから読み取ることも難しい. これがJSの世界に閉じただけならば問題なかったのだが, 変更前後でグラフ構造の比較が必要になる上, 「変更箇所から10モジュールくらい跨いだ先のJSファイルが読み込んでいるCSSファイルの評価順序が変わった」のを上手く検知しながら他を壊さないように直す方法を探す必要がある. さらに悪いことに, その評価順序の変更の適用される対象が「最終的にはフラットな構造を上から下まで読み込んだ順序に評価する」cascading style sheetsなのであった.

結果を先に述べると, 上の方で述べた「上位のcomponentがclass属性を介して下位のcomponentのclass属性を上書きして微妙なレイアウトを調整する」ために必要なcascading順序が大きく破綻して, 「注意して確認すると壊れている」「大きく破綻するケースもあれば小さくずれているだけも両方ありうる」状況になってしまった.

それでもreftestがあれば早期に問題には気づけたであろうが, 当時の自分たちはreftestを持っておらず, しかも悪いことにリファクタリング作業が半分以上進んだ段階でその問題が発覚したために(それまで気づくことができなかった), 今更ディレクトリ構造の変更を中断して全てをrollbackするのも難しい状態になっていた.

では仮にreftestを持っていた場合は早期に問題が検知できて堅実に進めることができたのだろうか? その場合は先にも述べたようにモジュール間の依存グラフ構造を黒ひげ危機一発的におっかなびっくりと壊さないように変更を進めることになる. それが嫌であれば、コード行数10~15万行の中から, まず安易な上書きをしているところをすべて取り除く必要があり,それは縦横無尽に鎮火を前提として書かれたhackを全て取り除くだけのチームの体力が要求される. どちらにせよ, コードの責務分界点を分けるという目的に到達するまでに年単位の労力が必要になる. そして繰り返すが当時の自分たちにはreftestはない. 事実上, この路線のままでは軽微なcleanupにも等しいリファクタリングさえもロクにできない状況に追い込まれていた. リファクタリングできないコードベースとか敗北宣告にも等しかった.

解決に向けて

話を戻す, 今更rollbackして後にも引けない, かといって放っておくと前進すら難しい自分たちは以下の強行着陸を採用することになる..

  1. 可能な限りモジュール依存グラフの評価順序を変えないために, JSのimport文のsortを諦める
  2. CSS Module by css-loaderを使うのではなく, 素のcssをconcatすることでcascadingの評価順序を壊さないようにする

1は, 評価順序を変えなければ最終成果物が変わらないという保証が取れたので, 2で払うべきコストを先送りしてディレクトリ構造の変換を継続するための手段. 2は, すでに壊れてしまったものを制御可能な状態に戻すための処置. 手間は増えるしダルいものの, 明示的にcascadingの順序を制御できるために, もっともシンプルな解決方法となる.

この2つを, 最終的に2に持っていく合意のもとに壊れた箇所を修正しながら適用することになった. そもそもclass属性上書きしているのを直すべきなのだが, 影響範囲が大きすぎる + regressionの鎮火が優先であるため, 次のフェーズの課題とした.

結果的に多数のregressionは鎮火し, その後細々と変え続けた結果, 開始から数カ月(機能実装などで一時中断していたので実働としてはもっと短い)ののちにディレクトリ構造の変更は完了したのであった(CSS Module撤廃はside work的に現在も進行中)..

......同僚各位、その節は本当にご迷惑をおかけしました....

なんか話が長くなってきたので具体的にどうしたかは次のエントリで書くことにする.