👀

JavaScriptにおける同期処理と非同期処理の理解: ECサイトの例を交えて

2023/05/13に公開

Written with ChatGPT-4 @2023年5月13日

🐿️はじめに

プログラミングの学習において、同期処理・非同期処理は重要な概念の一つです。

しかしながら、同期処理と非同期処理の違いを深く理解するのは、初学者にとっては難易度が高いかもしれません。私自身もまだ学習途中であり、この複雑なトピックの理解を深めるための備忘録として、そして自己学習の一環としてこの記事を作成しました。

この記事では、これらの概念の理解および文章の執筆にChatGPT-4の助けを借りつつ、JavaScriptにおける同期処理と非同期処理の基本的な概念とその適用例を掘り下げています。

特に、ECサイトでの実際の使用例や、Next.js, Supabaseでの実装例を交えながら、「これらの処理方式がどのように働き、それぞれの長所と短所をどのように理解し活用できるか」 について説明します。

なお、記事の内容は私自身の理解と学習を反映したものです。万全を期していますが、誤解を招く表現や間違った情報が含まれている可能性があります。そのような場合、私と共に学んでいる読者の皆様からのフィードバックを大変歓迎いたします。

⚡同期処理と非同期処理の特徴

まずは同期処理と非同期処理の特徴をステップバイステップで記述していきます。

同期処理の手順

ステップ1: タスクを順番に実行
同期処理では、一つのタスクが完了するまで次のタスクは実行されません。つまり、タスクの処理は1つずつ順番に行われます。(このような特徴をシングルスレッドと呼びます)

ステップ2: タスクの完了を待つ
プログラムはタスクが完了するまで待ちます。例えば、データベースからのデータ取得やファイルの読み込みなど、タスクの処理が完了するまでプログラムはブロックされます。

ステップ3: 結果を受け取る
タスクが完了すると結果が返され、その後に続く処理が実行されます。

〈メリット〉

  1. コードがシンプルで理解しやすい。
  2. タスクの実行順序が明確。

〈デメリット〉

  1. タスクが完了するまでプログラムがブロックされ、リソースの利用効率が低い。
  2. タスクの完了を待つ必要があり、パフォーマンスが低下する可能性がある。

非同期処理の手順

ステップ1: タスクの並行実行
非同期処理ではタスクが完了するのを待たずに次のタスクを実行します。つまり、複数のタスクの処理が並行して行われます。

ステップ2: コールバック関数やプロミスを使用
非同期処理では、タスクの結果を受け取るためにコールバック関数やプロミス(Promise)を使用します。これにより、タスクの処理が完了した時点で結果を受け取ることができます。

ステップ3: 結果を受け取る
タスクの処理が完了すると、コールバック関数やプロミスの結果が返され、その後に続くタスクが実行されます。

〈メリット〉

  1. タスクの並行実行により、リソースの利用効率が高い。
  2. パフォーマンスが向上する可能性がある。

〈デメリット〉

  1. コードが複雑になり、理解が難しくなることがある。
  2. タスクの実行順序が不明確になることがある。

💡 同期処理と非同期処理の選択は、「パフォーマンス」と「コードの可読性・保守性」のトレードオフになります。

同期処理と非同期処理はそれぞれ異なる特徴を持ち、それによりリソースの利用効率も異なります。

同期処理では一つの処理が終わるまで次の処理は待機状態となるため、CPUの計算能力を最大限に利用することが難しく、リソースの利用効率が低くなります。

一方、非同期処理では待機時間中に他の処理を行うことが可能となるため、リソースをより効率的に利用することができます。したがって、非同期処理は特に高負荷な状況や多くのタスクを同時に行う必要がある場合に有効です。

🐲同期処理と非同期処理の機能例

ここで、同期処理と非同期処理の機能例をそれぞれ解説します。

同期処理の機能例

例: ファイルの読み込みとデータ処理

  1. プログラムは、ファイルを読み込むタスクを実行します。
  2. ファイルの読み込みが完了するまでプログラムは待ちます。
  3. ファイルの読み込みが完了したら、読み込んだデータを処理するタスクを実行します。

この例では、ファイルの読み込みが完了するまでデータ処理のタスクは実行されません。これが同期処理の典型的な動作です。

非同期処理の機能例

例: Web APIへのリクエストとデータ処理

  1. プログラムは、Web APIへのリクエストを行うタスクを実行します。
  2. リクエストが完了するのを待たずに、他のタスク(例えば、UIの更新)を実行します。
  3. Web APIからのレスポンスが返されたら、コールバック関数やプロミスを使ってデータを処理するタスクを実行します。

この例では、Web APIへのリクエストとそれ以外のタスクが並行して実行されます。これが非同期処理の典型的な動作です。

