JavaScriptのコールバック関数を理解する - Express.jsの実装から学ぶ非同期処理の基本
JavaScript のコールバック関数を理解する - Express.js の実装から学ぶ非同期処理の基本
Express.js でシンプルな API を作りながら、コールバック関数の仕組みとJavaScript の非同期処理について深く理解していきます。
はじめに
クラウドエース株式会社 第一開発部 喜村です。
普段は SRE 領域を担当していますが、とある事情で TypeScript を触る機会があり Express.js を使った Web アプリ開発に挑戦しました。
そこで最初に出会ったのがこのようなコードでした:
app.ts:
app.get('/', (req, res) => {
res.contentType('json').json({
message: 'This is practice-api service.',
});
});
一見シンプルに見えるこのコードですが、実はJavaScript の非同期処理とコールバック関数の重要な概念が詰まっていることを知りました。
この記事では、TypeScript 初心者の私が実際にコードを書きながら得た気づきと技術指導を通じて学んだことを共有します。
同じような初心者の方の参考になれば幸いです。
実装時の気づき:コールバック関数とは何か
app.get('/', (req, res) => {})
の理解
最初は単純なルーティングだと思っていましたが、実際に実装してみて以下のことを理解しました:
app.ts:
app.get('/', (req, res) => {
res.contentType('json').json({
message: 'This is practice-api service.',
});
});
-
req, res
はコールバック関数の引数 -
req
(第一引数): リクエストオブジェクト(express.Request
) -
res
(第二引数): レスポンスオブジェクト(express.Response
)
JavaScript の非同期処理とコールバックの重要性
上司からの重要な指摘
JavaScript はシングルスレッド動作するので並列処理ができません。(マルチコアで複数プロセスを作動させれば可能)
では並行処理をどうやっているか?というと非同期処理という仕組みで、通常では時間のかかる処理が発生した際に処理はブロック(中断)されるところを、待たずに別の処理を実行します。
非同期処理の流れ
つまり、以下のような処理の流れになります:
- 非同期処理が関わる場合、コードを上から順番に記述しても、必ずその順序で処理されるとは限らない
- 処理完了後に別の処理を実行できるようにするのが、コールバックの仕組み
- 別の処理を実行している間に非同期の処理が完了すると、そちらに処理が戻る
なぜコールバック関数が必要なのか
従来の同期処理では:
リクエスト受信 → 処理実行 → レスポンス送信
しかし、JavaScript の非同期処理では:
リクエスト受信 → 他の処理も並行実行 → 処理完了時にコールバック実行 → レスポンス送信
このため、処理完了時に何をするかを事前に定義しておく必要があります。それがコールバック関数です。
コールバック関数の仕組みを深掘り
基本的な構造
// 基本的な構造
some.method(param, () => { return 'hoge' });
// 簡潔に表現すると
some.method(param, FUNC);
実装側では以下のようになっています:
function method(param1, callback) {
// なんらかの処理
callback();
}
Express.js での実際の動作
Express.js の app.get
では以下のような流れになります:
- HTTP リクエストが到着
- ルーティングマッチング
- コールバック関数
(req, res) => {}
が呼び出される - コールバック内でレスポンス処理
app.get('/', (req, res) => {
// この関数がコールバックとして呼び出される
res.contentType('json').json({
message: 'This is practice-api service.',
});
});
重要なポイント
() => {}
はfunction () {}
のシンタックスシュガー- Express だけでなく、モダンなフレームワークで頻出
Express.js でのコールバック関数の型システム
型定義を追いかけてみる
実際に VSCode で「実装にジャンプ」(F12)して、app.get
の型定義を確認してみました:
RequestHandler
の定義:
export interface RequestHandler<
P = ParamsDictionary,
ResBody = any,
ReqBody = any,
ReqQuery = ParsedQs,
LocalsObj extends Record<string, any> = Record<string, any>,
> {
(
req: Request<P, ResBody, ReqBody, ReqQuery, LocalsObj>,
res: Response<ResBody, LocalsObj>,
next: NextFunction,
): unknown;
}
型システムから見るコールバック関数
この型定義から分かることは:
-
コールバック関数は3つの引数を受け取る:
req
,res
,next
-
req
: リクエスト情報を含むオブジェクト -
res
: レスポンスを送信するためのオブジェクト -
next
: 次の middleware に処理を渡すための関数(今回は使用しない)
実際の使用例での型推論
app.get('/', (req, res) => {
// TypeScript が自動的に型を推論
// req: express.Request
// res: express.Response
res.contentType('json').json({
message: 'This is practice-api service.',
});
});
重要なポイント:
- TypeScript が自動的に
req
とres
の型を推論してくれる - 明示的に型を書く必要がない(
app.get
の文脈から推論可能) - これにより、コードがシンプルで読みやすくなる
コールバック関数の実践的な理解
実際の動作確認
$ curl http://localhost:8080/
{"message":"This is practice-api service."}
このシンプルな API コールの裏では:
- Express.js が HTTP リクエストを受信
- ルーティング
'/'
にマッチ - コールバック関数
(req, res) => {}
が非同期で実行 -
res.contentType('json').json()
でレスポンス送信
コールバック関数の利点
- 非ブロッキング: 他のリクエストを並行処理可能
- イベント駆動: リクエスト到着時に適切な処理を実行
- 拡張性: middleware チェーンで機能を追加可能
まとめ
Express.js の app.get('/', (req, res) => {})
という一行のコードから、以下の重要な概念を学びました:
-
JavaScript の非同期処理の仕組み
- シングルスレッドでの並行処理
- 非同期処理によるノンブロッキング処理
-
コールバック関数の本質
- 処理完了時の動作を事前定義
- イベント駆動プログラミングの基礎
-
TypeScript の型システム
- オーバーロードによる型解決
- 可読性を考慮した型明示の判断
TypeScript 初心者だった私にとって、これらの理解は大きな収穫でした。Express.js だけでなく、モダンな JavaScript/TypeScript 開発全般で重要な基礎知識となることを実感しています。
コールバック関数は、Promise、async/await といった新しい非同期処理パターンの基礎でもあります。今回の経験を通じて、「なぜこの仕組みが必要なのか」を理解できたことで、今後の学習への道筋が見えてきました。
同じように TypeScript を学び始めた方にとって、この記事が少しでもお役に立てれば幸いです。
この記事は実際の開発体験と技術指導を基に作成しました。
Discussion