SSR対応SPAアプリの作り方(React/TypeScript/Express)

Next.jsを使わないReactのSSR解説で分かりやすいやつが無かったので、書きます！

環境構築からとっても丁寧にやっていきます:cactus:


##最初に

### 読んで欲しい人

* 『**SSR**したいけど難しそう....:fearful:』

  って人はもちろん

* 『何も分からないけど**SPA**が作ってみたい！:smiley:』

  って人も実際に作って楽しめるように書いてます。

> **SPA** (Single Page Application) : ネイティブアプリ（AppStoreやGooglePlayからインストールできるやつ）っぽいWebサイトのこと。ReactやVueなどのJavaScriptフレームワークを使って作るのが一般的です。



### 作るもの

SSRを使用した簡単なカウントアップアプリを作ってみます。

![countup.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227791/737f87de-0f92-cddd-3a21-c8415d4673c7.gif)


最終的な制作物のソースコードは[こちら](https://github.com/Michin0suke/ssr-sample)です。



### 学べること

* Node, Yarn, Git, ESList, Babel, Webpackのフロントエンド開発環境の構築。
* React Hooks（useState）を使って、モダンな書き方のReactを書いてみる。
* TypeScriptを使ってみる。
* Express.jsを使ってSSRができるようになる。

> ReactやTypeScriptの詳しい記法については踏み込みません。良質な記事がQiita等にたくさんあるので、気になるところは調べながら進めてください。



### 進め方について

なるべくコピペはせず、**何をしているか考えて**進めていただきたいです。

設定ファイルを暗記するのは無駄なので、**設定ファイルはコピペ**してください。



## SSR(Server Side Rendering)とは

Reactなどで作成されたSPAアプリは、クライアント（ユーザーが使っているブラウザ）で実行され、UI (DOM)を形成します。その性質上、以下のような問題があります。

+ Googleのクローラが正しくインデックスしてくれない。
+ OGPを含む`<head/>`が全てのページで同じになるので、Twitterカードなどが意図通りに表示できない。
+ 初回表示速度（クリティカルレンダリングパス）に時間がかかる。

そこで、サーバー側でJavaScriptをHTMLをして返すことを**SSR**（Server Side Rendering）と呼びます。やっていることはほとんどPHPなどのサーバーサイド言語と同じですが、SSRと呼ぶので難しく感じるだけです。

![not-ssr.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227791/9dd75e20-2a0c-eee9-0a5c-ab5f84f6eab4.jpeg)


この記事ではSSRを解説しますが、小規模な（ページ数が限定されている）プロダクトでは、SSRの代わりに**Prerendering**という手法を検討した方がいいです。これは、あらかじめ**SSG**（Static Site Generator）を使用して、JavaScriptをHTMLファイルに変換して配置しておく方法です。SSRより簡潔で、リクエストごとにサーバーでHTMLを生成する処理が必要がないので、**メンテナンスがしやすい**ことと、**サーバーにかかる負荷がSSRより小さい**ことがメリットです。



> * **クローラ** **:** Googleが世界中にあるサイトを把握するために、いろんなサイトを巡回させているロボットのこと。クローラがサイトの情報を保存することを**インデックス**と呼びます。最近GoogleのクローラはJavaScriptを実行してくれるようになったらしいですが、まだ完全ではなく**SEO**が劣る可能性があります。
>
> * **SEO** (Search Engine Optimization) **:** Googleなどの検索結果の上に方に表示されるように調整すること。
>
> * **UI** (User Interface) **:** サイトの見た目のこと。
>
> * **DOM** (Document Object Model) **:** ブラウザがHTMLを元に形成するツリー状の構造のこと。よく混同されますがHTMLとは別のものです。JavaScriptから操作できます。
>
> * **OGP** (Open Graph Protocol) **:** Twitterカードなど、SNS上でリンク先の情報を表示するときに使われるheadタグです。**OGPをクロールに来るbotはGoogleのbotと違ってJavaScriptを解釈しません。**



## 環境構築

<font color="Crimson">**フロントエンド(クライアントサイド)の環境構築はとても面倒で楽しくないです。環境構築が終われば楽しいので、投げ出さずに頑張ってください！**</font>



### 推奨環境

環境構築は**macOS**を使っている前提で進めます。

Windows等ご利用の方は別途調べながら進めてみてください。



### Homebrew

Macのパッケージマネージャです。パッケージとはソフトウェアのこと。Qiitaなど多くの記事でインストールしてあることが前提になっているので、Macで開発するなら必須です。

1. AppStoreから[Xcode](https://apps.apple.com/jp/app/xcode/id497799835?mt=12&ign-mpt=uo%3D4)をインストールします。

2. 以下のコマンドを実行して、コマンドライン・デベロッパツールをインストールします。

   ```sh
   $ xcode-select --install
   ```

3. 以下のコマンドを実行して、Homebrewをインストールします。

   ```sh
   $ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
   ```

4. 以下のコマンドを実行して、`Example usage: ...`が表示されればOKです。

   ```sh
   $ brew
   ```

> 参考 : [macOSにHomebrewをインストール](https://qiita.com/pypypyo14/items/4bf3b8bd511b6e93c9f9)



### VSCode

たぶん現時点では最も優れたコードエディタであるVSCodeをインストールします。

* [VSCode公式ホームページ](https://code.visualstudio.com/)のダウンロードボタンからVSCodeをダウンロードします。

<img width="300" alt="ダウンロードボタン" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227791/2ceda5b5-0ce1-2228-ae22-1695d993ccda.png">


* ダウンロードしたzipファイルを解凍してインストーラを実行し、インストールを完了してください。



###Node & Yarn

**Node**は、サーバーサイドで使えるJavaScript環境です。クライアントサイドの開発環境(Webpack等)にも必須なのでよく誤解がありますが、一応サーバーで動くものです。

**Yarn**は、Nodeのパッケージマネージャです。同じNodeのパッケージマネージャに有名な**npm**がありますが、Yarnはnpmよりインストールの速度が速く、npmの上位互換と考えていいと思います。

Nodeは**Nodebrew**でバージョン管理すると便利なので、ここではNodebrewを使用してNodeをインストールします。

![homebrew.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227791/7eef6d85-3e7f-c660-a230-9b119f804b07.jpeg)


* 以下のコードを実行してください。

> ※ `$`（コマンドプロンプト）が書かれているコードは、Macのターミナルで実行してください。（iTerm等でも可）

```sh
# nodeのバージョン管理のためのnodebrewのインストール
$ brew install nodebrew
# nodebrewのディレクトリのセットアップ
$ nodebrew setup
# パスを通しておく
$ echo "export PATH=$HOME/.nodebrew/current/bin:$PATH" >> ~/.bash_profile
$ source ~/.bash_profile
# nodeのダウンロードとセット
$ nodebrew install stable
$ nodebrew use stable
# yarnのインストール
$ brew install yarn --ignore-dependencies
```

Yarnは「HomebrewのNode」に依存するので、Yarnをインストール時に「HomebrewのNode」も一緒にインストールしようとしますが、今回は「NodebrewのNode」使用しているので、`--ignore-dependencies`オプションを使って「HomebrewのNode」をダウンロードさせないようにしています。

> 参考 : [install nodebrew, node and yarn](https://qiita.com/morikiyo/items/cd219a0b7aaed5b23d51)



### 作業ディレクトリの作成

* 適当なディレクトリを作成して移動します。

```sh
$ mkdir ssr-sample && cd $_
```

> `$_`は直前に実行したコマンドの最後の引数（上のコマンドだと`ssr-sample`）を表します。

* Yarnを初期化（`package.json`の作成）しておきます。

```sh
$ yarn init -y
```

* 作成したディレクトリをVSCodeで開きましょう。『ファイル』 >『 開く』から開けます。

<img width="300" alt="ファイルから開く" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227791/f7953cbc-2011-8107-1fe2-5fc1f247a971.png">




### Git

プログラムのバージョンを管理するために、Gitも入れておきましょう。Gitはプログラムの更新履歴を管理したり、複数人で一つのプロダクトを更新するのに役立ちます。

* GitをHomebrewからインストールします。

* ```sh
  $ brew install git
  ```

* Gitを初期化します。

  ```sh
  $ git init
  ```

* `.gitignore`を作成します。

  ```sh
  $ touch .gitignore
  ```

* `.gitignore`に以下を書き込みます。

  ```git
  /node_modules/
  ```

  `.gitignore`に書いたファイルはGitで管理されません。`node_modules`ディレクトリ内のファイルはYarnでインストールしたパッケージなので容量が多い上に、`package.json`があれば`$ yarn install`コマンドで復元できるのでGit管理はしないように設定しましょう。

  この記事ではローカルリポジトリへのコミットしかしないので、詳しく知りたい人はGitをまとめた別記事を参照してください。



### ESLint

ESListは、コードがちゃんとしたフォーマットになっているかチェックして、修正もしてくれるツールです。JavaScriptはセミコロン『 ; 』をつけるかどうかなど、コーディングの自由度が高いので、チームで開発する場合など特に活躍します。

設定は好みがあるのでいろいろな種類がありますが、Airbnbという企業の作った一番人気のコーディング規則を使ってみましょう。（細かくカスタマイズもできます）

① **ESLintと関連パッケージのインストール**

  ```sh
  # airbnbのeslint設定に必要なパッケージを一括でインストールします。
  # 『It seems as if you are using Yarn. Would you like to use Yarn for the installation? (y/n)』と聞かれたら、『y』と入力してEnterを押してください。
  $ npx install-peerdeps --dev eslint-config-airbnb
  ```
  > npm（Yarnと同じパッケージマネージャで、Nodeと一緒にインストールされる）の`npx`コマンドを使って`install-peerdeps`コマンドを実行し、`eslint-config-airbnb`が必要としているパッケージ（`eslint`など）を一括でインストールしています。
  >
  > 参考: [Github: Airbnb/JavaScript](https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb/README.md)

② **ESLintの設定ファイルを作成**

  `.eslintrc`を作成して、

  ```sh
  $ touch .eslintrc
  ```
  
  以下を書き込んでください。

  ```json
  {
    "extends": [
        "airbnb",
        "airbnb/hooks"
    ],
    "env": {
      "browser": true
    },
  }
  ```
  >  `extends`は指定した設定を継承します。`airbnb`とReact Hooksを使用するための`airbnb/hooks`を継承しています。
  
  >  `env`で`browser`を指定することで、`window.document`などブラウザでのDOM操作をするためのコードでエラーが出ないようにしています。
  
③ **VSCodeのESLint拡張のインストール**

   ESLintをYarnで入れただけでは、VSCode上で構文チェックできません。

   左側にある <img width="20" alt="拡張機能をインストールするボタン" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227791/95829b27-7f78-a832-325c-318ef7cda9d7.png"> のマークをクリックして、『eslint』を検索し、**ESLint**をインストールしてください。

④ **VSCode-ESLintの設定**

   VSCode上でのESLintを設定します。

   * 以下のコマンドを実行して`.vscode/settings.json`を作成します。

   ```sh
   $ mkdir .vscode && touch $_/settings.json
   ```

   * 以下の内容を書き込みます。（**コピペ推奨**）

   ```json
    {
     "eslint.run": "onSave",
     "eslint.autoFixOnSave": true,
     "eslint.validate": [
       "javascript",
       "javascriptreact",
       {
         "language": "typescript",
         "autoFix": true
       },
       {
         "language": "typescriptreact",
         "autoFix": true
       }
     ]
   }
   ```

   > 設定の詳細
   >
   > * **eslint.run** : `onSave`を指定すると、保存時に構文チェックを行います。
   > * **eslint.autoFixOnSave** : `true`にすると、保存時に構文の修正を行います。
   > * **eslint.validate** : ESLintを有効にするファイルの種類を指定します。それぞれ、`.js`(JavaScript)、`.jsx`(JavaScript + React)、`.ts`(TypeScript)、`.tsx`(TypeScript + React)を指定しています。`.ts`と`.tsx`については、`autoFix`を指定しないと修正されません。
   >
   > 参考 : [microsoft/vscode-eslint](https://github.com/microsoft/vscode-eslint)
   
   

ESLintの設定が反映されずエラーが出たままのときは、VSCodeを開きなおすと直ります。



### ファイル構成

```
.
├── .git               ← Gitが作った。Git管理のための隠しファイル。
├── node_modules       ← Yarnが作った。Yarnでインストールしたパッケージが入ってる
├── .eslintrc　　　　　　← 新しく作った。eslintの設定ファイル。
├── .gitignore         ← 新しく作った。Gitで管理しないファイルを指定する。
├── .vscode            ← 新しく作った。vscodeの設定ファイルを入れる。
│   └── settings.json  ← 新しく作った。vscode-eslintの設定をした。
├── package-lock.json  ← Yarnが作った。パッケージの依存関係が書いてある。編集とかはしない。
├── package.json       ← Yarnが作った。インストールしたパッケージとかが書いてある。
└── yarn.lock          ← Yarnが作った。Yarnの依存関係を管理する。
```





## STEP1 - ExpressでWebサーバを立てる

Webアプリを配信するためにはWebサーバが必要です。Webサーバとは、HTMLやJavaScript、画像などをクライアント（ブラウザ）に渡すものです。SSRでは、**NodeでJavaScriptをHTMLに変換**する必要があるので、Nodeのフレームワークである**Express**を使いましょう。



### 1. 必要なパッケージのインストール

```sh
$ yarn add express esm
```
インストールしたパッケージの概要は以下の通りです。
* **Express.js** : JavaScriptのNode.jsのWebアプリケーションフレームワークです。
* **esm** : `node -r esm index.js`の様に指定すると、Nodeで`import`/`export`の構文を利用できるようになります。



###2. index.jsの作成

* `index.js`を作成します。

```sh
$ touch index.js
```



* `index.js`に以下を書き込みます。

```js
import express from 'express';
import ssr from './src/ssr';

const app = express();

// 3000番ポートでWebサーバを立てる
app.listen(3000);

// https://localhost:3000 にアクセスがあったら ssr() を返す
app.get('/', (_, res) => {
  res.send(ssr());
});
```

`index.js`はExpressのルーティング（どのURLが来たら、どのファイルを返すか）などを設定するファイルです。今のコードでは、http://localhost:3000/でアクセスしたら、`./src/ssr`の`ssr()`を返す設定をしました。



###3. ssr.jsの作成

* `src/ssr.js`を作成します。

```sh
$ mkdir src && touch $_/ssr.js
```

* `src/ssr.js`に以下を書き込みます。

```js
const ssr = () => (`
  <html>
    <head>
      <title>CountUp</title>
      <meta charset="utf-8"/>
    </head>
    <body>
      <div id="app">
        <h1>0</h1>
        <button type="button">+</button>
        <p>${new Date().toTimeString()}</p>
      </div>
    </body>
  </html>
`);

export default ssr;
```

`src/ssr.js`は、3.で設定した通り、実際に返される**文字列**になります。

`new Date().toTimeString()`のところは、現在の時刻です。



### 4. package.jsonの編集

Expressを起動してWebサーバを立てるために、Nodeを`index.js`に対して実行するコマンドを設定します。

* `package.json`に以下を追加します。

```json
"scripts": {
  "start": "node -r esm index.js"
},
```

追加した後の`package.json`はこのような感じになります。

```json
{
  "name": "ssr-sample",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "node -r esm index.js"
  },
  "devDependencies": {
    "eslint": "^6.1.0",
    "eslint-config-airbnb": "^18.0.1",
    "eslint-plugin-import": "^2.18.2",
    "eslint-plugin-jsx-a11y": "^6.2.3",
    "eslint-plugin-react": "^7.16.0",
    "eslint-plugin-react-hooks": "^1.7.0"
  },
  "dependencies": {
    "esm": "^3.2.25",
    "express": "^4.17.1"
  }
}
```

`package.json`の`script:{}`に追加したコマンドは、`yarn start`のように実行することができます。



###5. Expressの起動

* Expressを起動しましょう！

```sh
$ yarn start
```

http://localhost:3000/にアクセスして、下の画像のように表示されれば完璧です！これがSSRの基礎になります。

ページをリロードすると時間表示が更新されます。**ページのリクエストごとにJavaScriptからHTMLを生成している**ことが確認できます。

<font color="MediumVioletRed">まだハリボテのHTMLを生成しているだけなので、`+` ボタンを押しても何もおきません。</font>

<img width="400" alt="STEP1の画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227791/9fd65739-3cb5-10eb-a41f-772af20529c6.png">



### 6. Gitにコミットする

* 忘れないようにgitにコミットしておきます。

```
$ git add .
$ git commit -m "STEP1 - ExpressでWebサーバを立てる"
```



###ファイル構成

```
.
├── .git
├── node_modules
├── .eslintrc
├── .gitignore
├── .vscode
│   └── settings.json
├── index.js           ← 新しく作った。Expressの設定。
├── package-lock.json
├── package.json       ← 編集した。startコマンドを追加。
├── src                ← 新しく作った。ソースファイルを入れる。
│   └── ssr.js         ← 新しく作った。ブラウザに送る文字列を返す。
└── yarn.lock
```



## STEP2 - Reactを使う

**STEP1**では実装しなかったカウントアップの仕組みをReactを使って実装してみましょう。

ReactはそのままではNodeやブラウザで実行できないので、Nodeやブラウザで実行できるJavaScriptに変換するために、**Babel**を使用する必要があります。

### 1. 必要なパッケージのインストール

```sh
$ yarn add react react-dom
$ yarn add -D @babel/cli @babel/core @babel/preset-env core-js@3 @babel/preset-react
```

インストールしたパッケージの概要は以下の通りです。

* **React** : JavaScriptのUIフレームワーク
  * **react-dom** : `render()`などを使うために必要です。

* **Babel** : ES6の構文などをブラウザで表示できるように変換します。
  * **@babel/core** : Babelのコア。
  * **@babel/cli** : Babelをコマンドラインから使用できるようにします。
  * **@babel/preset-env** : 最新のJavaScriptを変換するための設定をまとめたプリセット。
    * **core-js** : babel/preset-envが**Polyfill**をするために必要です。
  * **@babel/preset-react** : Reactを変換するためのプリセット。



> **Polyfill** : 最近の機能をサポートしていない古いブラウザーで、その機能を使えるようにするためにコードを変換すること。



>  **`-D`オプションをつけるかどうかの違い**
>
>  `yarn add`に`-D`オプションをつけると、`package.json`の`devDependencies`に追加されます。`dependencies`に追加しても`devDependencies`に追加してもパッケージとして公開しなければ違いはありませんが、ExpressやReactは、`dependencies`の追加しないとESLintに怒られます。
>
>  参考 : [【いまさらですが】package.jsonのdependenciesとdevDependencies](https://qiita.com/chihiro/items/ca1529f9b3d016af53ec)



### 2. .babelrcの作成

* Babelの設定ファイル`.babelrc`を作成します。

```sh
$ touch .babelrc
```
* 以下の内容を書き込みます。

```json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    [
      "@babel/preset-react",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}
```

> 参考 : [Babel7.4で非推奨になったbabel/polyfillの代替手段と設定方法](https://aloerina01.github.io/blog/2019-06-21-1)



### 3. index.jsの編集

* `index.js`の2行目の`./src/ssr`を`./views/ssr`に変更して、以下のように書き換えます。

```javascript
import express from 'express';
import ssr from './views/ssr';

const app = express();

app.listen(3000);

app.get('/', (_, res) => {
  const response = ssr();
  res.send(response);
});
```

import先のパスを変更したのは、`./ssr`ディレクトリ内のReactで書かれたファイルをBabelでコンパイルして、Nodeで実行できるファイルとして`./views`に出力するからです。



###4. ssr.jsxの編集

* `src/ssr.js`の拡張子を`.jsx`に変更しします。

```sh
$ mv src/ssr.js src/ssr.jsx
```
> **.jsx** : ReactでJSX(HTMLライクな記法)を用いる場合は、拡張子を`.jsx`にします。



* `renderToString`を使用して、以下のように書き換えます。

```react
import React from 'react';
import { renderToString } from 'react-dom/server';
import CountUp from './CountUp';

// React ElementをHTMLに変換
const ssr = () => (`
  <html>
    <head>
      <title>CountUp</title>
      <meta charset="utf-8"/>
    </head>
    <body>
      <div id="app">
        ${renderToString(<CountUp />)}
      </div>
    </body>
  </html>
`);

export default ssr;
```

`renderToString()`を使用すると、Reactのコンポーネントを文字列に変換することができます。上のコードでは、`<CountUp />`コンポーネントを文字列に変換して、`ssr()`で返す文字列の中に埋め込んでいます。



###5. CountUp.tsxの作成

* `src/CountUp.jsx`を作成します。

```sh
$ touch src/CountUp.jsx
```
* 以下の内容を書き込みます。

```react
import React, { useState } from 'react';

const CountUp = () => {
  const [count, setCount] = useState(0);

  return (
    <>
      <h1>{count}</h1>
      <button type="button" onClick={() => setCount(count + 1)}>+</button>
      <p>{new Date().toTimeString()}</p>
    </>
  );
};
export default CountUp;
```

**useState**は、React Hooksの機能の一つです。上の例だと、`count`をState(状態)、`setCount()`をStateを更新する関数として宣言しています。stateの初期値は`useState()`の引数で決まるので0です。Stateは絶対に直接変更せず、`count`を1増やす場合は`setCount(count + 1)`のようにします。

`<></>`で囲んでいるのは、Reactコンポーネントの返り値は何かのタグで囲っていないとエラーが出るためです。



### 6. package.jsonの編集

* Babelを実行するためのコマンド(`babel`)を`package.json`に書き込みます。

```json
"scripts": {
  "start": "node -r esm index.js",
  "babel": "babel src -x '.js,.jsx' -d views"
},
```

`-x`オプションで対象とするファイルの拡張子を指定し、`-d`オプションで書き出し先のディレクトリを指定しています。

上のコマンドだと、『`src`ディレクトリに含まれる`.js`か`.jsx`拡張子のファイルをコンパイルして`views`ディレクトリに書き出す』という意味になります。



### 7. Babelの実行

* Babelを実行します。

```sh
$ yarn babel
```

すると、Babelによってsrcディレクトリの`ssr.jsx`と`CountUp.jsx`がviewsディレクトリに`.js`拡張子で書き出されるはずです。

> **babelが正しく実行されない場合**
コメントにて`yarn babel`が正常に実行されないとのご指摘がありました。
実行環境による違いと思われますが、その場合は`package.json`を以下のようにすると正常に実行されるようです。
1.「-x .js,.jsx」（シングルクォートで囲わない）もしくは
2.「-x ',.js,.jsx,'」（シングルクォートで囲い、カンマでも囲う）



### 8. Expressの起動

* Expressを起動します。

```sh
$ yarn start
```

* http://localhost:3000/を開きます。

表示されるUIは**STEP1**と同じです。

### 9. Gitにコミットする

忘れないようにgitにコミットしておきます。

```
$ git add .
$ git commit -m "STEP2 - Reactを使う"
```



###ファイル構成

```
.
├── .git
├── node_modules
├── .babelrc           ← 新しく作った。Babelの設定ファイル。
├── .eslintrc
├── .gitignore
├── .vscode
│   └── settings.json
├── index.js           ← 編集した。ssr.jsのパスを変更。
├── package-lock.json
├── package.json       ← 編集した。babelコマンドを追加。
├── src
│   ├── CountUp.jsx    ← 編集した。ReactでState管理してカウントする。
│   └── ssr.jsx        ← 編集した。CountUpを文字列に変換する。
├── views              ← Babelによって生成される。
│   ├── CountUp.js 　  ← Babelによって生成される。
│   └── ssr.js　　　　　 ← Babelによって生成される。
└── yarn.lock
```



ポイントは`ssr.jsx`内で、**`renderToString()`関数を使ってJSXをHTMLとして解釈できる文字列に変換している**ところです。

しかし、**<font color="Coral">`+`を押してもまだ何も起こりません。</font>**それもそのはず、サーバーでJavaScriptをHTMLに変換してクライアントに返しているだけで、**クライアントであるブラウザではJavaScriptが動いていない**からです。クライアントに返されるHTMLは、**STEP1**と同じです。

つまり、この記事の冒頭に載せたSSRの図は正確ではなく、修正すると以下のようになります。

![SSRの図2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227791/4d154724-c27f-9496-c11c-41abbde596a2.jpeg)


## STEP3 - Webpackを使う

Expressで配信する静的なHTMLとは別に、クライアントで動作させるためのJavaScriptファイルも生成し、クライアントに読み込ませる必要があります。Webpackを使えば、Babelでコンパイルするのと同時に、**複数のJavaScriptファイルを1つにバンドルする**ことができます。



### 1. 必要なパッケージのインストール

```sh
$ yarn add -D webpack webpack-cli babel-loader
```

インストールしたパッケージの概要は以下の通りです。

* **Webpack** : 複数のJavaScriptなどのファイルを1つのファイルにバンドルします。
  * **webpack-cli** : Webpackをコマンドラインから使用できるようにします。
  * **babel-loader** : WebpackでBabelを使用してJavaScriptをコンパイルできるようにします。



### 2. エントリーポイントの作成

* `src/client.jsx`を作成します。

  ```sh
  $ touch src/client.jsx
  ```

* 以下の内容を書き込みます。

  ```react
  import React from 'react';
  import { hydrate } from 'react-dom';
  import CountUp from './CountUp';
  
  // idがappの部分をhydrateで描画する
  hydrate(<CountUp />, document.querySelector('#app'));
  ```

  SSRでないReactアプリでは`render()`を使用してUIを描画しますが、SSRを使用してサーバーで描画されている場合は`hydrate()`を使用します。サーバーで描画した部分を、ブラウザで再描画しないようにするためです。



### 3. webpack.config.jsの作成

Webpackの設定ファイル`webpack.config.js`を作成します。

```sh
$ touch webpack.config.js
```

以下の内容を書き込みます。

```js
const path = require('path');

module.exports = {
  resolve: {
    // 対象にする拡張子の指定
    extensions: ['.js', '.jsx'],
  },
  entry: {
    // エントリーポイントの指定
    client: './src/client.jsx',
  },
  output: {
    // アウトプット先のディレクトリを指定(assets)
    path: path.resolve(__dirname, 'assets'),
    // アウトプットするファイルの名前を指定(名前は変更しない)
    filename: '[name].js',
  },
  module: {
    rules: [
      {
        // 拡張子が.jsか.jsxだった場合に適用するルール
        test: /\.js(x?)$/,
        // node_modulesディレクトリ(Yarnでインストールしたパッケージが入ってる)は除外
        exclude: /node_modules/,
        use: [
          {
            // babelの設定
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  {
                    useBuiltIns: 'usage',
                    corejs: 3,
                  },
                ],
                [
                  '@babel/preset-react',
                  {
                    useBuiltIns: 'usage',
                    corejs: 3,
                  },
                ],
              ],
            },
          },
        ],
      },
    ],
  },
};
```



### 4. Webpackを実行するコマンドの追加

* Webpackを実行するためのコマンド(`build`)を`package.json`に書き込みます。

```json
"scripts": {
  "start": "node -r esm index.js",
  "babel": "babel src -x '.js,.jsx' -d views",
  "build": "webpack --mode development",
},
```

> `--mode`オプションで`production`か`development`を指定しないと、ビルド時に警告が出ます。



### 5. Webpackの実行

* Webpackを実行します。

  ```sh
  $ yarn build
  ```

  すると、Babelによってsrcディレクトリのファイルが、assetsディレクトリに`client.js`という１ファイルにまとめて書き出されます。`client.js`と関係があるファイルだけがまとめられるので、`ssr.js`はバンドルに含まれません。



### 6. client.jsのルーティング

Webpackでバンドルした`assets/client.js`をクライアントで読み込む必要があるので、Expressで`assets`ディレクトリ内のファイルを返すように設定します。

* `index.js`に`app.use(express.static('assets'));`を追加して、以下のように編集します。

```js
import express from 'express';
import ssr from './views/ssr';

const app = express();

app.listen(3000);

app.use(express.static('assets'));

app.get('/', (_, res) => {
  const response = ssr();
  res.send(response);
});
```

これで、[http://localhost/client.js](http://localhost/client.js)にアクセスすることで、`assets/client.js`を取得することができるようになりました。



### 7. ssr.jsxの編集

* `src/ssr.jsx`に`<script src="./client.js"></script>`を追加します。

```react
import React from 'react';
import { renderToString } from 'react-dom/server';
import CountUp from './CountUp';

const ssr = (): string => (`
  <html>
    <head>
      <title>CountUp</title>
      <meta charset="utf-8"/>
    </head>
    <body>
      <div id="app">
        ${renderToString(<CountUp />)}
      </div>
      <script src="./client.js"></script>
    </body>
  </html>
`);

export default ssr;
```

これで、ブラウザでWebpackでバンドルした`client.js`が読み込まれることになります。



### 8. Babelの実行

*"6."* で`ssr.js`のを編集したので、Babelを実行して、サーバーで生成されるHTMLに`<script src="./client.js"></script>`が追加されるようにします。

* Babelを実行します。

```sh
$ yarn babel
```





### 9. Expressの起動

* Expressを起動します。

```sh
$ yarn start
```

* [http://localhost:3000/](http://localhost:3000/)を開きます。

表示は同じですが、`+`を押すことでカウントが増え、時刻表示も更新されるのが分かります。


![countup.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227791/737f87de-0f92-cddd-3a21-c8415d4673c7.gif)


### 10. Gitにコミットする

忘れないようにgitにコミットしておきます。

```
$ git add .
$ git commit -m "STEP3 - Webpackを使う"
```



### ファイル構成

```
.
├── .git
├── node_modules
├── .babelrc
├── .eslintrc
├── .gitignore
├── .vscode
│   └── settings.json
├── assets               ← Webpackによって生成される。
│   └── client.js        ← Webpackによって生成される。srcをバンドルしたもの。
├── index.js             ← 編集した。client.jsを配信するように設定。
├── package-lock.json
├── package.json         ← 編集した。buildコマンドを追加。
├── src
│   ├── CountUp.jsx
│   ├── client.jsx       ← 新しく作った。ブラウザで読み込む用のJavaScript。
│   └── ssr.jsx          ← 編集した。client.jsをブラウザで読み込む。
├── views
│   ├── CountUp.js
│   ├── client.js        ← Babelによって生成される。が、サーバーでは必要ないので使われない。
│   └── ssr.js
├── webpack.config.js    ← 新しく作った。Webpackの設定ファイル。
└── yarn.lock
```







## STEP4 - TypeScriptを使う

JavaScriptは動的型付けを行います。例えば、数値と文字を足し算するとこうなります。

```js
const a = 1;
const b = '2';
const sum = a + b; // 12
```

これくらい簡単なプログラムだと素晴らしいことなんですが、プロジェクトが大きくなるとバグが起きやすくなります。一方、TypeScriptを使用して静的型付けを行うと、実行前にエラーを出してくれます。

```typescript
const a = 1;
const b = '2';
const sum: number = a + b; // Error
```

sumの型を`number`(数値)として指定しているので、*数値*と*文字列*の足し算に対してエラーを出してくれています。



### 1. 必要なパッケージのインストール

```sh
$ yarn add -D typescript @babel/preset-typescript
```

インストールしたパッケージの概要は以下の通りです。

* **Typescript** : JavaScriptで静的型付けをできるようにした言語です。
  * **@babel/preset-typescript** : TypeScriptをBabelでコンパイルできるようにするプリセットです。





### 2. ESLintの設定

* TypeScriptを扱うためのパーサをインストールします。

```sh
$ yarn add -D @typescript-eslint/parser
```

* `.eslintrc`を以下のように編集します。

```json
{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "airbnb",
    "airbnb/hooks"
  ],
  "plugins": [
    "@typescript-eslint"
  ],
  "settings": {
    "import/extensions": [
      ".js",
      ".jsx",
      ".ts",
      ".tsx"
    ],
    "import/resolver": {
      "node": {
        "extensions": [
          ".js",
          ".jsx",
          ".ts",
          ".tsx"
        ]
      }
    }
  },
  "rules": {
    "react/jsx-filename-extension": [
      "error",
      {
        "extensions": [
          ".js",
          ".jsx",
          ".ts",
          ".tsx"
        ]
      }
    ],
    "import/extensions": [
      "error", "always",
      {
        "js": "never",
        "jsx": "never",
        "ts": "never",
        "tsx": "never"
      }
    ]
  }
}
```

TypeScriptをパース(構文解析)するために、`parser`を指定しています。継承している`airbnb`には`@typescript-eslint`が含まれていないので、TypeScriptのプラグインも追加しました。

`.tsx`ファイルでもJSXを扱えるようにするために`react/jsx-filename-extension`ルールを指定しています。

 > 参考 : [@typescript-eslintでtypescriptのlintをeslintで行いつつ、airbnbの設定でいきましょう的なお話](http://watanabeyu.blogspot.com/2019/02/typescript-eslinttypescriptlinteslintai.html)

ESLint関係の設定は難しいので、動いてるからよし！ぐらいの心持ちで行きましょう。



### 3. 定義ファイルのインストール

Reactの型定義ファイルもインストールしておきます。

```sh
$ yarn add -D @types/react @types/react-dom
```

> **型定義ファイル** : TypeScriptで型を扱うために、Reactなどの外部モジュールの型の定義が必要です。型定義ファイルは自作することもできます。



### 4. .babelrcの編集

* `.babelrc`に`@babel/preset-typescript`を追加して、以下のように編集します。

```json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    [
      "@babel/preset-react",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    "@babel/preset-typescript"
  ]
}
```



### 5. webpack.config.jsの編集

* `webpack.config.js`を`.ts`、`tsx`の拡張子を処理するように書き換えます。

* また、Babelでコンパイルできるように、4.で行なった`.babelrc`と同じく`@babel/preset-typescript`を追加します。

```js
const path = require('path');

module.exports = {
  resolve: {
    // 対象にする拡張子の指定（パッケージも含まれるので、.jsは必須）
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },
  entry: {
    // エントリーポイントの指定
    client: './src/client.tsx',
  },
  output: {
    // 書き出し先のディレクトリを指定(assets)
    path: path.resolve(__dirname, 'assets'),
    // 書き出すファイルの名前を指定(名前は変更しない)
    filename: '[name].js',
  },
  module: {
    rules: [
      {
        // 拡張子が.jsか.jsxだった場合に適用するルール
        test: /\.ts(x?)$/,
        // node_modulesディレクトリは除外
        exclude: /node_modules/,
        use: [
          {
            // babelの設定
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  {
                    useBuiltIns: 'usage',
                    corejs: 3,
                  },
                ],
                [
                  '@babel/preset-react',
                  {
                    useBuiltIns: 'usage',
                    corejs: 3,
                  },
                ],
                '@babel/preset-typescript',
              ],
            },
          },
        ],
      },
    ],
  },
};
```



###6. ssr.jsxの編集

* `src/ssr.jsx`の拡張子を`.tsx`に変更します。

```sh
$ mv src/ssr.jsx src/ssr.tsx
```
* src/ssr.tsx`を以下の様に書き換えます。

`: string`という返り値の型を追加しました。

```react
import React from 'react';
import { renderToString } from 'react-dom/server';
import CountUp from './CountUp';

// 返り値の型を指定しました。
const ssr = (): string => (`
  <html>
    <head>
      <title>CountUp</title>
      <meta charset="utf-8"/>
    </head>
    <body>
      <div id="app">
      	${renderToString(<CountUp />)}
      </div>
      <script src="./client.js"></script>
    </body>
  </html>
`);

export default ssr;
```



###7. CountUp.jsxの編集

* `src/CountUp.jsx`の拡張子を`.tsx`に変更します。

```sh
$ mv src/CountUp.jsx src/CountUp.tsx
```
* `src/CountUp.tsx`に以下の内容を書き込みます。`: JSX.Element`という返り値の型を追加しました。

```react
import React, { useState } from 'react';

const CountUp = (): JSX.Element => { // 返り値の型を指定しました。
  const [count, setCount] = useState(0);

  return (
    <>
      <h1>{count}</h1>
      <button type="button" onClick={() => setCount(count + 1)}>+</button>
      <p>{new Date().toTimeString()}</p>
    </>
  );
};

export default CountUp;
```

### 8. client.jsxの編集

* `src/client.jsx`の拡張子を`.tsx`に変更します。

```sh
mv src/client.jsx src/client.tsx
```



### 9. package.jsonの編集

* Babelの対象とする拡張子を、`.js,.jsx`から`.ts,.tsx`に編集します。

```json
"scripts": {
  "start": "node -r esm index.js",
  "build": "babel src -x '.ts,.tsx' -d views"
},
```



### 10. Babelの実行

* Babelを実行します。

```sh
$ yarn babel
```

Babelによって、srcディレクトリの`greeting.tsx`と`ssr.tsx`がviewsディレクトリに`.js`拡張子で書き出されます。



### 11. Webpackの実行

* Webpackを実行します。

```sh
$ yarn build
```

Webpackによって、srcディレクトリのファイルが`client.tsx`をエントリーポイントとしてバンドルされ、viewsディレクトリに`client.js`という１ファイルにまとめて書き出されます。



### 12. Expressの起動

`yarn start`を実行して[http://localhost:3000/](http://localhost:3000/)を開きます。

```sh
$ yarn start
```

見た目は変わっていませんが、TypeScriptを使用することで強固なプログラムになりました。



### 13. Gitにコミットする

忘れないようにgitにコミットしておきます。

```
$ git add .
$ git commit -m "STEP4 - TypeScriptを使う"
```



###ファイル構成

```
.
├── .git
├── node_modules
├── .babelrc            ← 編集した。TypeScriptをコンパイルする。
├── .eslintrc           ← 編集した。TypeScriptを構文チェックする。
├── .gitignore
├── .vscode
│   └── settings.json
├── assets
│   └── client.js
├── index.js
├── package-lock.json
├── package.json        ← 編集した。buildコマンドをTypeScriptに対応。
├── src
│   ├── CountUp.tsx     ← 編集した。拡張子を変更と型の追加。
│   ├── client.tsx      ← 編集した。拡張子を変更。
│   └── ssr.tsx         ← 編集した。拡張子を変更と型の追加。
├── views
│   ├── CountUp.js
│   ├── client.js
│   └── ssr.js
├── webpack.config.js   ← 編集した。TypeScriptをバンドルする。
└── yarn.lock
```



お疲れ様でした！:tada::tada::tada:

以上がモダンなTypeScript & ReactでのSSR/SPA開発です。

しっかりと開発環境を作ったので、複雑なプロジェクトにも応用できると思います。



## 参考

[React Server-Side Rendering Example](https://github.com/Rohitkrops/ssr) : めちゃくちゃ参考にしました。SSRありとなしをエンドポイントで分けているので、違いを体感できて面白いです。



## 最後に

分からないところ、ご指摘等あれば、お気軽にコメントや[Twitter](https://twitter.com/home)までご連絡ください。

本当はContextを使うところまでやりたかったですが、分かりづらくなりそうだったのでやめました。