😇

ここがむずいよ!scss

2024/12/06に公開

ディップ Advent Calendar 2024の記事です。他の記事も是非見てね

はじめに

これまでバックエンドエンジニアとして活動していましたが、この4月からは初めてフロントエンドを担当しています。

何もかもが新しい経験の中、特に苦戦しているのがscssの実装です。簡単なcssの実装経験はこれまでにもありましたが、scssは僕が知っていたcssとは全く異なるものでした。

スタイルを定義する際にネストが使えるのも衝撃だったし(2023年8月からcssでもネストが対応された)、変数や関数を定義出来るのも衝撃だったし、@use@import@forwardはややこしいし、@mixinは未だに拒否反応が出てしまいます。もはやマークアップ言語では無いと思います。

四苦八苦しながらも、最近になってようやくscssに慣れてきたので、scssの何が難しいのかを改めて整理し、その解決方法について考えてみようと思います。

scssが難しい理由

cssのメタ言語であるcssは、cssに比べ実装の自由度が高いです。それ故、特にチーム開発では可読性保守性が低下することが往々にしてあります。

具体的な要因として、主に以下の3つが挙げられると考えています。

  1. 過度なネストによる複雑化
  2. 変数やネイティブ関数による過剰な抽象化
  3. ディレクトリ構造の過度な複雑化

それぞれの事象が発生する原因とその対策について考えを述べていきます。

1. 過度なネストによる複雑化

scssの重要な機能であるネストは、適切に使用すればコードの可読性や構造化に大きな効果をもたらしますが、乱用や不適切な設計が行われると逆にコードの保守性や可読性が大きく低下する「諸刃の剣」だと思います。

例えば、以下のようなヘッダーを定義しているHTMLにおいて、linkクラスにスタイルを付与したい場面で、次のようにscssを書いたとします。

html
<div class="header">
  <nav class="nav">
    <ul class="menu">
      <li class="item">
        <a href="#" class="link">Menu Item</a>
      </li>
    </ul>
  </nav>
</div>
scss
.header {
  .nav {
    .menu {
      .item {
        .link {
          color: blue;
        }
      }
    }
  }
}

上記のコードは、構造化により親子関係が直感的に把握しやすいというメリットはあります。一方で、デメリットが沢山あります。

まず、スタイルの再利用性が低いです。.linkに対するスタイルが特定の階層構造(.header .nav .menu .item)に依存しているため、他の場所で.linkを再利用したい場合、新たなスタイルを定義する必要があります。意図している場合を除いて、過度に特異性を高めることは拡張性を低下させるので避けるべきです。

また、HTML構造に依存しているので、HTMLを変更しづらいです。例えば、上記のHTMLからどれか一つのタグを削除しただけで、上記のスタイルは機能しなくなります。

加えて、特異性(Specificity)が高いため、スタイルの上書きが難しいです。意図している場合を除いて、一般的に、過度に特異性を高めることは拡張性を低下させるので避けるべきです。

よって、上記のように無意味に構造化を行うのではなく、必要最低限のセレクタを使用したネストに留めることで、拡張性や保守性に配慮した実装を心がけるべきです(この点でBEM記法はとても効果的だと思います)。

2. 変数やネイティブ関数による過剰な抽象化

scss特有の昨日である変数定義や関数は、適切に使うことでコードの冗長化を防ぐ等のメリットがあります。例えば、基本カラーを変数として定義したカラーファイルを作成して共通化したり、頻出するUIコンポーネントをプレースホルダーセレクタ(%使って定義するやつ)として定義して再利用することは、変数や関数の効果的な活用方法だと思います。

一方で、過度な利用は、これまた多くのデメリットを引き起こすと考えています。

まず、変数や関数の利用が多くなるほど、保守が困難になります。多くの場所で呼び出されている関数や変数は、その分だけ影響範囲が大きくなります。

また、適切に変数や関数を設計しないと、逆に再利用性が低下する場合があります。

例えば下記は極端な例ですが、似た役割を果たしている2つの限定的な処理を関数化しており、コードの冗長化と再利用性の低下を招いています。

scss
@function calculate-header-font-size($base-size) {
  @return $base-size * 1.2;
}

@function calculate-card-font-size($base-size) {
  @return $base-size * 1.5;
}

.header {
  font-size: calculate-header-font-size(10px);
}

.card {
  font-size: calculate-card-font-size(10px);
}

このことから、不必要に処理や定数を共通化することは避け、意味のあるまとまりで共通化するべきです。

3. ディレクトリ構造の過度な複雑化

scssのディレクトリ設計についてはさまざまなブログや記事でテンプレートが紹介されています。しかし、結局のところ適切なディレクトリ設計はプロダクトの規模や種類によると思います。

例えば、とあるブログで紹介されているテンプレートでは、animationsディレクトリ内にトランジションやアニメーションに関する定義をまとめる方法を紹介していますが、アニメーションが存在しないプロダクトの場合は当然このディレクトリは不要になります。

そのため、一概にディレクトリ設計をこうすべき!と述べることは出来ないと思います。しかし、ディレクトリ設計におけるアンチパターンとして、過度の構造を複雑化しすぎることが挙げられると思います。

何も考えずに複雑なディレクトリテンプレートを導入しようとすると、そのスタイルをどのディレクトリに記述するべきかの判断が曖昧になり、結果的に不適切なスタイル配置を招いてしまいます。特にチーム開発においては、メンバー同士でこのような意思判断基準を統一することは困難です。

よって、ディレクトリ構造は過度に複雑化させたり、何も考えずにテンプレートを真似することは避け、無理のない範囲での適切な細分化に留めるべきです。

個人的には、基本的に各コンポーネントに対して1つのスタイルファイルを定義する運用が一番楽で好きです。具体的には↓のようなイメージです。

├── src
│   ├── styles      # 共通で使うものを最小限ここで管理
│   │   ├── color.scss    # 共通で使うカラーをまとめたもの
│   │   └── global.scss   # プロダクト全体で共通して使用するもの
│   ├── components  
│   │   ├── Accordion
│   │   │   ├── index.jsx
│   │   │   └── accordion.scss
│   │   ├── Button
│   │   │   ├── index.jsx
│   │   │   └── button.scss
│   │   └── ...
│   └── style.scss

おわりに

結論として、必要最低限の機能で適切な実装を行うことが大事なんだなと思いました。

今後何か新しい気づきがあったら加筆・修正していこうと思う。

Discussion