デザイン CSS Sass フロントエンド

メタプログラミング Sass - 柔軟なウェブデザインを実現する方法

デザイン CSS Sass フロントエンド

この記事は GMO ペパボデザイナー Advent Calendar 2020 9 日目の記事です。

デザイン部でデザインエンジニアを自称している gyugyu です。最近自宅の近所に肉屋を見つけたので家焼肉をやっています。

現在ペパボでは Inhouse というデザインシステムを策定しており、これはコンポーネントスタイルライブラリやコンテンツ作成ガイドラインを含む、比較的広義のデザインシステムです。このコンポーネントスタイルライブラリの技術的リードを、私は業務の一環として行っています。

はじめに - ペパボのサービスとウェブデザインの難しさ

ペパボはこれまで様々な分野で様々なサービスを提供してきました。各サービスのウェブデザインの方針として、提供者がペパボであるということを強調せず、サービスごとに違う手触りを持つようになっています。このサービスデザインの方針ゆえに、 Bootstrap のような単一の見た目、もしくは変数を操作することで多少の見た目の違いを実現する CSS フレームワークでは対応できないと判断しました。

この判断には Sass の言語仕様から制約を受けていたという側面もあります。以前の Sass では変数や function, mixin が全てグローバルに存在しており、あらかじめ import しておくとどこからでも使えるという状態でした。この言語仕様で複雑なパラメータ管理を実現しようとすると、どこで何が定義されているのか、もしくは定義されていないのかがすぐわからなくなってしまいます。

こうした悩ましさに一定の解決をもたらしたのが、 Dart Sass 1.23.0 から導入された @use ルールです。 @use ルールにより、比較的モダンな言語に近い、デフォルトでは閉じているモジュールシステムが実現されました。 Node に慣れている方は require を想像してもらうと良いかと思われます。

Inhouse のコンポーネントスタイルライブラリはこの @use ルールが導入されたころに構想され、 2020 年 6 月に v0.1.0 がリリースされ、 2020 年 12 月現在では v0.3.0 まで更新されています。スタイルライブラリと言いつつも内部的にはフレームワークに近い発想で実装されており、 @use ルールとメタプログラミングによって柔軟性を担保しつつ、フレームワークとしての硬さも両立するようになっています。 Inhouse 開発はペパボ内部で OSS 開発をしているような、難しさと面白さを兼ね備えたエキサイティングな試みであると言えます。

さて、今回の記事では Inhouse を支えている Sass メタプログラミングの中から、実践的な小技を見ていきましょう。 Sass の大まかな書き方については、 Sass 公式のドキュメントを参照してください。

メタプログラミング tips 1. 動的に変数を取得する

@use ルールを使うと変数や関数をグローバルに書き込むことなく参照することができるようになるというのは前に述べたとおりです。まずはストレートにモジュール化された変数を参照するコードを書いてみましょう。以下のような _foo.scss を用意します。

$width: 100%;
$color: #333;

定義した width, color 変数を参照する index.scss を試しに作ってみましょう。

@use 'foo';

.container {
  width: foo.$width;
  color: foo.$color;
}

このときに例のように _foo.scss から属性名に対応する変数を取得して CSS として適用するルールにしたとします。配列の each…in と string interpolation を利用すると、属性が増えてもシンプルに書き下すことができそうですね。

@use 'foo';

.container {
  $properties: width, color;  // もっと要素が増えても大丈夫

  @each $property in $properties {
    #{$property}: 0;  // ここでモジュールから変数を取得する処理を書きたい
  }
}

モジュール変数が map だったならば、見慣れた map-get 関数を使うことで取得できたでしょう。しかし、モジュール変数は map ではないので、一筋縄では取得できません。

その問題を解決するのが sass:meta ビルトインモジュール です。 meta.module-variables 関数を使うと、モジュールで定義した変数を map 形式で一気に取得することができます。

実は map-get も新しい Sass ではビルトインモジュールになっており、 sass:map に定義されています ので、 sass:meta に合わせてこれを使うようにしましょう。上記の例で実現したいコードは以下のように書き下すことができます。

@use 'foo';
@use 'sass:meta';
@use 'sass:map';

