【Spring Boot】TypeScript 導入方法

&emsp;

:::note
*本記事の目的*

**Spring Boot アプリ開発で TypeScript が使えるようになる**
:::
&emsp;

Spring Boot に TypeScript を導入する手順は、次の順番で説明します。

1. 開発環境
1. 導入の準備
1. 具体的な導入手順《本編》
1. TypeScript が反映されないときの対処法

「開発環境」では、本記事で解説する内容が正しく挙動したことを実際に視認できた条件・環境を整理しました。「導入の準備」では、本格的な導入の前までに実装しておきたいことを整理しました。「具体的な導入手順《本編》」では、実際にSpring Boot へ TypeScript を組み込む方法を整理しました。「TypeScriptが反映されないときの対処法」では、ボク自身が初めてSpring Boot アプリ開発で TypeScript を使った際、困った点とその解決法をまとめました。

&emsp;

ムダな前置きは省いて、早速解説します :dash:
&emsp;

> ここから先の解説内容は、Spring Boot 以外のアプリ開発（Ex：Rails とか）でも、ほぼ同じ原理・方法で導入できるので、気になった方はチラ見・参照・ブクマしてください。

&emsp;

# ◆ 開発環境

| Category        | Value,Version                 |
| :-------------: | :---------------------------: |
| OS              | MacOS Sonoma 14.3.1           |
| Project	      | Gradle Groovy                 |
| Language        | Java 17                       |
| Framework       | Spring Boot 3.1.9             |
| Group           | com.project                   |
| Artifact        | sprigbootproto                |
| Name            | SprigBootProto                |
| Package name    | com.project.sprigbootproto    |
| Packaging       |  Jar                          |
| Dependencies    | Spring Web, Thymeleaf         |
| Editor          | IntelliJ IDEA CE              |