💡 同期処理は、処理が順番に行われることが重要な場合に適しています。一方、非同期処理は、待ち時間が発生するタスク(例: ネットワーク通信やデータベースアクセス)と、待ち時間を有効活用できる他のタスクを並行して実行することで、パフォーマンスを向上させることができます。

🦌JavaScriptで非同期処理を実装する際に重要となる概念

次に、JavaScriptで非同期処理を実装する際に重要となる概念を以下にリストアップします。

1. イベントループ

JavaScriptはシングルスレッドで動作するため、イベントループを使って非同期処理を行います。イベントループは、タスクキューにあるタスクを順番に実行し、完了したら次のタスクを実行する仕組みです。
加えて、JavaScriptのイベントループではマイクロタスクキュー(Promiseなどが該当)とマクロタスクキュー(setTimeoutsetIntervalなどが該当)が存在し、それらが異なる優先度で処理されることを理解することも重要です。

2. コールバック関数

コールバック関数は、他の関数に渡される関数で、非同期処理の結果が得られたときに実行されます。コールバック関数を使って非同期処理の結果を受け取り、処理を行うことができます。

3. プロミス (Promise)

プロミスは、非同期処理の結果を表すオブジェクトで、結果がまだ得られていない状態(pending)、成功した結果が得られた状態(fulfilled)、処理が失敗した状態(rejected)の3つの状態を持ちます。プロミスを使って、非同期処理の成功・失敗に応じた処理をチェーン(複数の操作や処理を連結させること)できるようになります。

4. async/await(エーシンク/アウェイト)

async/awaitは、プロミスをより直感的に扱う構文です。asyncキーワードを使って関数を宣言することで、その関数内でawaitキーワードを使ってプロミスを待機できるようになります。これにより、非同期処理を同期処理のように書くことができ、コードの可読性が向上します。
また、async/awaitではtry/catch構文を用いてエラーハンドリングを行うことが一般的です。非同期処理においては、エラーが発生した場合でも適切に対応することが重要です。

5. タイマー関数

JavaScriptでは、setTimeout()setInterval()などのタイマー関数を使って、一定時間後や一定時間ごとに関数を実行することができます。これらの関数も非同期処理を実現するために使用されます。

これらの概念を理解し、適切に使い分けることで、JavaScriptで効果的な非同期処理を実装することができます。

🧐同期処理と非同期処理はどうやって使い分けるの?ECサイトを例に

同期処理と非同期処理は、ユーザーエクスペリエンスだけでなく、パフォーマンスやサーバーへの負荷といった観点からも重要な要素です。

ここでもう一度、それぞれの基本的な定義を復習してみましょう。

次に、ECサイトの開発において、同期処理と非同期処理を使う典型的な機能をそれぞれ解説します。

〈同期処理の利用例〉

  • ページ遷移
    ユーザーが様々なページ(商品一覧、商品詳細、カート、注文確認等)を閲覧する際、通常は同期処理でページ遷移が行われます。ユーザーがリンクをクリックした時点でリクエストがサーバーに送信され、レスポンスが返ってくるまでユーザーは待つ必要があります。この間、ユーザーは新しいページが表示されるまで他の操作(例:商品をカートに追加する)を行うことは通常できません。

〈非同期処理の利用例〉

  1. 商品検索・絞り込み
    ユーザーがキーワードを入力したり、カテゴリーや価格帯などのフィルターを適用したりすると、その結果がリアルタイムで更新されます。これは処理は非同期処理により実行されます。ユーザーは検索結果が表示されるのを待っている間でも、現在のページをスワイプするなどの他の操作を続けることができます。

  2. カートへの商品追加
    ユーザーが商品をカートに追加する際、全てのページがリロードされるのではなく、カートのアイコンや数量のみが更新されます。これも非同期処理によるものです。

  3. お気に入り登録・解除
    ユーザーが商品をお気に入りに登録または解除すると、お気に入りの状態が即座に更新されます。これにより、ユーザーはページリロードなしで最新のお気に入りの状態を確認することができます。

  4. 住所・支払い方法の変更
    ユーザーが自身のプロフィール情報(例えば、住所や支払い方法)を更新する際、その変更が即座に反映されます。つまり、ユーザーはページをリロードすることなく、最新の情報を確認することができます。

これらの例は、同期処理と非同期処理がどのようにユーザーエクスペリエンスに影響を与えるかを明確に示しています。

これらの機能を非同期処理で実装することでECサイトはより使いやすくなり、ユーザーエクスペリエンスが向上します。同期処理と非同期処理を適切に使い分けることで、効率的なECサイトの開発を行うことが可能となるわけです。

🐢ハンズオン〈Next.jsとSupabaseで非同期処理を用いたお気に入り機能を実装する〉

