最近のウェブアプリは、以前に比べて機能が充実し、複雑になっています。そのため、信頼できるアプリを開発するためには、適切な設計の考え方が必要です。
ここでは、モダンなウェブアプリを開発するための5つの設計の考え方を紹介します。
1 モジュール化
モジュール化とは、アプリを小さな部品(モジュール)に分割することです。モジュール化を行うことで、以下のメリットがあります。
コードが読みやすくなり、理解しやすくなる
問題が発生したときに原因を特定しやすくなる
機能の追加や変更がしやすくなる
モジュール化を行う際は、以下のようなポイントを押さえましょう。
- モジュールの単位は、小さく単純なものにする
- モジュールの役割や責任を明確にする
- モジュール間の依存関係を少なくする
1-2 モジュール化のサンプル
モジュール化の概念を具体的なコードの例で説明します。
ここでは、簡単なユーザー管理システムを想定し、ユーザーの情報を扱うUserクラスとユーザー情報の検証を行うValidatorクラスをモジュールとして分割します。
以下に、その二つのモジュール(User.js と Validator.js)とそれらを使用するメインのアプリケーションコード (app.js) の例を示します。
User.js
// User.js
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
// ユーザーの情報を表示する
getUserInfo() {
return `名前: ${this.name}, 年齢: ${this.age}`;
}
}
export default User;
このUserクラスは、ユーザー固有の情報(ここでは名前と年齢)を保持し、それに関連する機能(この場合は情報の取得)を提供します。
Validator.js
// Validator.js
export const validateUser = (user) => {
// ユーザーの名前と年齢が適切かどうかを検証する
if (!user.name || user.name === '') {
return '名前が無効です。';
}
if (!user.age || user.age < 0) {
return '年齢が無効です。';
}
return '検証が完了しました。';
}
validateUser関数は、ユーザーオブジェクトが有効なデータであるかどうかを検証する責任を持っています。
ユーザーの名前と年齢が適切に設定されているかをチェックします。
app.js
// app.js
import User from './User.js';
import { validateUser } from './Validator.js';
// 新しいユーザーを作成する
const newUser = new User('山田太郎', 30);
// ユーザーを検証する
const validationMessage = validateUser(newUser);
console.log(validationMessage);
// ユーザーの情報を取得して表示する
console.log(newUser.getUserInfo());
このメインのアプリケーション (app.js) では、UserクラスとvalidateUser関数をインポートして、それらを利用しています。
新しいユーザーを作成し、検証し、情報を表示するという具体的な手順をこのファイルに記述しています。
このように各モジュールは、明確な責任と役割を持っており、モジュール間の依存関係が最小限になるように設計されています。
それぞれのモジュールが別々のファイルとして存在し、それらを必要に応じてメインのアプリケーションコードでインポートして使うことで、コンポーネントを再利用しやすく、コードベースをよりメンテナンスしやすいものにすることができます。
2. コンポーネント化
コンポーネント化とは、画面を小さな部品(コンポーネント)で構成することです。コンポーネント化を行うことで、以下のメリットがあります。
- 画面の変更がしやすくなる
- コンポーネントを再利用できる
- テストしやすくなる
コンポーネント化を行う際は、以下のようなポイントを押さえましょう。
- コンポーネントは、小さく単純なものにする
- コンポーネントの役割や責任を明確にする
- コンポーネント間の依存関係を少なくする
2-2 コンポーネント化のサンプル
コンポーネント化の例として、Reactを使用し、ユーザープロファイルカードを表示する小さなコンポーネントを作成する方法を説明します。
この例では、UserProfile コンポーネントを基本として、さらに細分化したUserAvatarとUserInfoコンポーネントに分けてみます。
UserAvatar.js
// UserAvatar.js
const UserAvatar = ({ src, alt }) => {
return (
<img src={src} alt={alt} className="user-avatar" />
);
};
export default UserAvatar;
ユーザーのアバターを表示するコンポーネントです。
UserInfo.js
ユーザーの名前と詳細情報を表示するコンポーネントです。
// UserInfo.js
const UserInfo = ({ name, description }) => {
return (
<div className="user-info">
<h2>{name}</h2>
<p>{description}</p>
</div>
);
};
export default UserInfo;
UserProfile.js
ユーザープロファイルカード全体を構成する親コンポーネントです。
先ほど作成したUserAvatarとUserInfoをここで使用します。
// UserProfile.js
import UserAvatar from './UserAvatar';
import UserInfo from './UserInfo';
const UserProfile = ({ user }) => {
return (
<div className="user-profile">
<UserAvatar src={user.avatarUrl} alt={user.name} />
<UserInfo name={user.name} description={user.description} />
</div>
);
};
export default UserProfile;
この構造では、それぞれのコンポーネントが独立しており、それぞれが一つの役割を持つように作られています。
UserAvatarは画像表示の責任を、UserInfoはテキスト情報の表示を担い、UserProfileはこれらをまとめてプロファイルカードとしての見た目を構成します。
コンポーネント化のポイントは、「各コンポーネントが独自の責務を持ち、あるコンポーネントの変更が他のコンポーネントに広がりにくいようにする」ということです。
このアプローチにより、画面の一部を変更したいときに特定のコンポーネントだけを修正すれば良くなり、また、それぞれのコンポーネントを他の場所でも再利用しやすくなります。
テストに関しても特定の機能だけを持つコンポーネントは、その機能に特化したテストが行いやすくなります。
よって、全体の品質が維持しやすくなるのです。
3. 状態管理
状態管理とは、アプリのデータを一元的に管理することです。状態管理を行うことで、以下のメリットがあります。
- データの整合性が保たれる
- データの変更を追跡しやすくなる
- パフォーマンスが向上する
状態管理を行う際は、以下のようなポイントを押さえましょう。
- 状態の単一性を保つ
- 状態の変更を制御する
- 状態の監視を行う
3-2 状態管理のサンプル
状態管理の例を説明するために、ReactのフックであるuseStateとuseEffectを使ったシンプルなカウンターアプリのコードを考えます。
このアプリでは、ボタンをクリックすることで数値が増減し、その数値の状態が管理されていることを示します。
// Counter.js
import React, { useState, useEffect } from 'react';
const Counter = () => {
// 状態`count`の初期化と、`count`を更新するための関数`setCount`
const [count, setCount] = useState(0);
// `count`が更新されたときに実行される副作用
useEffect(() => {
document.title = `クリック数: ${count}回`; // ページタイトルを更新
}, [count]); // 依存配列内の`count`が変化したときのみ`useEffect`を実行
return (
<div>
<p>あなたは{count}回ボタンをクリックしました。</p>
<button onClick={() => setCount(count + 1)}>クリック</button>
<button onClick={() => setCount(count - 1)}>デクリメント</button>
<button onClick={() => setCount(0)}>リセット</button>
</div>
);
};
export default Counter;
このコードでは、以下の点に注意して状態管理を行います。
状態の単一性
カウンターの値countは一つの状態としてuseStateを用いて管理されています。
状態の変更を制御
setCountはcountの値を変更する唯一の方法であり、countを直接変更することはできません。
状態の監視
useEffectフックを使用することで、countの状態が更新された際に副作用(この場合はページタイトルの変更)が実行されます。
この例では、数値のカウンターがいくつかのボタンによって操作され、現在の数値が画面に表示されています。
状態countが更新されるたびに、それを反映するためにコンポーネントが再レンダリングされます。
また、useEffectを使ってcountの変更を監視し、変更があったときにドキュメントのタイトルを更新しています。 状態管理を適切に行うことで、アプリケーションの動作が予測可能になり、デバッグや機能拡張が容易になります。
また、この単純なカウンターアプリでも見られるように、コンポーネントの状態が他の部分(この場合はドキュメントのタイトル)に連動して動作することで、一貫性と凝集性のあるインタラクティブなユーザーインターフェースを作成することが可能になります。
4. テストの実施
テストとは、アプリの機能が正常に動作するかを確認することです。テストを行うことで、以下のメリットがあります。
- 問題を早期に発見できる
- 問題修正後の検証ができる
- 品質を向上させることができる
テストを行う際は、以下のようなポイントを押さえましょう。
- 単体テスト、結合テスト、統合テストなど、適切なテストを実施する
- テストツールを活用する
4-2 テストのサンプル
テストの重要な側面の一つに、コンポーネントレベルでの単体テスト(Unit Testing)があります。
React コンポーネントの単体テストの場合、慣例的に Jest というテストランナーと、Enzyme や React Testing Library といったテスティングユーティリティを使用することが多いです。
以下に、React Testing Library を使用した単純なカウンターコンポーネントのテストの例を示します。
まずは、簡略化されたカウンターコンポーネントを想定します。
// Counter.js
import React, { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span data-testid="count">{count}</span>
<button onClick={() => setCount(count + 1)}>
カウントアップ
</button>
</div>
);
}
次に、このカウンターコンポーネントのテストをReact Testing Libraryを使用して書いてみましょう。
// Counter.test.js
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import Counter from './Counter';
describe('Counter コンポーネント', () => {
test('初期表示に 0 が表示される', () => {
render(<Counter />);
expect(screen.getByTestId('count')).toHaveTextContent('0');
});
test('ボタンがクリックされた後、カウントが 1 増加する', () => {
render(<Counter />);
fireEvent.click(screen.getByText('カウントアップ'));
expect(screen.getByTestId('count')).toHaveTextContent('1');
});
});
テストの実施は以下のポイントに注意しています。
適切なテストの実施
- 初期表示テスト: コンポーネントが描画された際の初期状態を確認する。
- イベントテスト: ユーザーのアクションに対するコンポーネントの反応(この場合はボタンクリック)を検証する。
テストツールの活用
- render関数はコンポーネントを仮想DOMに描画する。
- fireEventは仮想DOM上でのユーザーイベントをシミュレートする。
- screenはテストランナー内で仮想DOMにクエリを投げてエレメントを取得するためのヘルパーオブジェクト。
これらのテストによって、コンポーネントの各機能が想定通りに動作することを保証し、バグの早期発見やリファクタリング後の検証を効率的に行うことができます。
また、コンポーネントやアプリケーションの品質を継続的に保つことができます。
5. パフォーマンスの確認
パフォーマンスとは、アプリの表示速度や反応速度のことです。
パフォーマンスを向上させることで、以下のメリットがあります。
- ユーザーがストレスを感じにくくなる
- アプリの評価が向上する
パフォーマンスを向上させるためには、以下のようなポイントを押さえましょう。
- リソースの使用量を最小限にする
- ブラウザのキャッシュを活用する
- パフォーマンス計測を行う
以上の設計の考え方を取り入れることで、信頼性と拡張性の高いウェブアプリを開発することができます。
5-2 パフォーマンスの確認のサンプル
パフォーマンスの確認を行う際には、開発者ツール、パフォーマンス監視ツール、または専門のライブラリーを使用することが一般的です。
ここでは、Google Chrome の開発者ツールを使用したパフォーマンス計測と、React アプリケーションにおけるパフォーマンスの計測について示します。
Chrome 開発者ツールを使用したパフォーマンス計測
1 Google Chrome ブラウザでアプリケーションを開きます。
2 右クリックして「検証」を選択するか、ショートカット Ctrl + Shift + I (Macの場合は Cmd + Option + I)を使用して開発者ツールを開きます。
3 「Performance」タブに移動します。
4 録画ボタンをクリックし、アプリケーションを操作します。
5 操作終了後、録画を停止し、タイムラインとパフォーマンスの詳細を分析します。
React アプリケーションにおけるパフォーマンスの計測
React では React Developer Tools の拡張機能を利用することで、コンポーネントレベルでのパフォーマンスを監視できます。
1 「React Developer Tools」をブラウザの拡張機能としてインストールします。
2 アプリケーションで特定の操作を行います。
3 React Developer Toolsの「Profiler」タブを開きます。
4 「Record」ボタンを押してからアプリケーションでのアクションを行い、録画を停止します。
5 プロファイリング結果を分析します。
React コンポーネントのレンダリングパフォーマンスの向上に役立つ具体的なコードレベルでの一例として、React.memo を使用することが挙げられます。
これは、コンポーネントが同じpropsで何度もレンダリングされるのを防ぎ、不要なレンダリングを避けるための最適化手法です。
const MyComponent = React.memo(function MyComponent(props) {
/* レンダリングに関する処理 */
});
React.memo はコンポーネントの props が変更されない限り、レンダリングをスキップし、パフォーマンスを向上させます。
また、パフォーマンス計測のためには、計測対象のアクションが実行される前と後でのリソースの使用量やレスポンスタイムを計測し、これらのデータを基に最適化を行います。
例えば、不要なJavaScriptファイルの読み込みを省略したり、画像のサイズを適切に調整するなどの対策が挙げられます。