Scala.js入門:Webフロントエンド開発の新しい選択肢
序文
現代のWebフロントエンド開発は、JavaScriptという言語とその巨大なエコシステムを中心に展開されています。React、Vue、AngularといったフレームワークがUI構築を効率化し、Node.jsとnpmがパッケージ管理とビルド環境を支え、Webはかつてないほど動的でインタラクティブなアプリケーションプラットフォームへと進化しました。
しかし、この急速な進化は同時に新たな課題も生み出しました。アプリケーションが大規模化・複雑化するにつれ、動的型付け言語であるJavaScriptの持つ柔軟性が、逆に大規模なコードベースの維持管理を困難にする一因となりました。リファクタリングの恐怖、実行時まで気づけない型エラー、エディタによる不完全なコード補完。これらの課題に対する有力な解決策として登場したのが TypeScript です。
TypeScriptはJavaScriptに静的な型システムを導入し、コンパイル時に多くのエラーを検出可能にしました。これにより、開発体験は劇的に向上し、大規模アプリケーションにおけるコードの堅牢性と保守性は飛躍的に高まりました。今やTypeScriptは、現代フロントエンド開発のデファクトスタンダードと言っても過言ではない地位を確立しています。
では、フロントエンド開発における「型」の探求は、TypeScriptで終わりなのでしょうか?
もし、TypeScriptよりもさらに強力で表現力豊かな型システムを持つ言語をフロントエンドで使えたら? もし、関数型プログラミングの強力なパラダイムを、ブラウザ上で最大限に活用できたとしたら? そして、サーバーサイドで実績のある堅牢な言語を、クライアントサイドでもシームレスに利用できたとしたら?
この問いに対する一つの魅力的な答えが Scala.js です。
Scala.jsは、強力な静的型付け言語であるScalaをJavaScriptにコンパイルするための公式コンパイラです。それは単なる言語の移植に留まらず、Scalaの持つ表現力豊かな型システム、オブジェクト指向と関数型プログラミングの美しい融合、そして堅牢な標準ライブラリを、そのままWebフロントエンド開発の世界に持ち込みます。
この記事では、TypeScriptが切り開いた静的型付けフロントエンド開発の道をさらに一歩進む選択肢として、Scala.jsの世界を詳細に解説します。Scala.jsとは何か、なぜ今注目する価値があるのか、そしてどのように始めればよいのか。その基本から実践的な応用までを旅しながら、Webフロントエンド開発の新しい可能性を探っていきましょう。これは、あなたの開発体験を根底から変えるかもしれない、挑戦的で、しかし非常に実り豊かな選択肢の物語です。
第1章:Scala.jsとは何か?
1.1. Scala.jsの概要
Scala.jsは、その名の通り、ScalaのコードをJavaScriptのコードに変換するコンパイラです。Scalaの作者であるMartin Odersky氏が率いるEPFL(スイス連邦工科大学ローザンヌ校)のチームによって開発が進められている公式プロジェクトであり、その完成度と安定性は非常に高いレベルにあります。
Scala.jsの目的は、開発者がScalaという単一の言語を使って、サーバーサイドからクライアントサイド(Webブラウザ)まで、一貫した開発を行えるようにすることです。これにより、以下のような強力なメリットが生まれます。
-
究極の型安全性: Scalaは、Javaよりもさらに厳格で表現力豊かな静的型システムを備えています。高階型(Higher-Kinded Types)、型クラス(Typeclasses)、代数的データ型(ADT)、暗黙の型変換(Implicits)といった高度な機能を駆使することで、実行時エラーの多くをコンパイル時に排除し、極めて堅牢なアプリケーションを構築できます。
-
表現力豊かな言語機能: Scalaは、オブジェクト指向プログラミング(OOP)と関数型プログラミング(FP)のハイブリッド言語です。これにより、問題領域に応じて最適なパラダイムを選択できます。不変性(Immutability)を基本とする強力なコレクションライブラリ、エレガントなパターンマッチ、
for
式(for-comprehension)による非同期処理やコレクション操作の簡潔な記述など、コードの可読性と表現力を飛躍的に向上させる機能が満載です。 -
シームレスなコード共有: サーバーサイド(JVMで動作)とクライアントサイド(ブラウザで動作)で同じScalaコードを共有できます。これは、単に同じ言語を使うというレベルに留まりません。例えば、APIでやり取りするデータモデル(
case class
で定義)、入力値のバリデーションロジック、ビジネスロジックの一部などを、共通のモジュールで一度だけ定義し、サーバーとクライアントの両方で再利用できます。これにより、コードの重複が排除され、仕様の齟齬も防ぐことができます。
Scala.jsは、JavaScriptを「アセンブリ言語」のように捉え、より高レベルで安全な言語であるScalaからコンパイルするというアプローチを取ります。これにより、開発者はJavaScriptの癖や落とし穴から解放され、アプリケーションのロジックそのものに集中することができるのです。
1.2. なぜScala.jsなのか? TypeScriptとの比較
「静的型付けならTypeScriptで十分では?」という疑問はもっともです。ここでは、Scala.jsがTypeScriptと比べてどのような優位性を持つのかを具体的に見ていきましょう。
1. 型システムの表現力
TypeScriptの型システムは非常に優れており、JavaScriptの動的な性質をうまくモデル化していますが、Scalaの型システムはさらに一段上の表現力と厳密性を持ちます。
その代表例が代数的データ型 (Algebraic Data Types, ADT) です。ADTは、特定の型の取りうる値を厳密に限定するための強力な手法です。Scalaでは sealed trait
と case class
/object
を使ってこれを非常に簡潔に表現できます。
“`scala
// ScalaでのADT表現
sealed trait Option[+A]
case class Some+A extends Option[A]
case object None extends Option[Nothing]
def show(opt: Option[String]): String = {
opt match {
case Some(value) => s”The value is $value”
case None => “There is no value”
}
}
“`
このコードでは、Option[String]
型の変数が取りうる値は Some[String]
か None
のいずれかである、ということがコンパイラによって保証されます。そして match
式(パターンマッチ)では、コンパイラがすべてのケース(Some
と None
)を網羅しているかチェックしてくれます。もし case None
を書き忘れると、コンパイルエラーが発生し、潜在的なバグを防いでくれます。
TypeScriptでもTagged Union Typesを使って同様の表現は可能ですが、Scalaの方がより言語レベルで自然にサポートされており、パターンマッチと組み合わせることでその真価を発揮します。
2. 不変性 (Immutability) の重視
関数型プログラミングの中核的な思想の一つに、状態の変更を避け、不変のデータ構造を扱うというものがあります。これにより、副作用が減り、コードの予測可能性が高まり、特に非同期処理や並行処理においてバグの温床となる競合状態を防ぎやすくなります。
Scalaの標準コレクションライブラリは、デフォルトで不変です。
“`scala
// Scalaの不変List
val numbers = List(1, 2, 3)
val newNumbers = numbers.appended(4) // numbersは変更されず、新しいListが返る
println(numbers) // => List(1, 2, 3)
println(newNumbers) // => List(1, 2, 3, 4)
“`
一方、JavaScriptの Array
は可変(ミュータブル)です。TypeScriptを使っていても、push
や sort
などの破壊的メソッドを誤って使ってしまうリスクは残ります(ReadonlyArray
型もありますが、標準ではありません)。Scalaでは、不変性が言語とライブラリの設計思想に深く根付いているため、自然と安全なコードを書きやすくなります。
3. サーバーサイドとのエコシステム共有
Scalaは元々、JVM(Java仮想マシン)上で動作するサーバーサイド言語として大きな成功を収めてきました。Akka、Play Framework、Cats、ZIOなど、強力で成熟したライブラリやフレームワークが数多く存在します。
Scala.jsの大きな利点は、この成熟したエコシステムの一部をフロントエンド開発にも持ち込めることです。もちろん、JVMに依存するライブラリは使えませんが、純粋なScalaで書かれたロジックライブラリ(例えば、JSONパーサーのuPickle
やCirce
、テストフレームワークのscalatest
など)は、クロスコンパイル設定を行うだけでクライアントサイドでも利用できます。これは、サーバーとクライアントで同じライブラリ、同じツールチェーンを使えるという、TypeScriptにはない強力なアドバンテージです。
1.3. Scala.jsの動作原理
Scala.jsのコンパイラは、Scalaのコードを直接JavaScriptに変換するわけではありません。一度、Scala.js IR (Intermediate Representation) という中間表現に変換されます。このIRは、Scalaのセマンティクスを保ちつつ、JavaScriptに変換しやすいように最適化された形式です。
このIRを経由するアーキテクチャにはいくつかの利点があります。
- モジュール性: Scalaコンパイラのバックエンドとして実装されているため、Scala言語の進化に追従しやすいです。
- 最適化: JavaScriptへの最終変換前に、IRのレベルで様々な最適化を施すことができます。
開発プロセスでは、主に2つのコンパイルモードを使い分けます。
-
fastOptJS
: 開発用のモードです。コンパイル速度を最優先し、最小限の最適化のみを行います。これにより、コードを変更してからブラウザで結果を確認するまでのサイクルを高速に回すことができます。 -
fullOptJS
: 本番用のモードです。Google Closure Compiler という非常に強力なJavaScriptオプティマイザを使い、徹底的な最適化を行います。これにより、以下のような効果が得られます。- デッドコードエリミネーション (DCE): アプリケーションで全く使われていないコード(ライブラリ内の未使用関数なども含む)を根こそぎ削除します。
- インライニング: 関数呼び出しをその場に展開し、オーバーヘッドを削減します。
- minify/uglify: 変数名を短くするなどして、ファイルサイズを極限まで圧縮します。
この fullOptJS
のおかげで、Scala.jsで生成されたJavaScriptは、人間が手で書いたコードよりも小さく、高速になることさえあります。Scalaの豊富な標準ライブラリを使っても、最終的な成果物のサイズが肥大化しにくいのは、この強力な最適化のおかげです。
第2章:開発環境の構築
Scala.jsを始めるために、まずは開発環境を整えましょう。ScalaのエコシステムはJavaをベースにしているため、いくつかのツールのインストールが必要です。
2.1. 必要なツール
-
Java Development Kit (JDK): バージョン8以上が必要です。Adoptium (旧AdoptOpenJDK) などからインストールするのが一般的です。
bash
# macOS (Homebrew)
brew install openjdk@11 -
sbt (Scala Build Tool): Scalaプロジェクトにおける標準的なビルドツールです。依存関係の管理、コンパイル、テスト、パッケージングなど、開発に必要なあらゆるタスクを担います。
bash
# macOS (Homebrew)
brew install sbt -
Node.js: Scala.js自体はNode.jsを直接必要としませんが、生成されたJavaScriptを実行したり、JavaScriptエコシステムのライブラリ(例:React)と連携したり、開発サーバーを動かしたりする際に必要となります。
-
コードエディタ / IDE:
- Visual Studio Code + Metals: MetalsはScalaのLanguage Serverであり、VSCode上で快適なコード補完、型情報表示、定義ジャンプなどを提供してくれます。軽量でモダンな開発体験を求めるならおすすめです。
- IntelliJ IDEA + Scala Plugin: 伝統的で強力なIDEです。リファクタリング機能やデバッガなどが非常に充実しており、大規模開発で真価を発揮します。
2.2. sbtプロジェクトのセットアップ
sbtを使えば、Scala.jsプロジェクトの雛形を簡単に作成できます。ターミナルで以下のコマンドを実行してください。
bash
sbt new scala/scala-js.g8
このコマンドは、公式のプロジェクトテンプレート(giter8テンプレート)を使って、インタラクティブにプロジェクトを生成します。プロジェクト名などを聞かれるので、適宜入力してください(デフォルトのままでも構いません)。
生成されると、以下のようなディレクトリ構造が出来上がります。
my-scalajs-app/
├── project/
│ └── plugins.sbt # sbtプラグインの定義
├── src/
│ └── main/
│ └── scala/
│ └── Main.scala # メインのScalaコード
├── build.sbt # プロジェクトのビルド設定
└── index.html # アプリケーションを読み込むHTML
ここで最も重要なファイルは build.sbt
です。中身を見てみましょう。
build.sbt
“`scala
// プロジェクト名やScalaのバージョンなどを定義
ThisBuild / scalaVersion := “3.3.1” // 例: Scala 3
ThisBuild / organization := “com.example”
lazy val root = (project in file(“.”))
.settings(
name := “my-scalajs-app”
)
.enablePlugins(ScalaJSPlugin) // Scala.jsプラグインを有効化
// JavaScriptライブラリの依存関係(必要な場合)
// npmDependencies in Compile ++= Seq(
// “react” -> “18.2.0”,
// “react-dom” -> “18.2.0”
// )
// Scalaライブラリの依存関係
libraryDependencies += “org.scala-js” %%% “scalajs-dom” % “2.8.0”
// メインクラスを指定し、JSファイルがロードされたときに実行されるようにする
scalaJSUseMainModuleInitializer := true
“`
いくつかの重要な設定項目を見ていきましょう。
.enablePlugins(ScalaJSPlugin)
: このプロジェクトでScala.jsを有効にするための最も重要な設定です。libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.8.0"
:- これはライブラリの依存関係を定義しています。
scalajs-dom
は、ブラウザのDOM APIを型安全に操作するためのライブラリです。 %%%
という演算子に注目してください。これはsbtの特別な記法で、Scala.jsプロジェクトであることを示しています。sbtは自動的にプラットフォーム(この場合はScala.js)に合ったバージョンのライブラリを選択してくれます。
- これはライブラリの依存関係を定義しています。
scalaJSUseMainModuleInitializer := true
: これを設定すると、@JSExportTopLevel
を付けなくても、main
メソッドを持つオブジェクトがエントリーポイントとして自動的に実行されるようになります。
2.3. “Hello, World!” アプリケーションの作成
それでは、実際に簡単なアプリケーションを作成して動かしてみましょう。
まず、src/main/scala/Main.scala
を以下のように編集します。
src/main/scala/Main.scala
“`scala
import org.scalajs.dom
import org.scalajs.dom.document
object Main {
def main(args: Array[String]): Unit = {
// DOMに新しい要素を追加する関数
def appendPar(targetNode: dom.Node, text: String): Unit = {
val parNode = document.createElement(“p”)
parNode.textContent = text
targetNode.appendChild(parNode)
}
// "Hello, World!"というテキストを持つpタグをbodyに追加
appendPar(document.body, "Hello, World!")
}
}
“`
このコードは、scalajs-dom
ライブラリを使って、標準的なDOM操作を行っています。document.createElement
や appendChild
といった、JavaScriptでお馴染みのAPIが、Scalaの型安全なメソッドとして利用できることがわかります。
次に、このアプリケーションをブラウザで表示するための index.html
を確認します。
index.html
“`html
“`
script
タグが、sbtによって生成されるJavaScriptファイルを指していることに注意してください。パスは build.sbt
の設定によって変わります。
最後に、sbtを使ってScalaコードをJavaScriptにコンパイルしましょう。プロジェクトのルートディレクトリでターミナルを開き、sbt
と入力してsbtシェルを起動します。
bash
$ sbt
sbt:my-scalajs-app>
sbtシェル内で fastOptJS
コマンドを実行します。
sbt:my-scalajs-app> fastOptJS
コンパイルが成功すると、target/scala-3.3.1/my-scalajs-app-fastopt.js
というファイル(と、ソースマップファイルなど)が生成されます。
これで準備は完了です。index.html
ファイルを直接ブラウザで開いてみてください。画面に “Hello, World!” と表示されれば成功です!
第3章:Scala.jsの基本文法と機能
Scala.jsの真価は、既存のJavaScriptエコシステムとシームレスに連携できる点にあります。ここでは、そのための基本的な機能を見ていきましょう。
3.1. JavaScriptとの相互運用性 (Interoperability)
Scala.jsは、既存の何百万ものJavaScriptライブラリやAPIを無視するのではなく、それらを型安全に活用するための洗練された仕組みを提供します。
1. ScalaからJavaScriptを呼び出す
- 動的な呼び出し (
js.Dynamic
)
最も手軽な方法は js.Dynamic.global
を使うことです。これは、JavaScriptのグローバルスコープにあるオブジェクトに動的にアクセスするための機能です。
“`scala
import scala.scalajs.js
import scala.scalajs.js.Dynamic.global
// JavaScriptの console.log("Hello from Scala.js")
と等価
global.console.log(“Hello from Scala.js”)
// JavaScriptの alert(Math.random())
と等価
global.alert(global.Math.random())
“`
js.Dynamic
は型安全性を犠牲にするため、プロトタイピングや、型定義を書くのが面倒な場合に限定して使うのが良いでしょう。
- 静的な呼び出し(ファサード型)
本番のコードでは、JavaScriptのライブラリやAPIに対してファサード型 (Facade Types) を定義するのがベストプラクティスです。これは、@js.native
アノテーションと trait
を使って、既存のJavaScriptオブジェクトの「型だけの定義」をScalaで記述するものです。
例えば、ブラウザの localStorage
APIに型を付けてみましょう。
“`scala
import scala.scalajs.js
import scala.scalajs.js.annotation.JSGlobal
@js.native
@JSGlobal(“localStorage”)
object LocalStorage extends js.Object {
def setItem(key: String, value: String): Unit = js.native
def getItem(key: String): String | Null = js.native
def removeItem(key: String): Unit = js.native
def clear(): Unit = js.native
}
“`
@JSGlobal("localStorage")
: グローバルスコープにあるlocalStorage
オブジェクトにマッピングすることを示します。@js.native
: このobject
やメソッドが、Scala.jsによって実装されるのではなく、既存のJavaScript環境にすでに存在すること(ネイティブ実装)を示します。メソッドの本体はjs.native
と書くだけで実装は不要です。String | Null
: Scala 3のUnion Typesを使い、getItem
がstring
またはnull
を返すことを型レベルで表現しています。
このファサード型を定義すれば、あとは普通のScalaオブジェクトのように、型安全に localStorage
を利用できます。
scala
LocalStorage.setItem("username", "Taro")
val username = LocalStorage.getItem("username")
println(s"Username from localStorage: $username") // => "Username from localStorage: Taro"
もし setItem
の第二引数に Int
を渡そうとしたり、存在しないメソッドを呼び出そうとしたりすると、コンパイルエラーになります。これがファサード型の強力な点です。多くの有名なJavaScriptライブラリについては、コミュニティによって高品質なファサード型がすでに作られ、公開されています(例: ScalablyTyped)。
2. JavaScriptからScalaを呼び出す
逆に、Scalaで書いたコードをJavaScript側から呼び出したい場合もあります。そのためには @JSExportTopLevel
や @JSExport
アノテーションを使います。
“`scala
import scala.scalajs.js.annotation.{JSExportTopLevel, JSExport}
object MyApi {
@JSExportTopLevel(“myApp”)
def run(): Unit = {
println(“myApp.run() was called from JavaScript!”)
}
}
class Calculator {
@JSExport
def add(a: Int, b: Int): Int = a + b
}
object Calculator {
@JSExportTopLevel(“createCalculator”)
def create(): Calculator = new Calculator()
}
“`
これをコンパイルすると、JavaScript側から以下のように呼び出せるようになります。
“`javascript
// myApp.run() を呼び出す
myApp();
// createCalculator() でインスタンスを作成し、addメソッドを呼び出す
const calc = createCalculator();
const result = calc.add(5, 10);
console.log(result); // => 15
“`
3. JavaScriptの型を表現する
Scala.jsには、JavaScript特有のデータ型や挙動をモデル化するための型が用意されています。
js.Object
: すべてのJavaScriptオブジェクトの基本型。js.Array[A]
: JavaScriptの配列。js.Dictionary[A]
: 文字列をキーとするオブジェクト ({ [key: string]: A }
)。js.UndefOr[A]
:A | undefined
を表現する型。JavaScriptのAPIではオプショナルな引数や戻り値でundefined
が頻繁に使われるため、これを安全に扱うために必須の型です。
3.2. DOM操作
第2章で見たように、scalajs-dom
ライブラリを使えば、型安全にDOMを操作できます。
“`scala
import org.scalajs.dom
import org.scalajs.dom.{document, html}
// IDで要素を取得
val button = document.getElementById(“my-button”).asInstanceOf[html.Button]
val output = document.getElementById(“output”).asInstanceOf[html.Div]
// イベントリスナーを追加
button.onclick = (e: dom.MouseEvent) => {
// マウスクリックイベントを処理
output.textContent = s”Button clicked at (${e.clientX}, ${e.clientY})”
}
“`
getElementById
の戻り値は dom.Element
なので、asInstanceOf
を使ってより具体的な型(html.Button
など)にキャストすることで、その要素固有のプロパティ(onclick
など)にアクセスできるようになります。
3.3. 非同期処理
現代のフロントエンド開発では非同期処理が不可欠です。JavaScriptでは Promise
や async/await
が使われますが、Scalaにはより強力なFuture
という抽象化があります。
Scala.jsでは、Scala標準の Future
がJavaScriptの Promise
と相互変換可能になっており、シームレスに連携できます。
Future
を使うには、ExecutionContext
が必要です。Scala.jsでは特別な設定は不要で、以下のようにインポートするだけで準備が整います。
scala
import scala.concurrent.ExecutionContext.Implicits.global
ブラウザの fetch
APIを Future
を使ってラップする例を見てみましょう。
“`scala
import org.scalajs.dom
import scala.concurrent.Future
import scala.scalajs.js.Thenable.Implicits._
import scala.util.{Failure, Success}
// 1. fetchはPromiseを返すので、ScalaのFutureに変換する
val userFuture: Future[dom.Response] = dom.fetch(“https://api.github.com/users/scala-js”)
// 2. レスポンスのJSONをパースする処理も非同期
val jsonFuture: Future[String] = userFuture.flatMap { response =>
if (response.ok) {
response.text() // これもPromise[String]を返すのでFuture[String]に変換される
} else {
Future.failed(new Exception(s”Request failed: ${response.statusText}”))
}
}
// 3. 最終的な結果を処理する
jsonFuture.onComplete {
case Success(jsonString) =>
println(“Successfully fetched user data:”)
println(jsonString)
case Failure(exception) =>
println(s”An error occurred: ${exception.getMessage}”)
}
``
flatMapを使うことで、非同期処理を繋げていくことができます。さらに、Scalaの
for`式を使うと、この非同期処理の連鎖を、まるで同期処理のように直感的に記述できます。
“`scala
val jsonForComprehension: Future[String] = for {
response <- dom.fetch(“https://api.github.com/users/scala-js”)
if response.ok
text <- response.text()
} yield text
jsonForComprehension.foreach(println)
``
for
この式は、内部的には
flatMapと
map` に展開されます。コールバック地獄を回避し、クリーンで可読性の高い非同期コードを書くための非常に強力なツールです。
第4章:実践的なフロントエンド開発
基本的な機能がわかったところで、より実践的なアプリケーション開発で使われるUIライブラリやビルドツールについて見ていきましょう。
4.1. UIライブラリの選択
素のDOM APIを直接操作するのは、小規模なアプリケーションでは問題ありませんが、状態管理が複雑になるにつれてコードが破綻しやすくなります。ReactやVueのようなUIライブラリは、UIをコンポーネントという単位で分割し、状態(State)とUIの見た目を宣言的に記述することで、この問題を解決します。
Scala.jsの世界にも、この宣言的UIのパラダイムを実現するための優れたライブラリがいくつか存在します。
1. Laminar
Laminar は、現在Scala.jsコミュニティで最も人気のあるUIライブラリの一つです。純粋なScalaで書かれており、外部のJavaScriptライブラリへの依存がありません。
Laminarの核となるコンセプトはFRP (Functional Reactive Programming) です。UIの要素やイベントを「ストリーム」として扱い、それらを組み合わせたり変換したりしてアプリケーションを構築します。
Var[A]
: 読み書き可能なリアクティブな変数(状態)。Signal[A]
: 読み取り専用のリアクティブな値。EventStream[A]
: イベントのストリーム。$
(オブザーバー):Var
やSignal
の変更を購読し、DOMの属性や子要素に反映させるための構文。
Laminarを使った簡単なカウンターアプリの例を見てみましょう。
“`scala
import com.raquo.laminar.api.L._
import org.scalajs.dom
object CounterApp {
def main(args: Array[String]): Unit = {
// 1. リアクティブな状態 (counterVar) を定義
val counterVar = Var(0)
// 2. UIを宣言的に記述
val appElement = div(
h1("Super Counter"),
button(
"–",
// クリックされたらcounterVarを-1する
onClick --> { _ => counterVar.update(c => c - 1) }
),
// counterVarの現在の値を表示。値が変わると自動で更新される。
span(
" ",
child.text <-- counterVar.signal, // signalを購読
" "
),
button(
"+",
// クリックされたらcounterVarを+1する
onClick --> { _ => counterVar.update(c => c + 1) }
)
)
// 3. DOMにマウント
render(dom.document.getElementById("app"), appElement)
}
}
“`
Laminarのコードは非常に宣言的です。child.text <-- counterVar.signal
という記述は、「このspan
要素のテキスト内容は、counterVar
のsignal
が発する値に常に追従する」という関係性を定義しています。開発者は状態(counterVar
)を更新するだけでよく、DOMの更新はライブラリが自動的に行ってくれます。
2. Tyrian
Tyrian は、Elmアーキテクチャ(TEA)に強くインスパイアされたUIライブラリです。TEAは、状態管理のパターンを厳格に定めることで、アプリケーションの挙動を非常に予測しやすくします。
TEAは3つの主要な要素で構成されます。
Model
: アプリケーションのすべての状態を保持する不変のデータ構造。View
:Model
を受け取り、UI(仮想DOM)を返す純粋関数。Update
:Msg
(メッセージ、ユーザーのアクションなどを表現)と現在のModel
を受け取り、新しいModel
とCmd
(コマンド、副作用を表現)を返す純粋関数。
Tyrianでのカウンターアプリは以下のようになります。
“`scala
import tyrian.
import tyrian.Html.
// 1. Model: アプリケーションの状態
final case class Model(count: Int)
// 2. Msg: アプリケーションで起こりうるイベント
enum Msg:
case Increment, Decrement
// 3. Update: Msgに応じてModelをどう更新するか
def update(msg: Msg, model: Model): (Model, Cmd[IO, Msg]) =
msg match
case Msg.Increment => (model.copy(count = model.count + 1), Cmd.None)
case Msg.Decrement => (model.copy(count = model.count – 1), Cmd.None)
// 4. View: ModelをUIに変換する
def view(model: Model): Html[Msg] =
div()(
h1(“Tyrian Counter”),
button(onClick(Msg.Decrement))(“-“),
span(s” ${model.count} “),
button(onClick(Msg.Increment))(“+”)
)
// アプリケーションのエントリーポイント
class CounterApp extends TyrianApp[Msg, Model] {
def init(flags: Map[String, String]): (Model, Cmd[IO, Msg]) =
(Model(0), Cmd.None)
def subscriptions(model: Model): Sub[IO, Msg] =
Sub.None
def update(model: Model): Msg => (Model, Cmd[IO, Msg]) =
msg => update(msg, model)
def view(model: Model): Html[Msg] =
view(model)
}
``
update`関数に完全に集約されるため、アプリケーションがどこでどのように変化するのかが一目瞭然になります。これにより、デバッグやテストが非常に容易になります。
Laminarと比べると少し記述量は増えますが、状態の更新ロジックが
3. scalajs-react
scalajs-react は、その名の通り、絶大な人気を誇るJavaScriptライブラリ React をScala.jsから型安全に利用するためのラッパーライブラリです。
利点:
* Reactの巨大なエコシステム(コンポーネントライブラリ、UIツールキット、開発ツールなど)を最大限に活用できます。
* 仮想DOMによる高いパフォーマンスが期待できます。
* Reactの知識がある開発者にとっては学習コストが低いです。
欠点:
* ReactというJavaScriptライブラリへの依存が発生します。
* LaminarやTyrianと比べると、よりボイラープレートコードが多くなりがちです。
scalajs-reactは、Reactのコンポーネント、State、Props、ライフサイクルといった概念を、Scalaの型システムを使って静的に検証できるようにラップします。
4.2. ビルドとバンドル
現代のフロントエンド開発では、複数のJavaScript/CSSファイルを一つにまとめる「バンドル」というプロセスが不可欠です。これにより、パフォーマンスの向上やモジュール管理の容易化が図られます。この役割を担うのがWebpackやViteといったJavaScriptのビルドツールです。
Scala.jsプロジェクトをこれらのモダンなJSツールチェーンと統合するために、sbt-scalajs-bundler
というsbtプラグインが広く使われています。
sbt-scalajs-bundler
を使うと、以下のことが可能になります。
npm
で管理されているJavaScriptライブラリ(React, d3.js, etc.)をsbtのビルド設定から直接利用できます。- sbtのタスクからWebpackを呼び出し、Scala.jsが生成したJSと他のJSライブラリ、CSSなどをまとめてバンドルできます。
- Webpack Dev Serverと連携し、コードの変更を即座にブラウザに反映させるHot Module Replacement (HMR) を実現できます。
設定は build.sbt
に少し追記するだけで済み、これによりScala.jsの開発体験は、一般的なTypeScript+React開発と遜色のない、非常にモダンで快適なものになります。
4.3. Full-Stack Scalaアプリケーションの構築
Scala.jsの最もユニークで強力な特徴の一つが、サーバーサイドとクライアントサイドでコードを共有できることです。sbtのマルチプロジェクトビルド機能を使うと、これを非常に綺麗に実現できます。
一般的なプロジェクト構成は以下のようになります。
full-stack-app/
├── client/ # Scala.jsのコード (フロントエンド)
├── server/ # JVM(Akka HTTP, http4sなど)のコード (バックエンド)
├── shared/
│ └── src/main/scala # サーバーとクライアントで共有するコード
└── build.sbt
build.sbt
では、client
, server
, shared
という3つのサブプロジェクトを定義します。そして、client
とserver
の両方がshared
プロジェクトに依存するように設定します。
build.sbt
(抜粋)
“`scala
// 共有プロジェクト (Scala/JVM と Scala/JS の両方にコンパイル)
lazy val shared = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file(“shared”))
.settings(
// 共有ライブラリの依存関係 (例: JSONライブラリ)
libraryDependencies += “com.lihaoyi” %%% “upickle” % “3.1.3”
)
// フロントエンドプロジェクト (Scala.js)
lazy val client = project
.in(file(“client”))
.enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin)
.dependsOn(shared.js) // sharedのJS版に依存
// バックエンドプロジェクト (JVM)
lazy val server = project
.in(file(“server”))
.dependsOn(shared.jvm) // sharedのJVM版に依存
“`
このshared
プロジェクトには、以下のようなコードを置きます。
-
データモデル: APIで通信するための
case class
。
scala
// shared/src/main/scala/com/example/model/User.scala
case class User(id: String, name: String, email: String)
このUser
クラスは、サーバーサイドではDBとのマッピングに使え、クライアントサイドではAPIレスポンスのパースに使えます。同じ定義を共有するため、型が一致しないというミスが起こりえません。 -
APIの定義: APIのエンドポイントやリクエスト/レスポンスの型を定義した
trait
。
scala
// shared/src/main/scala/com/example/api/MyApi.scala
trait MyApi {
def getUser(userId: String): Future[User]
def updateUser(user: User): Future[Boolean]
}
このtrait
を、サーバーサイドでは実装し、クライアントサイドではHTTPクライアントとして実装することで、型安全な通信が実現できます(tapir や autowire といったライブラリがこれをさらに自動化してくれます)。 -
共通ロジック: バリデーションルールなど、サーバーとクライアントの両方で必要なロジック。
このFull-Stackアプローチにより、開発効率とアプリケーションの堅牢性は劇的に向上します。
第5章:Scala.jsエコシステムと学習リソース
Scala.jsは、それ自体がコンパイラであると同時に、豊かなエコシステムの上に成り立っています。
5.1. 便利なライブラリ
フロントエンド開発で役立つ、クロスプラットフォーム対応のScalaライブラリをいくつか紹介します。
- テスティング:
scalatest
: Scalaで最も広く使われているテスティングフレームワーク。豊富なマッチャースタイルを提供します。munit
: 軽量でモダンなテスティングフレームワーク。セットアップが簡単です。
- JSON処理:
uPickle
: シンプルで高速なJSONシリアライズ/デシリアライズライブラリ。circe
: 型クラスベースの、非常に強力で型安全なJSONライブラリ。
- 日付・時刻:
scala-js-java-time
: Java 8のjava.time
APIのScala.js実装。サーバーサイドと同じAPIで日付や時刻を扱えます。
- その他:
scala-async
:Future
をasync/await
スタイルで書けるようにするマクロライブラリ。scala-parser-combinators
: テキストパーサーを構築するためのライブラリ。
5.2. 学習のためのリソース
Scala.jsを学び始めるための優れたリソースが揃っています。
- 公式ドキュメント: 全てはここから始まります。基本から相互運用性の詳細まで、網羅的に解説されています。
- Scala.js Hands-on Tutorial: 公式サイトにある実践的なチュートリアル。実際に手を動かしながら学べます。
- Laminar公式サイト: Laminarの使い方だけでなく、リアクティブプログラミングの考え方についても優れた解説がなされています。
- Gitter/Discord: Scala.jsのGitterチャンネルやScalaコミュニティのDiscordサーバーには、開発者が集まっており、質問をすれば親切に答えてくれるでしょう。
- 各種ブログやカンファレンス動画: “Scala.js” や選択したUIライブラリの名前で検索すると、多くの高品質な技術記事や発表動画が見つかります。
結論
Scala.jsは、TypeScriptがWebフロントエンド開発にもたらした「静的型付けによる安全性」という恩恵を、さらに高いレベルへと引き上げる可能性を秘めたテクノロジーです。
それは、単にブラウザでScalaが書けるというだけではありません。
- 比類なき型安全性: Scalaの表現力豊かな型システムは、ビジネスロジックの不整合やありえない状態といった、より高度なレベルのバグをコンパイル時に検出します。
- 優れた開発者体験: 不変性を基本とするデータ構造、強力なパターンマッチ、
for
式によるエレガントな非同期処理は、コードを簡潔で、予測可能で、そして書くのが楽しいものにしてくれます。 - 究極のコード共有: サーバーとクライアントでデータモデルやAPI定義を共有するFull-Stack Scalaのアプローチは、開発の重複をなくし、アプリケーション全体の一貫性と堅牢性を劇的に向上させます。
もちろん、Scala.jsは万能薬ではありません。Scala言語自体の学習コストはJavaScriptやTypeScriptよりも高く、コミュニティやライブラリの規模もJavaScriptエコシステムに比べれば小さいのが現実です。そのため、小規模なプロジェクトや、迅速なプロトタイピングが最優先される場面では、既存のJSフレームワークの方が適しているかもしれません。
しかし、長期的なメンテナンス性が求められる大規模なWebアプリケーション、複雑なビジネスロジックを持つドメイン駆動設計のフロントエンド、そして何より型による安全性を徹底的に追求したいプロジェクトにおいて、Scala.jsは他の選択肢を凌駕するほどの強力な価値を提供します。
Webフロントエンドの世界は、今やTypeScriptが主流です。しかし、技術の進化は止まりません。Scala.jsは、その次に来るかもしれない、より堅牢で、より表現力豊かで、より一貫性のある開発スタイルの扉を開く鍵となります。
もしあなたが、現在のフロントエンド開発に何らかの限界を感じ、より良い方法を探求したいと願うのであれば、ぜひScala.jsの世界に足を踏み入れてみてください。そこには、あなたのエンジニアとしての知的好奇心を満たし、ソフトウェア構築の新たな喜びを発見させてくれる、刺激的なフロンティアが広がっています。