クラスとコンポーネントの違い
次にバックエンドとフロントエンドのアプリケーションの役割の違いを見ていきましょう。バックエンドのアプリケーションでは、Serviceクラスが他のオブジェクトを呼び出して特定の処理を実行します。ドメインロジックは主に呼び出される側のEntityやValue Objectと呼ばれるオブジェクトに記述します。主にサービス層にはアプリケーションロジックやビジネスロジックを、ドメイン層にはドメインルールを書き込みます。これによりバックエンドがデータの整合性を担保することを目指します。データの整合性の担保がバックエンドのアプリケーションの主な役割です。
先ほどの記事クラスを例に挙げると「記事がアーカイブされていたらタイトルを編集できない」という仕様があった場合、それを実現するバリデーションが大切なロジックの一つと言えるでしょう。
editTitle(newTitle: Title) {
if (this.status === Status.Archived) {
throw new Error('Archived な記事のタイトルは編集できません');
}
this.title = newTitle;
}
フロントエンドの主な役割は次の2つに分けられます。1つ目は、ユーザーインターフェースの表示を制御したり、見た目や構造を提供したりするプレゼンテーションロジックです。2つ目は、ユーザーの操作に応じた動作を実現するイベントハンドラのロジックを構築することです。
フロントエンドでは親コンポーネントが子コンポーネントを呼び出すことでUIツリーを構築します。Reactのようなコンポーネント指向の宣言的なUIライブラリが登場するまでは、jQueryを使ってグローバルで手続的なロジックを記述していました。
しかし、Reactではコンポーネント単位でロジックを持つことができます。ReactコンポーネントはHTML、CSS、JSをカプセル化可能にしたものと言って良いでしょう。
まずはプレゼンテーションロジックの例を紹介します。プレゼンテーションロジックの一つは、ユーザーのためにデータを加工して表示するロジックです。例えば、上記の記事コンポーネントでは2024/12/31と表記しました。しかし、YouTubeの動画一覧で各動画の公開日は1日前や1年前のように相対的な日付で表示されています。これを記事コンポーネントで実装してみます。
// 相対時間を計算する関数
const calculateRelativeTime = (dateString: string) => {
const daysDifference = Math.floor(
(new Date().getTime() - new Date(dateString).getTime()) /
(1000 * 60 * 60 * 24)
);
if (daysDifference === 0) {
return '今日';
} else if (daysDifference === 1) {
return '昨日';
} else if (daysDifference > 1) {
return `${daysDifference}日前`;
} else {
return `${Math.abs(daysDifference)}日後`;
}
};
function Article() {
const publishedAt = '2024/11/30';
const relativePublishedAt = calculateRelativeTime(publishedAt); // 日付を相対時間に変更する
return (
<article>
<header>
<h2>記事タイトル</h2>
<p>著者: @Panda_Program(公開日: {relativePublishedAt})</p>
</header>
<p>本文です</p>
</article>
);
}

これがプレゼンテーションロジックの例です。古くはMVCフレームワークのViewファイルに直接書かれているほか、ViewModelの中に書かれていたロジックです。場合によってはAPIサーバーの中に書くこともあるかもしれません。しかし、現在このようなプレゼンテーションロジックは要求の変化に柔軟に対応するために、フロントエンドのアプリケーションで記述することがほとんどです。
次にイベントハンドラのロジックの例を紹介します。イベントハンドラは、ユーザーがブラウザで発火させたイベントに応じて何らかの処理を実行するものです。
例えば、スマホでWebサイトを見た時を想像してください。画面幅が狭いため、サイトのメニューが隠れているとします。メニューの開くボタンを押したらメニューが横から現れ、閉じるボタンを押せばメニューが画面から消えるという処理がイベントハンドラの例です。ボタンを押すことでイベントが発火し、そのイベントに応じてメニューが開閉するのがイベントハンドラの役割です。
より複雑な例を挙げると、フォームに値を入力しているときに「パスワードは8文字以上で入力してください」というバリデーションエラーをリアルタイムで表示することや、フォームの「送信する」ボタンを押したときにバックエンドにPOSTやPUTリクエストを送るといった非同期処理をすることもイベントハンドラの例です。
以下はいいねボタンをクリックすると、APIサーバーにPOSTリクエストをするイベントハンドラの例です。
function Article() {
// POST リクエストを送り、いいねの数を増やすイベントハンドラ
const handleClick = async () => {
await fetch('https://example.com/articles/100/favorite', {
method: 'POST',
});
};
return (
<div>
{/* ... → 他のコード */}
{/* ボタンをクリックするとイベントハンドラが動作する */}
<button type="button" onClick={handleClick}>
いいね👍
</button>
</div>
);
}
このように、フロントエンドの担当はプレゼンテーションロジックとイベントハンドラのロジックを管理することで、ユーザーがソフトウェアを使えるようにするためのUIを構築することです。
まとめ
本記事ではオブジェクト指向のクラスとコンポーネント指向のReactコンポーネントを比較して説明してきました。当然ですがReactコンポーネントとオブジェクト指向のクラスは異なるものであるため、本記事の内容はやや牽強付会だと思われるかもしれません。
ただし、この連載はバックエンドエンジニアの方がフロントエンドを学ぶための一時的な補助輪です。最終的にはフロントエンドの正しいメンタルモデルを身につけるために、ぜひReactのチュートリアルを試してみてください。
なお、この連載は野心的な取り組みであり、私自身も考えを深めたいため、記事内容に違和感やご意見がある方はぜひ私のXのアカウントにご連絡ください。今後の参考とさせて頂きます。
次回はReactの状態管理と関数コンポーネントについて学びます。
コード全文
class ArticleId {
constructor(public readonly id: number) {}
}
class Author {
constructor(public readonly author: string) {}
}
class Title {
constructor(public readonly title: string) {}
}
class Content {
constructor(public readonly title: string) {}
}
const Status = {
Published: 'published',
Archived: 'archived',
}
type StatusValue = (typeof Status)[keyof typeof Status]
class Article {
constructor(
public readonly articleId: ArticleId,
private title: Title,
private content: Content,
public readonly author: Author,
private status: StatusValue,
public readonly publishedAt: Date
) {}
static publish(articleId: ArticleId, title: Title, content: Content, author: Author): Article {
return new Article(articleId, title, content, author, Status.Published, new Date())
}
editTitle(newTitle: Title) {
if (this.status === Status.Archived) {
throw new Error('Archived な記事のタイトルは編集できません')
}
this.title = newTitle
}
getTitle(): Title {
return this.title
}
editContent(newContent: Content) {
this.content = newContent
}
getContent(): Content {
return this.content
}
archive() {
this.status = Status.Archived
}
}