このハンズオンでは、Next.jsSupabaseを使用してお気に入り機能を実装する手順を、ステップバイステップで解説します。ここでは、ユーザーがログインしている状態を前提として進めます。

Next.jsは、Reactを用いたサーバーサイドレンダリングや静的サイト生成が可能なフレームワークです。Supabaseは近年注目を集めているオープンソースのBaaS(Backend As A Service)で、認証やデータベースなど、バックエンドに必要な機能が提供されています。

お気に入り機能の実装

  1. APIエンドポイントの作成
    お気に入りの追加・削除を行うためのAPIエンドポイントを作成します。このエンドポイントはサーバーサイドで動作し、お気に入り情報をデータベースに保存・削除するロジックを実装します。

  2. お気に入りボタンコンポーネントの作成
    お気に入りボタンを表示するReactコンポーネントを作成します。このコンポーネントは、商品のIDやお気に入りの状態(登録済みかどうか)をpropsとして受け取ります。

  1. お気に入りボタンのクリックイベントの実装
    お気に入りボタンをクリックした際に、APIリクエストを送信してお気に入りの登録・解除を行う処理を実装します。この際、非同期処理(fetch関数やaxiosライブラリを使用)を用いてAPIリクエストを行います。

👇下記のコードは、お気に入りボタンがクリックされた際のイベントハンドラーの例です。

async function handleFavoriteClick() {
  const url = `/api/favorites/${productId}`;
  const method = isFavorited ? "DELETE" : "POST";
  const response = await fetch(url, { method });

  if (response.ok) {
    // 成功時の処理(お気に入り状態の更新)
  } else {
    // 失敗時の処理(エラーメッセージの表示)
  }
}

ここでは、お気に入りの状態(isFavorited)に応じてHTTPメソッドを選択し、APIリクエストを送信しています。リクエストが成功した場合(response.oktrueの場合)、お気に入り状態を更新します。リクエストが失敗した場合、エラーメッセージを表示します。

  1. お気に入り状態の更新
    APIリクエストが成功した場合、お気に入り状態を更新します。ここでは、Reactのステート管理ツール(useStateuseReducer)を用いて、お気に入りの状態を管理します。また、お気に入りボタンの表示を、登録済みかどうかに応じて変更します。

  2. 商品一覧・詳細ページにお気に入りボタンを追加
    商品一覧ページや商品詳細ページに、作成したお気に入りボタンコンポーネントを追加します。必要な情報(商品ID、お気に入り状態など)をpropsとして渡します。

これらの手順に従って、Next.jsを使ったお気に入り機能を実装することができます。

Supabaseの設定手順

このセクションでは、お気に入り機能のためのSupabaseのデータベースとユーザー認証機能の設定手順をステップバイステップで説明していきます。

データベースの設定

  1. Supabaseプロジェクトの作成:
    Supabase公式サイトにアクセスし、新しいプロジェクトを作成します。ここで、プロジェクト名やデータベースの設定を行います。

  2. データベーステーブルの作成:
    Supabaseのダッシュボードから、favoritesテーブルを作成します。このテーブルには、ユーザーID、商品ID、お気に入り登録日時といったカラムが含まれます。

👇下表はfavoritesテーブルの例です

カラム名 データ型 説明
id SERIAL 自動的に増加する一意のID。主キーとして機能します。
user_id INTEGER ユーザーIDを保存します。ユーザーテーブルとの外部キーとして機能します。
product_id INTEGER 商品IDを保存します。商品テーブルとの外部キーとして機能します。
created_at TIMESTAMP お気に入りとして登録された日時を保存します。デフォルトでは現在の日時。

〈補足〉SERIALはPostgreSQLで頻繁に使われる自動増加整数型で、TIMESTAMPは日付と時間の情報を含むデータ型です。INTEGERは整数を表しています。

  1. 認証システムの設定:
    Supabaseには独自の認証システムが組み込まれています。ダッシュボードの「Auth」セクションで、認証プロバイダ(メールアドレスとパスワード、ソーシャルログインなど)やセキュリティ設定を行います。

<-- より詳細な説明を追記予定です @6/15 -->

  1. Next.jsプロジェクトにSupabaseクライアントの追加:
    supabase-jsライブラリをインストールし、Supabaseクライアントを設定します。APIキーとSupabaseのURLを環境変数に設定して、プロジェクトで利用できるようにします。
npm install @supabase/supabase-js

その後、以下のようにSupabaseクライアントを設定します。

lib/supabase.js
import { createClient } from "@supabase/supabase-js";

const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_KEY;

const supabase = createClient(supabaseUrl, supabaseKey);

export default supabase;

これらの手順に従うことで、Next.jsに加えてSupabaseのデータベースを用いたお気に入り機能の実装が可能になります。

