新卒入社1ヶ月目が考えるWebフロントのコツ

Yoichiro Tanaka
nextbeat-engineering
20 min readMay 17, 2024

UIコンポーネントの構造・ドキュメント・変数

この記事では、私がこれまでにVue、React、Angular、Svelteなどのフレームワークを触って学んだことから、基本的なフロントエンドの設計指針についてまとめました。想定しているアプリケーションは8画面程度以上の中規模なものです。小規模なアプリケーションに関しては、特に設計を考えずに作成する方が早い場合もありますし、50〜100画面を超える大規模なアプリケーションの場合は、レイヤーを分けるだけでなく縦に分割する必要が出てくると思います。

この記事はあくまで私個人の考えに基づいていますが、参考にしていただければ幸いです。

記事について

本記事は、個人ブログで公開した内容を基にしています。個人ブログのURLは次の通りです。 個人ブログのURL

スタイル編

リセットCSSを使用する

リセットCSSは、Safari、Chrome、Firefoxなどのブラウザで異なる初期スタイルを統一するためのCSSです。私は以下のリセットCSSを使用することを推奨します。このリセットCSSは全ての初期スタイルを削除し、デバイス間のスタイルの差異を気にせずに開発を進めることができます。

リセットCSSのリンク

このスタイルをグローバルにインポートすることで、デバイス間のスタイル差異を気にしなくても良くなります。一方で、これ以外にグローバルにインポートするCSSは要素に適用されているスタイルの原因を追いにくくなるため、あまりおすすめしません。私がこのリセットCSSを好む理由は、全ての初期スタイルを削除することで、適用されているCSSが明示的に指定されたものであり、どこで設定されたかを追いやすくなるためです。

色の変数を設定する

デザインにおいて、色が少ないことはシンプルさを感じさせる重要な要素の1つです。そのため、使用する色を事前に定義しておくことをおすすめします。

具体的には、以下のような色の変数をデザインタイプごとに作成すると良いでしょう。(黒地のエリア用の変数や白地のエリア用の変数など、スタイルの異なる場所ごとに変数を作成します)すべての変数を埋める必要はなく、使用するものだけを定義すれば十分です。

上記それぞれの色には on 〇〇 color, on 〇〇 light color, on 〇〇 heavy color といった変数があります。デザインの際には、Figmaで同じ名前で色の変数定義を行っておくことをおすすめします。

文字列のコンポーネントを作成する

文字列に関しては変数定義だけでなくコンポーネント化することがおすすめです。(CSS変数もinput要素のplaceholderなどではテキストコンポーネントが使用できずCSSで指定するため必要です)

以下の種別のテキストコンポーネントを作成することをおすすめします。propsで種別を変更するか、コンポーネント単位で分けるかはどちらでも良いと思いますが、個人的にはJavaScriptで処理させないコンポーネントから分ける方が好きです。

上記それぞれのフォントには、強調表示用のstrongタイプも作成します。アクセシビリティの観点から、本当にstrongタグを使用するか、スタイルだけで強調するかは別途検討する必要があります。もちろん、デザインによってフォントの種類が少なくても多くなっても構いません。フォントに凝ることでデザインがより良く見えることがあります。

UIライブラリの意図されないカスタマイズをしない

Material UIやIonicなど、さまざまなUIライブラリがありますが、それらをそのまま使用すると、どこかで見たことがあるようなデザインになりがちです。サイト特有のオリジナリティが出せない、あるいはスタイリングに時間をかけられないといった理由から、既存のUIライブラリを使用し、グローバルCSSでカスタマイズすることがあります。

<!-- app.component.html -->
<button mat-button>Custom Button</button>
/* styles.scss */
button::ng-deep .mat-button-wrapper {
border-radius: 50%;
color: pink;
padding: 0;
}

しかし、これは推奨されません。CSSの追跡が難しくなるだけでなく、UIライブラリのバージョンアップによりクラス名が変更されると、UIが崩れてしまうことがあるからです。UIライブラリは提供されたカスタムAPIのみを使用するか、自分で実装することを検討しましょう。最近では、HeadlessUIのように、スタイリングは自分で行いながらも、アクセシビリティを考慮しやすいライブラリもあります。

UIライブラリをWrapする

これは通常のライブラリでも言えることですが、長期的な運用を見込む場合は、UIライブラリをWrapした自作コンポーネントを作成することをおすすめします。UIライブラリのアップデートによってインターフェイスが変更されても、修正箇所が少なくて済みます。また、UIが崩れた場合にも、他のライブラリや自作のものに置き換えるのが容易になります。さらに、UIライブラリに対するカスタマイズをサービス内で統一しやすくなり、TypeScriptで型が付与されていないUIライブラリでも、Wrapすることで自分で型を付けることができます。

