TypeScriptでクライアントとサーバーのバリデーションを共通化する方法!初心者向けデータ検証入門
生徒
「Webアプリを作るときに、画面とサーバーの両方で同じチェック処理を書くのが大変です。TypeScriptで効率よく共通化する方法はありますか?」
先生
「TypeScriptを使えば、クライアント側とサーバー側のバリデーションを一つの仕組みにまとめることができます。二重にコードを書く必要がなくなりますよ。」
生徒
「それは便利ですね。未経験の私でもわかるように、仕組みや具体的な書き方を教えてほしいです!」
先生
「お任せください。データの入力チェックの基本から、共通化の具体的な手順まで、簡単な例えを交えて順番に解説していきますね!」
1. バリデーションとは?
パソコンの操作やプログラミングが初めてという方に向けて、まずはバリデーションという言葉から説明します。バリデーションとは、日本語で言うとデータ入力チェックや正しいかどうかの検証という意味になります。
たとえば、インターネットで買い物をするときに、会員登録の画面でメールアドレスやパスワードを入力します。このときに、メールアドレスの欄に文字が何も入力されていなかったり、パスワードが短すぎたりした場合に、画面に「文字を入力してください」や「パスワードは8文字以上で入力してください」といった赤い文字のエラーメッセージが表示されることがあります。このように、ユーザーが入力したデータが、システムが受け入れても良い正しい形式になっているかどうかを、プログラムが事前に厳しくチェックする仕組みのことをバリデーションと呼びます。
もしこのバリデーションの仕組みがないと、システムが予期しないおかしなデータがそのまま登録されてしまい、後からアプリが正しく動かなくなったり、コンピューターが壊れる原因になってしまったりします。そのため、安全で使いやすいアプリケーションを作るためには、このデータチェックの仕組みが絶対に欠かせない重要な要素となります。
2. クライアントとサーバーの役割
Webアプリケーションの仕組みは、大きく分けてクライアントとサーバーという2つの役割に分かれています。この2つの関係を理解することが、チェック処理を共通化する最大の理由に繋がります。
クライアントとは、私たちが普段使っているパソコンやスマートフォン、タブレットなどの画面のことです。具体的には、Google ChromeなどのWebブラウザに表示されている画面そのものを指します。画面を操作するユーザーに最も近い場所にいるのがクライアントです。これに対して、サーバーとは、インターネットの奥深くにある大きなコンピューターのことで、画面から送られてきたデータをデータベースと呼ばれる倉庫に保存したり、複雑な計算を行ったりする役割を持っています。
一般的なWebアプリでは、ユーザーが画面で入力したデータは、一度クライアント側で簡単なチェックが行われた後、インターネットの線を通ってサーバー側に送られます。そして、サーバー側でもう一度、本当に安全なデータであるかどうかの最終チェックを行います。このように、データが通る道筋の最初と最後で、それぞれ役割を分担してデータを監視しているのです。
3. なぜ両方でチェックが必要なのか
ここで、一つの疑問が浮かびます。「クライアント側とサーバー側の両方で、わざわざ2回も同じようなデータチェックを行うのは無駄ではないのか」という点です。しかし、安全なアプリを作るためには、この両方での検証が絶対に必要になります。
クライアント側でのチェックは、主に操作しているユーザーの利便性を高めるために行います。ボタンを押した瞬間に、画面が切り替わることなくその場で「入力が漏れています」と教えてくれれば、ユーザーはすぐに間違いに気づいて修正することができます。もしサーバー側だけでしかチェックを行っていないと、データを送信してしばらく待った後に、画面がガタガタと動いてエラーが表示されるため、ユーザーはストレスを感じてしまいます。
一方で、サーバー側でのチェックは、システムの安全性を守るための最後の砦です。実は、クライアント側のブラウザに表示されているプログラムは、悪意のあるユーザーによって簡単に書き換えられたり、無効化されたりしてしまいます。つまり、クライアント側のチェックをずる賢くすり抜けて、不正なデータを直接サーバーに送りつけることが技術的に可能なのです。もしサーバー側が送られてきたデータを信じ切ってそのまま受け入れてしまうと、システムが乗っ取られたり、大切なデータが破壊されたりする危険があります。そのため、どちらか一方だけではなく、両方の場所で二重の壁を作ってチェックを行う必要があるのです。
4. 従来の開発が抱えていた問題点
これまでの古いWebアプリ開発では、クライアント側とサーバー側で、別々のプログラミング言語が使われることが一般的でした。たとえば、画面のプログラムはJavaScriptという言語で書き、サーバーのプログラムはPHPやJava、C#といった別の言語で書くという形です。
この場合、非常に面倒な問題が発生していました。それは、「同じチェックルールを、別々の言語で2回書かなければならない」ということです。たとえば「名前は20文字以内で入力すること」というルールを決めた場合、画面用のJavaScriptコードでその判定処理を書き、さらにサーバー用のPHPコードでも全く同じ判定処理を書く必要がありました。
これの何が問題かというと、後から仕様が変更になって「名前は30文字以内でも大丈夫にしよう」となったときに、両方のファイルを人間の手で修正しなければなりません。もし片方の修正を忘れてしまうと、画面では30文字まで入力できるのに、サーバーに送ったら20文字を超えているという理由で拒否されてしまうといった、ちぐはぐな不具合が発生してしまいます。開発の規模が大きくなればなるほど、この二重管理はプログラマーの手間を増やし、バグを発生させる大きな原因になっていました。
5. TypeScriptによる共通化のメリット
この二重管理という大きな悩みを一気に解決してくれるのが、TypeScriptというプログラミング言語です。TypeScriptは、世界中で広く使われているJavaScriptをベースに、より安全に、より正確にプログラムを書くことができるように改良された言語です。
現在では、このTypeScriptを使うことで、画面のプログラムだけでなく、サーバー側のプログラムも同じ言語で記述することができる環境が整っています。画面もサーバーも同じ言語で書けるということは、データのチェックを行うためのプログラムファイルを、一つだけ作成すれば良いということになります。その一つのファイルを、クライアント側からもサーバー側からも、共通の部品として呼び出して使い回すのです。
この共通化が実現すると、数多くの素晴らしいメリットが生まれます。まず、プログラムを書く量が単純に半分近くに減るため、開発のスピードが圧倒的に速くなります。さらに、入力ルールの変更があったときも、共通化された一つのファイルを修正するだけで、画面とサーバーの両方に自動的に変更が反映されます。修正漏れによる不具合が仕組みとして絶対に起きなくなるため、非常に安全で、メンテナンスがしやすい綺麗なアプリケーションを組み立てることができるようになります。
6. 条件分岐を使った手動バリデーションの例
それでは、具体的にTypeScriptを使ってどのようにチェックを行うのか、まずは一番基本となる条件分岐のプログラムを見てみましょう。初心者の方にもわかりやすいように、ユーザーが入力した年齢のデータをチェックする単純な関数を作成します。
関数とは、特定の処理をひとまとめにした便利な命令ボックスのようなものです。ここでは、年齢が0歳以上であるか、そして120歳以下であるかという正しい範囲に収まっているかを判定します。このファイルを一つ用意して、画面側とサーバー側の両方から読み込んで使用します。
function validateAge(age: number): boolean {
if (age < 0) {
return false;
}
if (age > 120) {
return false;
}
return true;
}
let inputAge = 25;
let isOk = validateAge(inputAge);
console.log(isOk);
上記のプログラムを実行した結果、出力される内容は次のようになります。データが正しい範囲にあるため、成功を表す言葉が表示されます。
true
このコードについて解説します。functionというのは、これから新しい関数という命令ボックスを作りますという宣言です。括弧の中にあるage: numberは、このボックスを使うときには必ず数値のデータを一つ入れてくださいという意味です。矢印の先にあるbooleanは、このボックスが最終的に「正しい(true)」か「間違い(false)」のどちらかの結果を返しますという約束事です。
ボックスの中ではifという仕組みを使って、もし入力された年齢が0より小さければ失敗、120より大きければ失敗、どちらにも引っかからなければ無事に合格ということで成功を返しています。この短いプログラムを共通の置き場所に置いておくことで、画面でもサーバーでも全く同じ年齢チェックを行うことができます。
7. 文字列の長さをチェックする共通の例
次に、別のパターンとして、ユーザーが入力するパスワードの文字数をチェックするプログラムを作成してみましょう。セキュリティを高めるために、パスワードは必ず8文字以上、かつ30文字以下で入力してもらうというルールを共通化します。
文字の長さを数えるときは、プログラミングの世界では長さという意味の単語を使って文字数を取得します。文字が何も入力されていない空っぽの状態や、短すぎる状態をこの共通関数で一発で検知できるようにします。
function validatePassword(password: string): boolean {
let textLength = password.length;
if (textLength < 8) {
return false;
}
if (textLength > 30) {
return false;
}
return true;
}
let userPassword = "abc";
let checkResult = validatePassword(userPassword);
console.log(checkResult);
上記のプログラムを実行すると、今回はパスワードが3文字しかなく、8文字以上というルールを満たしていないため、不合格を意味する結果が出力されます。
false
このプログラムでは、stringという言葉を使って、文字のデータを受け取るように指定しています。そして、受け取った文字の後ろに.lengthと付けることで、その文字が全部で何文字あるのかをコンピューターに数えさせています。変数というデータの入れ物であるtextLengthに入った数字を、続く条件分岐で判定しています。このように、文字列の検証も共通の関数として定義しておけば、画面でエラーを出すのも、サーバーで不正アクセスを弾くのも同じ基準で行えます。
8. 複数の入力項目をまとめて検証する例
実際のWebアプリでは、年齢やパスワードだけでなく、名前やメールアドレスなど、たくさんの項目を一度にまとめて送信することがほとんどです。そのため、それらの項目がすべて正しく入力されているかをまとめてチェックする構造を作ることが大切です。
今度は、ユーザーの名前と、そのユーザーが登録した会員種別のコードが正しい組み合わせになっているかを一括で確認するプログラムを見てみましょう。複数のデータを一つの塊として扱うために、プログラミングではオブジェクトという形式を使います。
function validateUserProfile(name: string, role: string): boolean {
if (name.length === 0) {
return false;
}
if (role !== "admin" && role !== "user") {
return false;
}
return true;
}
let sampleName = "たろう";
let sampleRole = "guest";
let isValidProfile = validateUserProfile(sampleName, sampleRole);
console.log(isValidProfile);
上記のプログラムを実行すると、名前は入力されていますが、会員種別が規定の文字ではないため、全体としてエラーと判定されます。
false
このコードの中にある===や!==という記号は、左右のデータが完全に一致しているか、あるいは一致していないかを調べるための特殊な比較の記号です。また、&&という記号は「なおかつ」という意味で、複数の条件を同時に満たしているかを調べるために使います。この例では、名前が空っぽではなく、かつ役割が管理者か一般ユーザーのどちらか一方でなければならないという複雑なルールを表現しています。これも一つの共通ファイルとして定義しておくことで、システムのどこからでも同じルールで全体の整合性を確認できるようになります。
9. ライブラリを活用した高度な共通化
ここまで紹介した手動でのチェック方法は、仕組みがシンプルでわかりやすい反面、入力項目が数十個に増えていくと、条件分岐のコードがどんどん長くなってしまい、書くのが少し大変になってきます。そこで実際の現場では、バリデーションをより簡単に、強力に行うためのライブラリと呼ばれる便利ツールがよく使われます。
TypeScriptの世界で特に人気があるのが、Zodという名前のライブラリです。これを使うと、データの設計図をあらかじめ定義しておくだけで、複雑な文字数制限や必須チェック、メールアドレスの形式チェックなどを、数行のコードで自動的に行ってくれるようになります。
この設計図を記述したファイルもまた、クライアント側とサーバー側の両方から共有ライブラリとして読み込むことができます。これにより、手書きの条件分岐をたくさん並べることなく、すっきりとした見た目で高度なバリデーション共通化システムを構築することができます。未経験の方も、まずは基本の条件分岐をマスターした後は、こうした便利な道具があることを知っておくと、今後のアプリ作りの幅がさらに広がっていくはずです。