はじめに
こんにちは。ZOZOTOWN開発本部フロントエンドの菊地(@hiro0218)です。
現在、ZOZOTOWNではWebフロントエンド技術のリプレイスプロジェクトが進行しています1。本記事では、WebフロントエンドのリプレイスでCSS in JSの技術選定をした際の背景や課題についてご紹介します。
既存技術スタックの課題
リプレイス以前の環境は、Classic ASPのテンプレートエンジンに依存したUI実装が多く存在しており、新規開発や変更のタイミングで実装をReact + CSS Modulesへ改修しています。そのため、レガシーな実装とモダンな実装が共存した状態です。
こういった背景から、リプレイス以前のUI開発では以下のような課題がありました。
- グローバルなCSSが多く、CSSの変更がどこへ影響するのか予測しづらいClassic ASPのテンプレートエンジンに依存したUI実装が多く存在しているため
- CSS Modulesの課題近い将来、非推奨になる可能性がある
css-loaderのCSS Modulesはメンテナンスモードになっており2、再リプレイスを視野に入れないで済むように活発な技術にしたい
ファイルを自動生成してクラス名の補完やエラーが分かるようにはしていた
コンポーネント側でクラス名が間違っていてもエラーが発生しない.d.ts
スタイルの優先度に保証がない
管理コストがかかる
CSSファイルとJSファイルが別なため
ローカルスコープ(CSSクラス名を衝突させない)になる設定を外していた
ページ単位でグローバルなCSSから上書きする必要があった3
CSS設計を用いてクラス名が競合しないようにしていた
CSS Modulesを利用していますが、レガシーな実装とモダンな実装の整合性を取るために、CSSのクラス名をローカルスコープに出来ていませんでした。いずれにしてもReactコンポーネントの再開発が必要になり、リプレイス時に過去の資産を完全に活かし切るのも難しい状況であったため、技術スタックの再考の余地はあると考えました。
リプレイス以前の環境における課題や背景については、「ITCSS を採用して共同開発しやすい CSS 設計を ZOZOTOWN に導入した話」でも触れております、興味のある方は併せてご確認ください。
なぜCSS in JSを使うのか
CSS in JSとは、コンポーネントに属するCSSをバンドルさせるためのアプローチです。CSS in JSを利用することで、CSSはコンポーネントに定義され、外部のCSSファイルに依存することなくコンポーネント単体で独立して動作させることができます。
既存技術スタックの課題でも挙げましたが、これまではグローバルなCSSを利用しており、CSSの定義を変更した際にどこへ影響があるか予測しづらいという課題がありました。CSS in JSを利用することで、CSSの変更による影響をコンポーネント内に留めることができます。
宣言的にUIを実装できるReactとの親和性も高いです。
CSS in JSの選定基準
CSS in JSのライブラリを選定する上で基準としたものは以下になります。
- タグ付きテンプレートリテラル記法が使えることメンバーがキャッチアップしやすいことを前提として、通常のCSSの使用感を変えたくない
オブジェクトスタイル記法だと既存実装を移植する際に難がある
- Sass(SCSS)記法のようなネストセレクタが使えること
- TypeScriptとの親和性があること
- 参考資料が多いこと
- メンテナンスが活発であること
CSS in JSの選定候補
いくつものCSS in JSライブラリを確認しましたが、使用感など選定基準にマッチしなかったものも多く、最終的に以下のライブラリが選定候補となりました。
Linaria(Zero-runtime CSS in JS)を検証する
パフォーマンスの観点で考えるとZero-runtime CSS in JSは理想的です。その中でもStyled ComponentsやEmotionなどの先発ライブラリと同様の構文を備えているLinariaは有力な候補のひとつでした。
しかしながら、検証していくうちに以下のような課題が出てきました。
- Linariaは動的なスタイルを使用した場合にCSS Custom Propertiesを出力するため、多用するとCSSが肥大化してしまう
- 動的なスタイルの値がundefinedな場合に不要なプロパティが残ってしまう
const Heading = styled.h1`
background-color: ${({ bg }) => bg};
`;
const Example = () => {
return (
<>
<Heading bg="red">Red Heading</Heading>
<Heading>Heading</Heading>
</>
);
};
<!-- 出力後 -->
<h1 class="tzzg9j5w" style="--tzzg9j5w-0:red;">Red Heading</h1>
<h1 class="tzzg9j5w" style="--tzzg9j5w-0:undefined;">Heading</h1>
<style>
.tzzg9j5w {
background-color: var(--tzzg9j5w-0);
}
</style>
- 同じコンポーネントをネストすると再利用されたCSS Custom Propertiesが上書きされていまい意図した動作をしない
const Stack = styled.div`
& > * + * {
margin-top: ${({ spacing }) => spacing || "0"};
}
`;
const Example = () => {
return (
<Stack spacing="1rem">
<Stack></Stack>
<Stack></Stack> {/* ここの margin-top が 0 になってしまう */}
</Stack>
);
};
ネストされた2番目のStackのmargin-topは1remとなることを期待するが、CSS Custom Propertiesの上書きによって0になってしまう
ZOZOTOWNは複雑なアプリケーションな上に開発メンバーも多く、上記のような課題が発生してしまうと開発の足かせになってしまいかねないため、今回は導入を見送りました。
Styled ComponentsとEmotionを比較する
選定候補はStyled ComponentsもしくはEmotionの2つになりました。この両ライブラリの比較検討の際に行った比較方法を紹介します。
機能面の比較
以前はStyled Componentsに比べ、後発のEmotionの方が利用できる機能は多かったようです。ですが、調べてみると現状はStyled ComponentsもEmotionと同様の機能があるようでした。
続きはこちら