コンポーネントの幅と高さの指定

コンポーネント内で幅(width)と高さ(height)を決め打ちすることをおすすめします。コンポーネントは、どこで使用されても崩れない安定した振る舞いを保つべきだからです。そのため、リセットCSSなどで要素のデフォルトスタイルを box-sizing: border-box にすることを推奨します。これにより、要素のサイズによって広がって欲しい場合には明示的に指定されるため、コンポーネントが使用側の状況で変化することが分かりやすくなります。また、隣接するコンポーネントや親コンポーネントの幅や高さの値を極力使用しないようにしてください。コンポーネント内で完結していないと、どこからその値が出てきたのか追えなくなり、コンポーネントの使用側の状態でUIが崩れてしまいます。

コンポーネント外に影響するmarginを避ける

marginは配置を調節するのに便利ですが、UIコンポーネント外に影響を及ぼすmarginを持っていると、親コンポーネントからの配置調整が非常に難しくなります。コンポーネント内で影響を閉じるために、marginがUIコンポーネント外に出ないようにしてください。

UIコンポーネント編

UIコンポーネント名のclass名を付与する

フレームワークにもよりますが、変更時やデバッグ時にブラウザのインスペクタから影響を受ける要素がどのコンポーネントであるかをわかりやすくするために、UIコンポーネントの最も外側の要素にはUIコンポーネント名のclass名を付与しておくことをおすすめします。これにより、インスペクタで特定した要素がどのコンポーネントであるかを素早く確認することができます。

import React, { FC } from 'react';
import './HamburgerMenuButton.scss';

const HamburgerMenuButton: FC = () => {
return (
<button className="hamburger-menu-button">
<span className="line"></span>
<span className="line"></span>
<span className="line"></span>
</button>
);
};

export default HamburgerMenuButton;

ドキュメントを書く

コードの関数にはJSDocやTSDocを記述することが多いと思いますが、UIコンポーネントにもコード内にドキュメントを記述していますか? ブラウザのインスペクタからUIコンポーネントを辿ることはできても、その逆は難しいことはありませんか? UIコンポーネントは再利用性の高いAPIであることが多いです。その際に、UIコンポーネント自体の説明や使用方法、注意点などを書いていないと、再利用や変更の際にUIコンポーネントを読み解かなければなりません。関数だけでなく、UIコンポーネント自体に対するコード内ドキュメントを書くことをおすすめします。

import { Component, EventEmitter, Input, Output } from '@angular/core';

/**
* 押下でオン・オフを切り替えるトグルボタン
*
* @example
* <app-toggle-button [(value)]="myValue" (toggle)="handleToggle($event)"></app-toggle-button>
*/
@Component({
selector: 'app-toggle-button',
template: './app-toggle-button.component.html',
styles: ['./app-toggle-button.component.scss'],
})
export class ToggleButtonComponent {
/** トグルボタンの現在のオン・オフ状態 */
@Input() value = false;

/** トグルボタン押下時に発火 */
@Output() valueChange = new EventEmitter<boolean>();
}

スクリプト編

コンポーネントレイヤーを3つに分ける

ヘキサゴナルアーキテクチャのように、Webフロントエンドもレイヤーで境界線を分けることによって扱いやすくします。

  • 共通UIコンポーネント層
  • 機能単位UIコンポーネント層
  • ページ単位UIコンポーネント層

基本的にディレクトリもこの3つで分けるのがおすすめです。

共通UIコンポーネント層では、ボタンやテキストフィールド、モーダルなどドメインに関わらないものを配置します。理想的には機能単位UIコンポーネント層やページ単位コンポーネント層にはHTMLタグがほとんどなく、共通UIコンポーネント層のコンポーネントだけで構成されるのが良いです。この層では状態を明示的に持たないようにしてください。テキスト入力欄であっても、状態自体は上位コンポーネントに任せるようにしてください。

機能単位UIコンポーネント層では、例えばユーザプロフィール変更フォームや通知一覧リストなど、特定のドメインと直結したコンポーネントを配置します。このレイヤーでは短時間であれば状態を保っても構いません。例えばモーダルを開いている間の入力情報を保持したり、フォームの内容を保持する場合などです。ただし、長期間保持する必要があるのであれば、ページ単位UIコンポーネント層、またはページ単位UIコンポーネント層経由で状態管理ライブラリに状態を持たせるようにしてください。

ページ単位UIコンポーネント層は、ユーザーが見えている画面に対応するコンポーネントを配置します。この層では主にスマートフォン用画面とPC用画面を切り替えたり、その画面の最初に閲覧した際にだけロードする値を保持します。また、状態管理ライブラリから状態を受け取ることも行います。基本的に大まかな配置以外のスタイリングは行いません。