認証機能の設定

  1. 認証機能の実装:
    Supabaseクライアントを活用して、ログイン、ログアウト、サインアップなどの認証機能をNext.jsプロジェクトに追加します。
  1. お気に入り機能のAPIエンドポイントで認証を行う:
    APIエンドポイントでユーザーが認証済みかを確認します。認証済みのユーザーのみがお気に入りの追加・削除を行うことができます。

👇以下にNext.jsのAPIルートにおけるお気に入り機能のエンドポイントの実装例を示します。

pages/api/favorites/[productId].js
// supabaseのライブラリをインポート
import supabase from "../../../lib/supabase";

// ハンドラ関数を定義
export default async function handler(req, res) {
  // ユーザー情報をクッキーから取得
  const { user } = await supabase.auth.api.getUserByCookie(req);

  // ユーザーが存在しない場合はエラーレスポンスを返す
	if (!user) {
	  return res.status(401).json({ error: "Unauthorized" });
	}

	// リクエストから商品IDを取得
	const productId = req.query.productId;

	// リクエストメソッドによって処理を分ける
	switch (req.method) {
		case "POST":
		// お気に入りを追加
		try {
		  const { data, error } = await supabase
		    .from("favorites")
		    .insert([{ user_id: user.id, product_id: productId }]);
		  
		  // エラーがある場合は例外をスロー
		  if (error) {
		    throw error;
		  }
		
		  // 正常終了したらデータをレスポンスとして返す
		  res.status(200).json(data[0]);
		} catch (error) {
		  // 例外が発生したら500エラーを返す
		  res.status(500).json({ error: error.message });
		}
		break;

		case "DELETE":
		// お気に入りを削除
		try {
		  const { error } = await supabase
		    .from("favorites")
		    .delete()
		    .match({ user_id: user.id, product_id: productId });

		  // エラーがある場合は例外をスロー
		  if (error) {
		    throw error;
		  }
		
		  // 正常終了したら成功メッセージをレスポンスとして返す
		  res.status(200).json({ message: "Favorite removed" });
		} catch (error) {
		  // 例外が発生したら500エラーを返す
		  res.status(500).json({ error: error.message });
		}
		break;

		default:
		// 対応していないリクエストメソッドの場合は405エラーを返す
		res.setHeader("Allow", ["POST", "DELETE"]);
		res.status(405).end(`Method ${req.method} Not Allowed`);
	}
}

上記のコードはNext.jsのAPIルートにおけるお気に入り機能のエンドポイントの実装例です。

認証済みのユーザーはこのエンドポイントを通じて商品のお気に入りの追加(POSTメソッド)または削除(DELETEメソッド)が可能です。

具体的な処理の流れは次の通りです:

  1. ユーザー認証
    supabase.auth.api.getUserByCookie(req)を使用してユーザーの認証状態を確認します。
  2. 商品IDの取得
    req.query.productIdを使用してリクエストから商品のIDを取得します。
  3. リクエストメソッドの確認
    req.methodに基づいてお気に入りの追加または削除の処理を行います。
  4. お気に入りの追加
    "POST"メソッドがリクエストされた場合、Supabaseのinsertメソッドを使ってお気に入り テーブルにレコードを追加します。
  5. お気に入りの削除
    "DELETE"メソッドがリクエストされた場合、Supabaseのdeleteメソッドを使って該当のレコードを削除します。
  6. デフォルトのレスポンス
    "POST"または"DELETE"以外のメソッドがリクエストされた場合、エラーメッセージとともにHTTPステータスコード405を返します。

Supabaseを利用することで、お気に入り機能を安全に実装することができます。また、Next.jsのAPIルートを活用することで、開発をより効率的かつ柔軟に開発を進めることが可能です。

🐫終わりに

太字で強調したトピックは、今後より深く掘り下げて学習し、理解を深めていきたいと考えています。また、適宜、図やコードでの説明を追加し、より分かりやすいコンテンツになるよう更新していく予定です。

この文章を書く過程は興味深いものでした。リモートワークをこなしながら、スマホでChatGPTと対話し、アイデアを磨いていくという経験は、時代の恩恵を感じさせるものでした。

AIの活用は、私たちの学習効率を一層向上させるだけでなく、新たな視点や洞察を提供してくれることでしょう。

本稿の内容について誤りや不足がある場合、または追加的な情報や視点を提供したい場合は、ぜひコメントでお知らせください。皆様のフィードバックをお待ちしております。

⚡参考&JavaScriptのおすすめ学習コンテンツ

https://www.udemy.com/share/103dh43@fGbVyqdDJxjpa_49mpjHJecdwWRhgYh7or9Sp8sVBno50gCtv2AzqPw_C335uRoUsg==/
https://zenn.dev/estra/books/js-async-promise-chain-event-loop

Discussion