Docker + React + Go API 通信できる環境を構築する
Docker概要
React と Go でフロントエンドとバックエンド間で通信する環境を構築します。
React の ビルドツールには Vite を使い、エディターには VSCode を使ってやっていきます。
動作環境は WSL2 の Ubuntu。以下の記事でセットアップした前提で進んでいきますが、Ubuntu ならだいたい大丈夫なはずです。
WSL2 + Docker + Docker Compose V2 セットアップ
今回は以下のふたつの Dockerfile を用意します。
- web コンテナ
- ベースイメージ: node:20.9-alpine
- app コンテナ
- ベースイメージ: golang:1.21-alpine
2023年11月時点の最新の安定リリース版です。
僕は alpine 信者なので alpine を使います。
対象読者
- Docker のことがなんとなくわかってる
- Docker の環境がある
- Golang の知識が少しある
mkdir
,ls
,cd
などの基本的な Linux コマンドを理解できる
到達目標
- Golang のサーバーから RTK Query で取得したデータをレンダリング
Go で API サーバー立てる
まずは今回の作業のためのディレクトリを作成します。
mkdir react-go-api
cd react-go-api
Docker コンテナの準備
docker/go/Dockerfile
というファイルを作り、以下の記述をします。
FROM golang:1.21-alpine
# ディレクトリ指定
ENV ROOT=/go/src/app
WORKDIR ${ROOT}
次に docker-compose.yaml
を作成します。
version: "3.9"
services:
app:
build:
context: .
dockerfile: ./docker/go/Dockerfile
volumes:
- ./backend/:/go/src/app
tty: true
この状態で、一旦コンテナを起動します。
docker compose up -d
すると以下のようなメッセージが最後に表示されます。
[+] Running 2/2
✔ Network react-go-api_default Created
✔ Container react-go-api-app-1 Started
この状態で、以下のコマンドを叩いてコンテナに入ります。
ちなみに alpine は ash、普通のやつは bash という違いがあります。
docker compose exec app ash
コンテナの中に入れたら、以下のコマンドで GOPATH モードから module モードにします。
go mod init react-go-api
すると backend/go.mod
というファイルが作成されました。
一応動作確認ができるように、backend/main.go
を作成し……と言いたいところですが、コンテナを root で起動して go mod init を叩いたため、backend
ディレクトリの所有権が root になってしまっていまい、VSCode などのエディターからは作成できなくなってしまいました。
一旦 Ctrl + D
でコンテナから出ます。
以下のコマンドを叩くと、ディレクトリやファイルの所有者などが把握できます。
ls -l
sudo 権限で backend の所有者を再帰的に自分に戻します。
sudo chown $USER:$USER -R backend
もう一度確認してみると、所有者が自分になっていることがわかります。
ls -l
改めて、動作確認ができるように、backend/main.go
を作成し、以下の記述をしておきます。
package main
import "fmt"
func main() {
fmt.Println("Hello Golang!")
}
もう一度コンテナに入り、動作を確認してみます。
docker compose exec app ash
go run main.go
Hello Golang! とターミナルに表示されたら成功です。
現在は以下のようなディレクトリ構成になっています。
.
├── backend
│ ├── go.mod
│ └── main.go
├── docker
│ └── go
│ └── Dockerfile
└── docker-compose.yaml
API サーバーから JSON レスポンスをできるようにする
次は Golang で API サーバーを立てて、 JSON をレスポンスできるようにします。
ルーターには gorilla/mux
を使うことにします。
app コンテナに入った状態で、以下のコマンドを叩きます。
go get github.com/gorilla/mux
すると go.mod が更新、 go.sum が生成されました。
これで mux が使えるようになります。
backend/controllers/webserver.go
を作成し、以下を記載します。
package controllers
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
)
// 頭文字を大文字にするとパッケージ外部から呼び出しできる
func StartWebServer() error {
fmt.Println("Start Web Server!")
r := mux.NewRouter().StrictSlash(true)
// URL に呼び出したい関数を登録する
r.HandleFunc("/api/todos", getTodos).Methods("GET")
// ポートを指定してサーバーを起動する
return http.ListenAndServe(":8080", r)
}
type Todo struct {
Id int
Title string
Completed bool
}
func getTodos(w http.ResponseWriter, r *http.Request) {
// フロントエンドとバックエンドのポートが違うので許可しておく
// (すべてを許可する設定にしているので、本番ではより制限を厳しくしておくように)
w.Header().Set("Access-Control-Allow-Origin", "*")
// 返却したい値を構造体で定義
todo1 := Todo{
Id: 1,
Title: "チャーハン作るよ!",
Completed: true,
}
todo2 := Todo{
Id: 2,
Title: "豚肉も入れるよ!",
Completed: false,
}
todos := []Todo{todo1, todo2}
// JSON にして返却
responseBody, err := json.Marshal(todos)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(responseBody)
}
main.go に以下を以下に変更します。
package main
import (
"log"
"react-go-api/controllers"
)
func main() {
// controllers パッケージから StartWebServer を呼び出す
err := controllers.StartWebServer()
if err != nil {
log.Fatal(err)
}
}
ディレクトリ構成は以下のようになりました。
.
├── backend
│ ├── controllers
│ │ └── webserver.go
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── docker
│ └── go
│ └── Dockerfile
└── docker-compose.yaml
これでサーバーは起動できるようになったのですが、このままだとコンテナの外からアクセスできないので、 docker-compose.yaml
も変更して、8080 を外部に公開します。
version: "3.9"
services:
app:
build:
context: .
dockerfile: ./docker/go/Dockerfile
volumes:
- ./backend/:/go/src/app
tty: true
ports:
- 8080:8080
Dockerfile
や docker-compose.yaml
など Docker に関わる変更を行なったら、再度ビルドする必要があるので、 一旦 Ctrl + D
でコンテナの外に出たあと、以下のコマンドを叩きます。
docker compose build
そしてコンテナを起動し、app コンテナに入ります。
docker compose up -d
docker compose exec app ash
コンテナの中で main.go を実行します。
go run main.go
サーバーが起動された状態で http://localhost:8080/api/todos にアクセスすると、以下のような JSON が返却されていることが確認できます。
[{"Id":1,"Title":"チャーハン作るよ!","Completed":true},{"Id":2,"Title":"豚肉も入れるよ!","Completed":false}]
とりあえず Golang で作った API からレスポンスを受け取ることはできるようになりました。
Ctrl + C
でサーバーを停止することができます。
その後、 Ctrl + D
でコンテナから出て、以下のコマンドでコンテナを終了しておきます。
docker compose down --remove-orphans
Node.js で API レスポンスを表示する画面を作る
次は React 側を整備していきます。
Docker コンテナの準備
docker/node/Dockerfile
というファイルを作成し、以下の記述をします。
FROM node:20.9-alpine
WORKDIR /home/node/app
docker-compose.yaml
を以下のように書き換えます。
version: "3.9"
services:
# app コンテナの記述...
web:
build:
context: .
dockerfile: ./docker/node/Dockerfile
volumes:
- ./frontend/:/home/node/app
tty: true
ports:
- 5173:5173
docker をビルドし、コンテナを起動します。
docker compose build
docker compose up -d
Vite React TypeScript 環境の構築
今回ビルドツールには Vite を使い、TypeScript で記述していくことにします。
以下のコマンドで web コンテナの中に入ります。
docker compose exec web ash
コンテナの中で以下のコマンドを叩きます。
npm create vite@latest .
都度質問が出てくるので、以下のように選択します。
Select a freamwork: React
Select a Variant: TypeScript + SWC
Done. と出たら、ターミナルにも表示されていると思いますが、以下のコマンドを実行します。
npm install
npm run dev
するとフロントエンドの開発サーバーが起動し、 http://localhost:5173 という URL が提示されます。
しかし、アクセスしても表示できません。
これは Vite が Docker コンテナ内へブラウザアクセスを許可していないためです。 (厳密に言うと、web コンテナから web コンテナの開発サーバーへアクセスすることはできるが、Windows から web コンテナにある開発サーバーにアクセスすることができない)
これを許可するために frontend/vite.config.ts
を少し書き換えなければならないのですが、やはりコンテナの root 権限で npm install
したため、frontend 以下のディレクトリとファイルの所有権が root になっており、 VSCode からファイルを操作することができなくなっています。
ということで backend のときと同じように、ディレクトリ全体の所有者を自分にします。
一旦 Ctrl + C
でサーバーを止めておき、 Ctrl + D
で web コンテナから出た状態で、以下のコマンドを叩きます。
sudo chown $USER:$USER -R frontend
ディレクトリの権限を確認してみると自分になっていることがわかります。
ls -l
ではさっそく、frontend/vite.config.ts
に少し変更を加えます。
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: true
}
})
server.host の項目を追加して、true に設定します。 これでもう一度 web コンテナに入り、開発サーバーを起動すれば、問題なく http://localhost:5173 をブラウザで開くことができます。
docker compose exec web ash
npm run dev
RTK Query を導入して Go からのデータを取得する
web コンテナの中で NPM で Redux と Redux Tool Kit を入れます。
npm install react-redux @reduxjs/toolkit
frontend/src/services/TodoService.ts
を作成し、以下を記述します。
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export type Todo = {
Id: number
Title: string
Completed: boolean
}
type Todos = Todo[]
export const todoApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8080/api/',
// header に設定したい内容
prepareHeaders: (headers, { getState }) => {
headers.set('accept', 'application/json')
return headers
},
// 今回はポートが違うので cors 対応
mode: 'cors'
}),
endpoints: (builder) => ({
getTodos: builder.query<Todos, void>({
query: () => `todos`,
}),
}),
})
// 自動的に hooks が生成される
export const { useGetTodosQuery } = todoApi
frontend/src/store.ts
を作成し、middleware に追加します。
import { configureStore } from '@reduxjs/toolkit'
import { todoApi } from './services/TodoService'
export const store = configureStore({
reducer: {
[todoApi.reducerPath]: todoApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(todoApi.middleware),
});
frontend/src/main.tsx
を開いて、Provider でラップして Store を設定します。
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App.tsx'
import './index.css'
import { store } from "./store.ts"
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
)
そして frontend/src/App.tsx
で実際に呼び出してみます。
import './App.css'
import { useGetTodosQuery } from './services/TodoService'
function App() {
const {data,error,isFetching } = useGetTodosQuery()
return (
<>
<div>
{error
?
<p>Error!</p>
: isFetching
? <p>Loading...</p>
: data ? <p>{data[0].Title} {data[1].Title}</p>
: <p>No Data.</p>
}
</div>
</>
)
}
export default App
ディレクトリ構成はこんな感じになりました。(node_modules は除外)
.
├── backend
│ ├── controllers
│ │ └── webserver.go
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── docker
│ ├── go
│ │ └── Dockerfile
│ └── node
│ └── Dockerfile
├── docker-compose.yaml
└── frontend
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
│ └── vite.svg
├── src
│ ├── App.css
│ ├── App.tsx
│ ├── assets
│ │ └── react.svg
│ ├── index.css
│ ├── main.tsx
│ ├── services
│ │ └── TodoService.ts
│ ├── store.ts
│ └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
ちゃんと取得されているか確認する
で、さっそく確認したいのですが、この状態だと Vite の開発サーバーと Golang の API サーバーを同時に起動するというのが少し難しいので、Dockerfile を書き換えます。
docker/go/Dockerfile
FROM golang:1.21-alpine
# ディレクトリ指定
ENV ROOT=/go/src/app
WORKDIR ${ROOT}
CMD ["go", "run", "main.go"]
docker/node/Dockerfile
FROM node:20.9-alpine
WORKDIR /home/node/app
CMD ["npm", "run", "dev"]
CMD コマンドを書くと、コンテナ起動時に自動的にこのコマンドを実行してくれます。
どちらもサーバー起動のコマンドなので、コンテナが立ち上がったときに自動で両方のサーバーが立ち上がります。
Docker に関わるファイルを書き換えたので、さっそくビルドして試してみます。
docker compose build
docker compose up -d
これで http://localhost:5173 にアクセスすると、backend/controllers/webserver.go
で定義した todo の内容が API サーバーから RTK Query を通して取得され、React でレンダリングされていることが確認できます。
確認が済んだら、以下のコマンドでコンテナを終了しておきます。
docker compose down --remove-orphans
広告