レスポンシブデザインの工夫

ブログ記事などの静的サイトを作成する場合は、CSSでメディアクエリを使ってレスポンシブデザインを実現するのがおすすめですが、Webアプリケーションなどの動的サイトでレスポンシブデザインをする場合、CSSでのメディアクエリが過剰に複雑になることがあります。

おすすめは、ページ単位UIコンポーネントと機能単位UIコンポーネントの中間層でスマートフォン用とPC用の配置コンポーネントの2つを作成し、ページ単位UIコンポーネントで両方を表示することです。このとき、JavaScriptでデバイス情報を読み取ってコンポーネントを切り替えると、SSR(サーバーサイドレンダリング)などを行った際に一瞬スマートフォン用(またはPC用)の画面が表示されてしまったり、レンダリングが一瞬遅れることがあります。そのため、JavaScriptではマウント時にどちらかのコンポーネントを削除し、CSSのメディアクエリで表示する方を決めることで、一瞬のデバイス差が気になりにくくなります。

<script lang="ts">
import Phone from './Phone.svelte';
import PC from './PC.svelte';

let onPhone: boolean | undefined = undefined;

onMount(() => {
onePhone = window.innerWidth < 768
});
</script>

{#if onPhone !== true}
<div class="pc-container">
<PC />
</div>
{/if}

{#if onPhone !== false}
<div class="phone-container">
<Phone />
</div>
{/if}

<style>
.pc-container {
display: none;
}

@media (min-width: 769px) {
.pc-container {
display: block;
}
.phone-container {
display: none;
}
}
</style>

状態管理ライブラリの活用

フロントエンドに限らず、状態が存在するとプログラミングは非常に複雑になります。Web APIサーバーで状態の複雑さが気になりづらいのは、Databaseという優れたソフトウェアに状態の複雑さを閉じ込めているからです。フロントエンドでも、状態管理ライブラリに状態の管理を任せることで、複雑さを軽減することができます。この際に、すべての状態を状態管理ライブラリに保存してしまうと、無意味に状態にアクセスできる範囲が広がってしまいます。そのページを表示する際に1度だけ読み込むものに関しては、状態管理ライブラリではなく、ページ単位管理UIコンポーネントに状態を持たせるようにしましょう。

クラス指向ではなくデータ指向

オブジェクト指向パラダイムに親しみのある方は、フロントエンドでも値に対してメソッドを持たせるためにクラスを作成することが多いかもしれません。しかし、これはあまり推奨できません。なぜなら、UIフレームワークやUIライブラリと自作インスタンスは非常に相性が悪いからです。TypeScript自体がクラスと相性がさほど良くないというのもありますが、UIライブラリを使用する際に毎回インスタンスとの変換を行うのは大変で、利益が少ないです。フロントエンドで扱うロジックは値とあまり結びついていないことも多いです。

私は、オブジェクト指向ではなくデータ指向を推奨します(オブジェクト指向とデータ指向が相反するわけではありません)。ここでいうデータ指向は、書籍「データ指向プログラミング」で紹介されている考え方です。データに対するロジックを関数で記述し、それに対してフロントエンドでも単体テストを記述します。

値は不変にする

基本的にフロントエンドでは、親コンポーネントから渡ってくるオブジェクトを受け取って表示するだけです。そのため、データをいじる必要はほとんどありません。自然と値は不変になるはずです。プロパティをreadonlyとして定義する必要があるかは賛否両論ですが、オブジェクトは不変なものとして扱うようにしてください。

APIサーバへのアクセス

APIサーバへのアクセスは、どこからでも発生する可能性があります。そのため、APIに破壊的変更が入ると影響範囲が大きくなりがちです。APIサーバへは直接 fetch を行うのではなく、何かしらの関数やクライアントでwrapすることをおすすめします。例えば、astahmer/openapi-zod-clientという、OpenAPIスキーマからクライアントを生成するライブラリがあります。基本的なJSONでの受け渡しのみを行う場合は、こちらを基本的に使用するのがおすすめです。ただし、ファイルなどの複雑な形式での通信や、SvelteKit特有のfetch(拡張もありますが)など使用できない場面もあります。

propsでは不変なオブジェクトで受け渡す

コンポーネント間ではバケツリレーが多く発生すると思います。その際に複数のpropsを一緒に受け渡すと、バケツリレーのコストが大きくなります。propsではプリミティブな型だけでなく、不変なオブジェクトも受け渡すことができます(この際にmutableなオブジェクトを渡すとバグが発生しやすくなります)。機能単位UIコンポーネント層以上では、ドメイン知識に沿ったオブジェクトの型を作成し、一気に受け渡しを行いましょう。共通UIコンポーネント層ではプリミティブ型で受け渡しを行うことをおすすめします。

バケツリレーは我慢

階層が増えてくるとpropsのバケツリレーが増え、状態管理ライブラリから直接取ってきたいという欲求が出てくるかもしれません。しかし、それをしてしまうと特定機能UIコンポーネント層の再利用が難しくなります。また、データの加工ロジックが重複する場合もありますが、それはUIコンポーネントから切り離して考えるべきです。最近ではSvelteやVue 3.3などでバケツリレーを簡単にする仕組みが登場しています。受け渡しをオブジェクトにまとめるなどして軽減できない場合は、バケツリレーを受け入れましょう。

外部APIからのデータの流れを一方向にする

基本的にページ単位UIコンポーネント以外では副作用を持たないことが重要です。副作用を持つと再利用性が失われ、子コンポーネントによる影響を考慮しなければならなくなり、親コンポーネントから扱いにくくなります。そのため、外部APIからのデータは状態管理ライブラリを経由して、上から下に流れるようにします。これにより、状態は状態管理ライブラリから上位階層から下位階層へと流れ、副作用をなくすことができます。

変数名はわかりやすく

イベントハンドラー関数名を onClick() などと命名することはありませんか? on:click="onClick" では、コードを読んだ際にクリック時に何が行われるのかが実装を見るまでわかりません。例えば、on:click="openModal" であれば、クリック時に何が実行されるのかが一目瞭然です。ハンドラや変数の命名をpropsの名前にしないようにしましょう。例外として、バケツリレーで親から受け取った値を子コンポーネントに流す場合は、そのままで構いません。

クラスの付け替えとスタイルの管理

スタイルをJavaScript側で変更する方法は各フレームワークに存在すると思いますが、大部分のスタイルをJavaScriptで定義するのは避けましょう。少しUIを修正したいだけなのにJavaScriptのロジックを読む必要が出てしまいます。基本的には、JavaScriptではクラスの付け替えを担当し、CSSでUI定義を行うようにします。例外として、棒グラフなどのように数値で長さを決める必要がある場合には、JavaScriptで操作する部分だけをJavaScriptで管理します。

エラーハンドリングの方法

エラーハンドリングの方式としては、大きく3つの方法が考えられます。Result型(Either型)をライブラリやユニオン型で定義して伝播させる方法、try-catchを使用する方法、発生箇所で対処する方法です。私の考えでは、フロントエンドでは外部API由来のエラーが多く、それらは防ぐことよりもスタックトレースで確認できることが重要だと思います。そのため、Result型のようにスタックトレースを取りにくいものは避け、throwを優先します。また、回復可能なエラーについてもスタックトレースを取得できるようにエラーを投げ、その旨をTSDocのthrowとして記述し、伝播させることをおすすめします。

UI崩れ防止とユーザーテスト

PlaywrightやCypressなどのE2Eテストフレームワークには、VRT(Visual Regression Testing)機能も含まれていることが多いですが、VRTとE2Eを同時に行うことはおすすめできません。VRTは実装後にしか作成できないため、テストは基本的に実装前に記述すべきです。E2Eテストを先に記述し、実装後に退行を防ぐためのVRTを作成します。また、VRTとE2Eでは目的が異なるため、役割に応じて別々に行うことが重要です。

最後に

この記事では、新卒入社1ヶ月目の私が考えるWebフロントエンドのコツや基本設計指針についてまとめました。フロントエンド開発は常に進化しており、新しい技術やベストプラクティスが次々と登場しています。そのため、今回ご紹介した内容も今後変わる可能性があります。

初めてフロントエンド開発に取り組む方や、これからもっと深く学びたいと考えている方にとって、少しでも役立つ情報となれば幸いです。

最後までお読みいただき、ありがとうございました。今後もこのブログを通じて知見を共有していきたいと思いますので、どうぞよろしくお願いします。

We are hiring!
本記事をご覧いただき、ネクストビートの技術や組織についてもっと話を聞いてみたいと思われた方、カジュアルにお話しませんか?

・今後のキャリアについて悩んでいる
・記事だけでなく、より詳しい内容について知りたい
・実際に働いている人の声を聴いてみたい

など、まだ転職を決められていない方でも、ネクストビートに少しでもご興味をお持ちいただけましたら、ぜひカジュアルにお話しましょう!

🔽申し込みはこちら
https://hrmos.co/pages/nextbeat/jobs/1000008

また、ネクストビートについてはこちらもご覧ください。

🔽エントランスブック
https://note.nextbeat.co.jp/n/nd6f64ba9b8dc

--

--