:::note warn 
ここから先は「↑の内容に沿って [spring initializr](https://start.spring.io/) で作成したプロジェクトを、IntelliJで開いている」という状況を想定／前提として、解説を進めます。
:::
&emsp;
&emsp;

# ◆ 導入の準備
&emsp;

ここでは「Spring Boot アプリへの TypeScript 導入《準備》」を、次の手順で済ませます。

1. トップページの表示
1. package.json のインストール
&emsp;

各ステップの詳細は、実際のコードを見せながら解説します :relaxed:
&emsp;
&emsp;

## 準備①：トップページの表示
&emsp;

ここでは、Spring Bootアプリのトップページに `index.html` を表示させていきます。
&emsp;

まず、`src/main/resources/templates/` 内に `index.html` ファイルを作成し、中身をカスタマイズします。
```html:index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Top</title>
    </head>
    <body>
        <h1>Welcome!</h1>
        <p>Lorem Ipsum</p>
    </body>
</html>
```
&emsp;

次に、`src/main/java/com.project.sprigbootproto/`へ`RequestCntroller`（任意の名前）というコントローラー用のJavaクラスを作成します。

そして、「`index.html` ＝ トップページを表示して 🥺 」というリクエストに対応する処理を、次のように記述します。
```java:RequestCntroller.java
package com.project.sprigbootproto;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class RequestController {
    @GetMapping /* ("/") はアリ／ナシどっちでもOK */
    public String index(){
        return "index";
    }   
}
```
&emsp;

ここで一度、アプリを実行して `localhost:8080` へアクセスし、トップページが正しく表示されるかどうかを確認しましょう。

正しく表示されていることを確認できたら、アプリの実行を停止します。
&emsp;

これで、準備①「トップページの表示」は完了です :tada:
&emsp;
&emsp;

## 準備②： package.json のインストール
&emsp;

ここでは、プロジェクトに `package.json` を追加します。
&emsp;

まず、ターミナルで次のコマンドを実行します。
```zsh:Terminal
(アプリのディレクトリへ移動)

% npm init -y   # -y オプションを付ければ、単に `npm init` を実行したときに答えなきゃいけない ”yes問答” を全スキップできる
```
プロジェクトに次のファイルが追加されれば、成功です。
 - [x] `package.json`
&emsp;
 
> 「`Node.js`？ `npm` ？？ 何やそれ :innocent: 」という状態のとき、ひとまずのザックリ理解に役立った記事を紹介しておきますね。
>
> [オススメ記事：そろそろ適当に npm install するのを卒業する](https://zenn.dev/ikuraikura/articles/71b917ab11ae690e3cd7)

&emsp;

これで、準備②「package.json のインストール」は完了です :tada:
&emsp;
&emsp;

# ◆ 具体的な導入手順《本編》
&emsp;

ここでは、「Spring Boot アプリへの TypeScript 導入《本編》」を、次の3つの構成で解説します。

1. TypeScript・webpack のインストール
1. webpack のカスタマイズ
1. TypeScriptの挙動確認
&emsp;

各ステップの詳細は、実際のコードを見せながら解説します :relaxed:
&emsp;
&emsp;

## STEP①：TypeScript・webpack のインストール
&emsp;

ここでは、プロジェクトへ`typescript` と `webpack` （ + 関連パッケージ）をインストールします。
&emsp;

先に、`typescript` だけインストールします。

具体的には、ターミナルで次のコマンドを実行します。
```zsh:Terminal
% npm install -D typescript   # `-D` は `--save-dev` でもOK
```

プロジェクトに次のファイル追加／編集がされれば、成功です。
 - [x] プロジェクトのルートディレクトリに `node_modules`
 - [x] `package.json` > `devDependencies` に `typescript:"(ver)"`
 - [x] `package-lock.json`
&emsp;

> 「`devDependencies`？ 何やそれ :innocent: 」という状態のとき、ひとまずのザックリ理解に役立った記事を紹介しておきますね。
>
> [オススメ記事：勉強メモ/npmの使い方(node.js=v0.11.16, npm=2.3.0, 2015-03時点)](https://qiita.com/msakamoto_sf/items/a1ae46979a42d6948ebd)

&emsp;

続いて、`webpack` をはじめとした次のパッケージをインストールします。

* `webpack`：モジュールバンドラー
* `webpack-cli`：ターミナル上で webpack を実行するためのツール
* `ts-loader`：TypeScript 用ローダ

具体的には、ターミナルで次のコマンドを実行します。
```zsh:Terminal
% npm install -D webpack webpack-cli ts-loader
```
&emsp;

これで、STEP①「TypeScript・webpack のインストール」は完了です :tada:
&emsp;
&emsp;

## STEP②：webpack のカスタマイズ
&emsp;

ここでは、プロジェクトで TypeScript が使えるように `webpack` を細かく設定します。

&emsp;

まず、ルートディレクトリに `webpack.config.js` というファイルを作成します。

続けて、`webpack.config.js` を次のように編集します。
```javascript:webpack.config.js
const path = require('path')   // Node.js の `path` モジュール（ファイルパス操作の関数を提供）を読み込み

module.exports = {
    // ココにカスタマイズしたい内容を記述していく
}
```

これで、webpack をカスタマイズしていくための雛形が完成したので、中身を埋めていきます。

具体的には、次の設定が必要です。

* エントリポイントの指定
* ローダの設定
* インポートの効率化
* 出力先の設定
* 環境の設定

&emsp;

### STEP②-1：エントリポイントの指定
&emsp;

ここでは、`entry`：エントリポイント（wecpack がモジュールをまとめてバンドルをつくるとき、そのモジュールの依存関係の解析を開始するファイル）を指定・作成します。
&emsp;

具体的には、次の記述をします。
```javascript:webpack.config.js
const path = require('path')

module.exports = {
    entry: './src/main/typescript/application.ts'
}
```
続けて、↑で指定したエントリポイントに合うように、main ディレクトリへ `typescript`（任意の名前）ディレクトリを、そしてその`typescript` ディレクトリに `application.ts`（任意の名前）ファイルを作成します。
&emsp;

これで、STEP②-1「エントリポイントの指定」は完了です :tada:
&emsp;
&emsp;

### STEP②-2：ローダの設定
&emsp;

ここでは、ローダ（wecpack が TypeScript のファイルをすべてロードするとき、そのロード対象の拡張子 `.ts` や、使うローダの指定など）を設定します。
&emsp;

具体的には、次の記述をします。

```javascript:webpack.config.js
const path = require('path')

module.exports = {
    entry: './src/main/typescript/application.ts',
    module: {
        rules: [
            {
             test: /\.ts$/,
             use: 'ts_loader',   // package.json にインストール済み
             exclude: /node_modules/,   // `exclude`の理由：パフォーマンスの最適化・・・通常、`node_modules`内のファイルはトランスパイルの必要がないため、`exclude`処理でビルドプロセスから除外
            },
        ],
    }
}
```
これで、STEP②-2「ローダの設定」は完了です :tada:
&emsp;
&emsp;

### STEP②-3：インポートの効率化
&emsp;

ここでは、インポートの効率化として、拡張子を省略できる（Ex：エントリポイントへのインポート時）ようにします。
&emsp;

具体的には、次の記述をします。
```javascript:webpack.config.js
const path = require('path')

module.exports = {
    entry: './src/main/typescript/application.ts',
    module: {
        rules: [
            {
             test: /\.ts$/,
             use: 'ts_loader',
             exclude: /node_modules/,
            },
        ],
    },
    resolve: {
        extensions: ['.ts', '.js']
    } 
}
```
これで、STEP②-3「インポートの効率化」は完了です :tada:
&emsp;
&emsp;

### STEP②-4：出力先の設定
&emsp;

ここでは、wecpack が作成したバンドルの出力先を設定します。
&emsp;

具体的には、次の記述をします。
```javascript:webpack.config.js
const path = require('path')

module.exports = {
    entry: './src/main/typescript/application.ts',
    module: {
        rules: [
            {
             test: /\.ts$/,
             use: 'ts_loader',
             exclude: /node_modules/,
            },
        ],
    },
    resolve: {
        extensions: ['.ts', '.js']
    },
    output: {
        filename: 'bundle.js'
        path: path.resolve(__dirname, 'src/main/resources/static/javascript'),
    },
}
```
`output` の各要素は、次の意味を持ちます。

- [x] `bundle.js`：出力先となるJavaScriptファイル***
- [x] `path`：1行目で読み込んだ、Node.js の `path` モジュール
- [x] `resolve`：`path` モジュールが持つ `resolve` 関数（引数に渡されたパスから絶対パスを生成する）の呼び出し
- [x] `__dirname`：Node.jsのグローバル変数で、現在実行中のスクリプト（今回は `webpack.config.js`）が存在するディレクトリの絶対パスを表す。
- [x] `'src/main/resources/static/javascript'`：出力先となるJavaScriptファイル、つまり、`bundle.js`が存在するディレクトリ***の相対パスを表す。

*** `bundle.js` ファイルと `javascript/` ディレクトリは、あとでおこなうコマンド操作時に自動で生成されるので、自作しなくてOK
&emsp;

まとめると、`output`の`filename`と`path`では、`path.resolve(__dirname, 'src/main/resources/static/js') `によって、`webpack.config.js` が存在するディレクトリの絶対パス `__dirname` と`'src/main/resources/static/javascript'` という相対パスを結合して、 TypeScript ファイルからトランスパイルされた JavaScript ファイルが出力されるディレクトリの絶対パスを生成されています（この絶対パスは、webpackの設定である output.path に設定され、そこにファイルが出力されるようになります）
&emsp;

これで、STEP②-4「出力先の設定」は完了です :tada:
&emsp;
&emsp;

### STEP②-5：環境設定
&emsp;

ここでは、環境を設定します。
> 環境設定はしておきましょう。というのも、何かしら設定しておかないと、コマンド実行時に以下の警告文が表示されます。
>```zsh:Terminal
>WARNING in configuration
>The 'mode' option has not been set, webpack will fallback to 'production' for this value.
>Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
>You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
>```

&emsp;

今回は「開発環境」を想定し、次の記述をします。
```javascript:webpack.config.js
const path = require('path')

module.exports = {
    entry: './src/main/typescript/application.ts',
    module: {
        rules: [
            {
             test: /\.ts$/,
             use: 'ts_loader',
             exclude: /node_modules/,
            },
        ],
    },
    resolve: {
        extensions: ['.ts', '.js']
    },
    output: {
        filename: 'bundle.js'
        path: path.resolve(__dirname, 'src/main/resources/static/javascript'),
    },
    mode: 'development',
}
```

> `mode`を`production`に設定すると、出力先となる`builde.js`の記述量が本番環境向けに短く圧縮されます。

&emsp;

これで、STEP②-5「環境の設定」は完了です :tada:

そして、STEP②「webpack のカスタマイズ」全体も完了です :tada::tada:
&emsp;
&emsp;

## STEP③：TypeScriptの挙動確認
&emsp;

ここでは、TypeScript をプロジェクトへ本格的に組み込んでいきます。

具体的には、次の実装が必要です。

* TypeScriptの設定
* TypeScriptの記述
* Webページでの描画確認

&emsp;
&emsp;

### STEP③-1：TypeScriptの設定
&emsp;

ここでは、具体的なTypeScriptを書いていく前に、TypeScript 自体の設定をします。


具体的には、ターミナルで次のコマンドを実行します。
```zsh:Terminal
% npx tsc --init   # `init` だとエラーが出るので注意！
```
次の表示／追加／編集があれば、成功です。
 - [x] ターミナルに↓の表示
 - [x] プロジェクトのルートディレクトリに `tscinfig.json` 追加
```zsh:Terminal
% npx tsc --init

Created a new tsconfig.json with:
  target: es2016
  module: commonjs
  strict: true
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true


You can learn more at https://aka.ms/tsconfig
```
&emsp;

ここで、追加された `tscinfig.json` の中身を見てください。

そして、次の`"compilerOptions"`というハッシュ配列の中で、以下をコメントアウトしてください（その他デフォルトでコメントアウトが外れていたキー／バリューは、基本的に外しっぱなしでOK）
```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dist",
    "esModuleInterop": true
    "strict": true
  }
}
```

続けて、次の`"include"`と`"exclude"`という配列を追加
```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dist",
    "esModuleInterop": true
    "strict": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]    /* パフォーマンスの最適化：通常、`node_modules`内のファイルはトランスパイルの必要がないため、`exclude`によりビルドプロセスから除外 */
}
```
これで、STEP③-1「TypeScriptの設定」は完了です :tada:
&emsp;
&emsp;

### STEP③-2：TypeScriptの記述
&emsp;

ここでは、実際に簡単なTypeScriptを書いていきます。

具体的には、導入の準備で用意した`index.html`上の`div`タグに、”Hello World”といった好きな文字列を表示させるためのTypeScriptを記述します。
&emsp;

まず、`/src/main/typescript/`ディレクトリ、つまり、`application.ts`と同じディレクトリに`demoCheck.ts`（任意の名前）を作成します。

続けて、作成した`.ts`ファイルを次のように編集します。
```typescript:demoCheck.ts
exports default class Greeting {
    phrase: string;
    
    constructor(phrase: string) {
        this.phrase = phrase
    }
    
    public sayPhrase(element: HTMLElement | null){
        if(element) {
            element.innerText = this.phrase
        }
    }
}
```
&emsp;


次に、`index.html`を次のように編集します。
```html:index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Top</title>
    </head>
    <body>
        <h1>Welcome!</h1>
        <p>Lorem Ipsum</p>
        <div id="demo"></div>   <!-- 追加 -->
    </body>
</html>
```
&emsp;

次に、`application.ts`（エントリポイント）を次のように編集します。
```typescript:application.ts
import Greeting from "./demoCheck";

const demo: HTMLElement | null = document.getElementById("demo");

const phrase = new Greeting("Hello world");

phrase.sayPhrase(demo);
```
これで、STEP③-2「TypeScriptの記述」は完了です :tada:
&emsp;
&emsp;

### STEP③-3： Webページでの描画確認
&emsp;

それでは、TypeScriptが思い通りに処理できているかどうかをブラウザで確かめます。
&emsp;

まず、TypeScriptをトランスパイルした結果を`static`ディレクトリにJavaScriptファイル `bundle.js`として出力するために、`package.json`の`"scripts"`を次のように編集します。
```json:package.json
{
  "name": "sprigbootproto",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"   /// 編集
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "ts-loader": "^9.5.1",
    "typescript": "^5.3.3",
    "webpack": "^5.90.3",
    "webpack-cli": "^5.1.4"
  }
}
```
&emsp;

次に、ターミナルで次のコマンドを実行します。
```zsh:Terminal
% npm run build
```
プロジェクトの`static`ディレクトリに、以下のディレクトリ／ファイルが追加されれば、成功です。
 - [x] `javascript/`
 - [x] `bundle.js`

この操作によって、作成したTypeScriptファイルがコンパイルされて、それらがバンドルされたものが`bundle.js`に出力されるようになりました。

&emsp;

続けて、`index.html`の`<body>`内を次のように編集します。
```html:index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Top</title>
    </head>
    <body>
        <h1>Welcome!</h1>
        <p>Lorem Ipsum</p>
        <div id="demo"></div>   
        <script src="/javascript/bundle.js"></script>   <!-- 追加 -->
    </body>
</html>
```
この追記によって、DOMに変換されたHTMLにJavaScriptが装飾されるようになりました。
&emsp;

ここまで来たら、Spring Bootアプリを起動し、`localhost:8080`にアクセスします。

そして、STEP③-2で指定した文字列が表示されていれば成功です:thumbsup_tone1:
&emsp;

これでSTEP③完了、つまり「**Spring BootへのTypeScript導入手順**」は終了です :tada::tada::tada:
&emsp;

お疲れ様でした。
&emsp;
&emsp;

# ◆ TypeScriptが反映されないときの対処法
&emsp;

ここでは「Spring BootアプリにTypeScriptが反映されない... :cry: 」と困ったときに当てはまるかもしれない、未反映の原因とその解決策を紹介します。

* 再ビルド忘れ
* bundle.js へのパス指定ミス

&emsp;

各原因の具体的な解決策は、以降で提案しますね。
&emsp;
&emsp;

## 原因①：再ビルド忘れ
&emsp;

「再ビルド（コンパイル）がおこなわれていない」ことが原因で TypeScriptの変更が反映されていない可能性があります...

そんなときは、次を試してみてください。

:::note
解決策①：**CLIからビルドする**
```zsh:Terminal
% npm run build
```
:::

繰り返しになりますが、Spring Bootでは静的リソース、つまり `resources/static/` ディレクトリ内の `.css` や `.js` などのファイルの「コンパイル→再ビルド」というプロセスが、デフォルトだと自動でおこなわれません。

したがって、↑のコマンドを打たないままTypeScriptを編集→アプリを実行（サーバーを起動）しても、編集内容はビルドされない、つまりTypeScriptが反映されない... となるので、注意しましょう。

&emsp;

## 原因②：bundle.js へのパス指定ミス
&emsp;

「HTMLファイル上で`build.js`へのパスが正しく設定されていない」ことが原因で TypeScriptが反映されていない可能性があります...

そんなときは、次を試してみてください。

:::note
解決策②：**`build.js`ファイルのパス指定が正しいかどうかを確認する**
```html:index.html
<body>
    <script src="/javascript/bundle.js"></script>
</body>
```
正しい指定位置は、ブラウザのデベロッパツールから確認可能です。

具体的には、`localhost:8080/`をルートディレクトリとし、ブラウザに描画されている`.js`ファイルの階層構造が見れます。
[![Image from Gyazo](https://i.gyazo.com/bdbc53d7a39dddf0531588a9321af74e.png)](https://gyazo.com/bdbc53d7a39dddf0531588a9321af74e)
:::

&emsp;
&emsp;
&emsp;

プログラミング学習のアウトプット、以上になります。
ご指摘あれば、お気軽にコメントください。
&emsp;

[参考 + 感謝：【Typescript入門】本当の初心者からTypescript×Webpackの開発環境構築までをハンズオン形式で学ぼう！](https://youtu.be/ECc1EXnx7VQ?si=LdZse4vFzNTTm_cg)
[参考 + 感謝：TypeScript《webpack公式》](https://webpack.js.org/guides/typescript/)