.container {
  $properties: width, color;
  $variables: meta.module-variables(foo);

  @each $property in $properties {
    #{$property}: map.get($variables, $property);
  }
}

今まで属性の名前がハードコードになっていた部分が配列と繰り返しになり、エレガントな記述になりました。この例では最小の実装なのであまりメリットを感じられませんが、コードの規模が大きくなるにしたがって効いてくる書き方です。

メタプログラミング tips 2. 動的に関数を呼び出す

変数を動的に取得するのと同じような方法で関数を動的に呼び出すこともできます。ですが、まずは @use ルールを使った標準的な関数の呼び出しを書いてみましょう。次のように記述した参照用の _bar.scss を用意します。

@function get-width() {
  @return 100%;
}

@function get-color() {
  @return #333;
}

@function sum($a, $b) {
  @return $a + $b;
}

この例では前述した tips と同じ結果を得るために、何も処理をせずに値を返すだけの get-width, get-color 関数と、引数を取って足し合わせた結果を返す sum 関数を定義しました。 index.scss では先ほどの例を関数を使って実現するバージョンにしてみましょう。

@use 'bar';

.container {
  width: bar.get-width();
  color: bar.get-color();
}

.dialog {
  z-index: bar.sum(2, 3);
}

z-index は sum 関数に引数を渡すテストのために作っています。それではまず、 container クラスを tips 1 と同じように配列と繰り返しを使うものに変更してみましょう。

@use 'bar';

.container {
  $properties: width, color;

  @each $property in $properties {
    $function-name: get-#{$property};
    #{$property}: 0;  // 動的に function-name の関数を呼び出したい
  }
}

meta.module-variables 関数と似ている meta.module-functions 関数 が定義されているので、これでモジュールに定義されている関数を map の形で取得できます。 map.get 関数を使うことで個別の関数を取り出せるので、取り出した個別の関数に meta.call 関数 を組み合わせることで実際に結果を得られるようになります。では、書き直してみましょう。

@use 'bar';
@use 'sass:meta';
@use 'sass:map';

.container {
  $properties: width, color;
  $functions: meta.module-functions(bar);

  @each $property in $properties {
    $function-name: get-#{$property};
    $function: map.get($functions, $function-name);
    #{$property}: meta.call($function);
  }
}

sum 関数のように引数を渡したいときは、 meta.call 関数に追加で引数を渡すことで実現できます。つまり、 dialog クラスのコードを実現したい場合には meta.call(map.get($functions, sum), 2, 3) とすれば良いですね。

拙作の Sass ユニットテストツールである @gyugyu/sassunit でもこの書き方が使われています。 @gyugyu/sassunit は Inhouse をテストするためにも使われています。 Sass が書いたとおりに動くか不安のある方や Sass のユニットテストに興味がある方はぜひ一緒にやっていきましょう。

おわりに - デザインエンジニアリングの可能性

ここ数年で、リッチなフロントエンド開発がより良いユーザ体験のためにも必須であると言われるようになりました。より良いユーザ体験をもたらすものである以上、デザインするという作業とは不可分と言えるでしょう。つまり、デザイナーが頭を働かせるべき場面がそれだけ増えているということでもあります。

デザイナーが考えるべきことが増加したという内訳には、デザイナーが直接フロントエンド開発を行うというものもあれば、オブジェクト指向 UI デザインを学ぶといった思考のレベルでのアップデートもあります。 2020 年にペパボ社内では『オブジェクト指向 UI デザイン』の読書会が行われ、エンジニア・デザイナーの垣根を越えて参加者が集まりました。

そして、考えるべきことが増えるということは、今までのやり方を続けていくだけでは時間が足りなくなってしまうということです。そのリソースの問題を解決するのがエンジニアリングや仕組み化・自動化の考え方で、以前の作業を省力的に済ませることによって、本当に創造的なことに集中できるようになるでしょう。メタプログラミング Sass のようなメタな考え方は、その効率化を実現する要素と言うこともできます。

創造性をエンジニアリングでブーストするところにこそ、デザインエンジニアリングの可能性はあるのです。デザインエンジニアリングに興味がある方はぜひお声がけください。