30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Next.js で始める gRPC 通信

サーバ・クライアント間の通信を gRPC で行う場合、インターフェイスを定義した共通のファイルから、サーバとクライアント双方のコードを生成することができる。
この記事では、インターフェイスの定義ファイルを作成するところから始めて、gRPC を利用した単純なウェブアプリを作っていく。
gRPC についての概念的な説明などは扱わず、実際に手元で動くウェブアプリを作ることで、gRPC を使った開発についてイメージしやすくなることを意図している。

Next.js では API Routes を使って API サーバを作ることができるが、それを gRPC クライアントとして実装する。
そのため、リクエストの流れは以下のようになる。

Frontend == (REST) ==> API Routes == (gRPC) ==> gRPC Server

動作確認は Node.js のv16.13.2で行っており、利用しているライブラリのバージョンは以下の通り。

  • gRPC サーバ
    • @grpc/grpc-js@1.5.5
    • google-protobuf@3.19.4
    • grpc_tools_node_protoc_ts@5.3.2
    • grpc-tools@1.11.2
    • ts-node-dev@1.1.8
    • typescript@4.5.5
  • gRPC クライアント
    • @grpc/grpc-js@1.5.5
    • @types/node@17.0.17
    • @types/react@17.0.39
    • eslint-config-next@12.0.10
    • eslint@8.9.0
    • google-protobuf@3.19.4
    • grpc_tools_node_protoc_ts@5.3.2
    • grpc-tools@1.11.2
    • next@12.0.10
    • react-dom@17.0.2
    • react@17.0.2
    • typescript@4.5.5

proto ファイル

まずは、「proto ファイル」と呼ばれる、インターフェイスを定義したファイルを作成する。

プロジェクトのルートディレクトリにprotosディレクトリを作り、そのなかに以下の内容のuser.protoを作成する。

syntax = "proto3";

service UserManager {
  rpc get (UserRequest) returns (UserResponse) {}
}

message User {
  uint32 id = 1;
  string name = 2;
  bool is_admin = 3;
}

message UserRequest {
  uint32 id = 1;
}

message UserResponse {
  User user = 1;
}

UserManagerというサービスを定義しており、このサービスはgetという関数(プロシージャ)を持つ。
getはパラメータとしてUserRequestを受け取り、UserResponseを返す。

この proto ファイルからコードを生成して、クライアントやサーバの開発を行っていく。
この記事ではどちらも TypeScript で開発するが、他の言語を使ってもよいし、クライアントとサーバで言語を揃える必要もない。
gRPC がサポートしている言語なら、どの言語でも proto ファイルからコードを生成できる。

gRPC サーバの開発

プロジェクトのルートディレクトリにserverディレクトリを作り、そこで gRPC サーバの開発を行う。
まずは開発に必要なライブラリをインストールする。

$ mkdir server
$ cd server
$ yarn add @grpc/grpc-js google-protobuf
$ yarn add -D grpc-tools grpc_tools_node_protoc_ts

次に、codegenというディレクトリを作り、proto ファイルをコンパイルしてそこにコードを出力する。

$ mkdir codegen
$ yarn run grpc_tools_node_protoc --plugin=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:codegen --grpc_out=grpc_js:codegen --ts_out=grpc_js:codegen -I ../ ../protos/user.proto

以下のように 4 つのファイルが生成されていれば成功。

$ ls -1 codegen/protos
user_grpc_pb.d.ts
user_grpc_pb.js
user_pb.d.ts
user_pb.js

ここから実際にコードを書いていくので、TypeScript のセットアップを行う。

$ yarn add -D typescript
$ yarn run tsc --init

次に、src/index.tsを作成し、以下のように書く。

import {
  sendUnaryData,
  Server,
  ServerCredentials,
  ServerUnaryCall,
} from "@grpc/grpc-js";
import { UserManagerService } from "../codegen/protos/user_grpc_pb";
import { UserRequest, UserResponse, User } from "../codegen/protos/user_pb";

// 実際には DB のような永続層から取得するはず
const users = new Map([
  [1, { id: 1, name: "Alice", isAdmin: true }],
  [2, { id: 2, name: "Bob", isAdmin: false }],
  [3, { id: 3, name: "Carol", isAdmin: false }],
]);

function get(
  call: ServerUnaryCall<UserRequest, UserResponse>,
  callback: sendUnaryData<UserResponse>
) {
  const requestId = call.request.getId();
  const targetedUser = users.get(requestId);

  const response = new UserResponse();
  if (!targetedUser) {
    throw new Error("User is not found.");
  }

  const user = new User();
  user.setId(targetedUser.id);
  user.setName(targetedUser.name);
  user.setIsAdmin(targetedUser.isAdmin);

  response.setUser(user);
  callback(null, response);
}

function startServer() {
  const server = new Server();
  server.addService(UserManagerService, { get });
  server.bindAsync(
    "0.0.0.0:50051",
    ServerCredentials.createInsecure(),
    (error, port) => {
      if (error) {
        console.error(error);
      }
      server.start();
      console.log(`server start listing on port ${port}`);
    }
  );
}

startServer();

先程生成したuser_grpc_pbuser_pbを import し、それを使ってコードを書いている。

最後に、ts-node-devを使ってサーバを起動する。

$ yarn add -D ts-node-dev
$ yarn run ts-node-dev src/index.ts

server start listing on port 50051と表示されれば成功。

gRPC クライアントの開発

クライアント側は Next.js を使うため、プロジェクトのルートディレクトリに戻って以下のコマンドを実行する。

$ yarn create next-app client --ts

この時点で、以下のようなディレクトリ構成になっているはず。

$ tree ./ -L 2
./
├── client
│   ├── README.md
│   ├── next-env.d.ts
│   ├── next.config.js
│   ├── node_modules
│   ├── package.json
│   ├── pages
│   ├── public
│   ├── styles
│   ├── tsconfig.json
│   └── yarn.lock
├── protos
│   └── user.proto
└── server
    ├── codegen
    ├── node_modules
    ├── package.json
    ├── src
    ├── tsconfig.json
    └── yarn.lock

以降は、clientに移動して Next.js での開発を行う。

まずはサーバのときと同様、gRPC 関連のライブラリのインストールと、proto ファイルのコンパイルを行う。

$ yarn add @grpc/grpc-js google-protobuf
$ yarn add -D grpc-tools grpc_tools_node_protoc_ts
$ mkdir codegen
$ yarn run grpc_tools_node_protoc --plugin=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:codegen --grpc_out=grpc_js:codegen --ts_out=grpc_js:codegen -I ../ ../protos/user.proto

続いて、以下の内容のpages/api/user.tsを作る。これが gRPC クライアントとして機能する。

import type { NextApiRequest, NextApiResponse } from "next";
import { credentials, ServiceError } from "@grpc/grpc-js";

import { UserManagerClient } from "../../codegen/protos/user_grpc_pb";
import { UserRequest, UserResponse } from "../../codegen/protos/user_pb";

const Request = new UserRequest();
const Client = new UserManagerClient(
  "localhost:50051",
  credentials.createInsecure()
);

export type UserApiResponse =
  | { ok: true; user: UserResponse.AsObject["user"] }
  | { ok: false; error: ServiceError };

export default function handler(
  apiReq: NextApiRequest,
  apiRes: NextApiResponse<UserApiResponse>
) {
  const { id } = JSON.parse(apiReq.body);
  Request.setId(id);
  Client.get(Request, (grpcErr, grpcRes) => {
    if (grpcErr) {
      apiRes.status(500).json({ ok: false, error: grpcErr });
    } else {
      const { user } = grpcRes.toObject();
      apiRes.status(200).json({ ok: true, user });
    }
  });
}

gRPC サーバと同様、proto ファイルから生成されたコードを使って実装している。

最後に、pages/index.tsxを編集して UI を作る。

import type { NextPage } from "next";
import { useState, Fragment, ChangeEvent } from "react";

import type { UserApiResponse } from "./api/user";

const App: NextPage = () => {
  const [result, setResult] = useState<string>("");
  const [selectedId, setSelectedId] = useState<number>();

  const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
    const id = Number(e.currentTarget.value);
    setSelectedId(id);

    const res = await fetch("/api/user", {
      method: "POST",
      body: JSON.stringify({ id }),
    });

    const json: UserApiResponse = await res.json();

    if (json.ok) {
      const { user } = json;
      setResult(JSON.stringify(user));
    } else {
      const { code, details } = json.error;
      setResult(`Error! ${code}: ${details}`);
    }
  };

  return (
    <div>
      {[...Array(3)].map((_, index) => {
        const id = index + 1;
        return (
          <Fragment key={id}>
            <input
              type="radio"
              value={id}
              onChange={handleChange}
              checked={id === selectedId}
            />
            {id}{" "}
          </Fragment>
        );
      })}
      <p>{result}</p>
    </div>
  );
};

export default App;

gRPC サーバが起動している状態で$ yarn run devしてhttp://localhost:3000/にアクセスすると、選択したチェックボックスに応じて表示が変わる。
例えば1を選択すると以下が表示されるはず。

{"id":1,"name":"Alice","isAdmin":true}

gRPC サーバが起動していない場合はエラーメッセージが表示される。

Error! 14: No connection established

引きこもり・日記・エンジニア人生

2 年ぶりに労働し始めたことでブログの更新頻度が露骨に落ちているが、文章を全く書いていないわけではなく、折に触れて社内で長文を投下している。

内容は本当に個人的なものというか、自分が考えていることや思っていることを書いているだけで、ブログと同じノリでやっている。
これは私だけがやっているわけではなく、例えば代表の庄田も最近考えていることや意識していることを scrapbox に書いていて、いくつかはブログの一部として社外にも公開されている。

自分の考えを文章にすることは私にとって自然なことなので深く考えずに書いていたのだが、その結果、会社から特別手当が支給された。

勤務先の HERP ではクオーター単位で全社的な振り返りを行っているのだが、そのタイミングで、会社が掲げているバリューを体現している人を対象に表彰が行われる(今日現在)。
私はバリューのひとつである「ストレートに話す」(現在は「ストレートに伝える」)で表彰され、お金をもらえた。
もともと文章を書くのは好きだったのが、それがこうやって何かの役に立つこともある、あるいは有利に働くこともあるんだなと思った。

引きこもりと日記

私が文章を書くようになったキッカケは明確で、引きこもり始めた前後に書くようになった。それまではそういう習慣はなかった。
自分が感じているつらさを他者に伝えることができない、いつの間にか話をはぐらかされている、そういうのがすごく悔しくて、論理的に話せるようになろうと思って、書き始めた。
やがて、他者に伝えるためではなく、自分の考えを整理するために書くようになった。
自分は何が苦しいのか、なぜ上手くやれないのか、どうすれば状況を打開できるのか。それを整理するために、思考と言語化を行ったり来たりしていた。文章にしないと思考がまとまらないし、後で振り返ることもできない。だから、考え事をするときは日記という形で文章に残すようになった。

日記を書くのが習慣になることで、文章を書くことが苦ではなくなった。
かなりエネルギーを使うので、余力がない、時間を捻出できない、ということはあるが、文章を書くこと自体はとても楽しい。
このブログも、書くこと自体がストレス解消になっている。書いている最中も楽しいし、書き上げることができれば、その時点で達成感や満足感がある。
だから続けてこれた。アクセス数や反響を重視していたら、ここまで続けてこれなかったと思う。あくまでも自分のために書いているからこそ、続けてこれた。

引きこもりになってよかったと思ったことなんて一度もないし、恐らくこれからもない。
機会損失はあまりにも大きかったし、スキルやスペックに相当な偏りのある人間になってしまったと思う。
しかし、「失ったものが大きすぎる」からといって、「得たものが何もない」というわけではない。
考えを文章として表現する習慣や書きながら思考を整理する習慣というのは、引きこもっていなかったら身につかなかった。

ブログとキャリア

そしてその習慣に助けられたというか、その習慣のおかげで今もソフトウェアエンジニアとして食べていけている気がする。

自分のために書いているこのブログだが、キャリアの武器としてもそれなりに機能していると思っている。
「30 代」「エンジニアとしての実務経験ゼロ」「エンジニアの知り合いもいない」という私がなんとか業界に潜り込むためには、アウトプットは絶対に必要だと思っていた。そのうちのひとつが、このブログだった。独学を始めて 1 年弱くらいで就職活動を行ったが、「初心者にしては」というエクスキューズはつくものの、ブログによる発信は概ね好評だったと思う。そして無事、希望した企業に入ることができた。

優秀なフロントエンドエンジニアが在籍している会社で、その方がいるから入社したのだが、その方にプログラミングの初歩を教えてもらったおかげで今の自分がいる。
以下の記事の内容の大部分はその方に教えてもらったことであり、私にとっての師匠である。

ブログというアウトプットがなければ、この会社に入れなかったかもしれない(本当に実務経験ゼロだったからその可能性は十分ある)。そう考えると、ブログを書いていたおかげで今のキャリアがあると言える。
これは全くの余談だが、師匠と連絡を取る機会が最近あり、このブログによる発信を褒めて頂いて嬉しかった。

そしてブログによる発信を続けてきたおかげで、社外のエンジニアの方と知り合い、交流する機会も増えてきた。私よりもはるかに優秀なエンジニアの方が私の存在を知ってくれていたりする。

プログラミングを始めたのが 30 歳からで、かつ才覚に乏しい(社内のエンジニアを見てると本当にそう思う。なんで Haskell や Rust をスラスラ書けるのか分からないし、なんで Nix をいきなり使えたりするのかも分からない。)自分がなんとかキャリアを作れているのは、引きこもり時代に培われた「文章を書くのが苦にならない」という特性に支えられている部分が意外と大きいのかもしれない。

特性と環境

特性それ自体に良し悪しはないと思っていて、それが活きるも死ぬも環境次第だと思っている。
「文章を書くのが苦にならない」という特性はエンジニアと相性がよかった。この界隈は個人がブログで発信するのは珍しくなく、基本的には歓迎される。インターネットとの相性のよさ、標準化された知識、など色々と理由はあると思うが、こういう業界は珍しい。

冒頭で述べた表彰にも同じことが言えて、私が「ストレートに伝える」ことができたのは、環境のおかげに過ぎない。
私とある程度以上の付き合いがある人なら分かると思うが、私は決して率直に発言していくようなタイプではない。むしろ「様子見」や「日和見」に徹するタイプであり、新しく入ったばかりの組織なら、なおさらそうする。
そういう私が問題意識を率直に伝えたり、「よくない」と思ったことについて「言ったほうがいい」「言おう」と思えるのは、かなりすごいことだと思う。

これは、オープンな社風によるところが大きい。
代表の庄田が以下のような記事を書いているが、実際に、かなり高い度合いで「オープンな組織」を実現できていると思う。

もうすぐ創業 5 年を迎え、正社員だけでも 50 人弱いる組織でこの透明性を維持できているのは、かなりすごいと思う。
これまで「伝統的な日本企業」や「Slack のコミュニケーションの大部分がプライベートチャンネル」という会社で働いてきたこともあり、なおさらそう思う。

引きこもりに苦しみながら日記を書いていた経験がこういう形でつながってくることがあるとは勿論思っておらず、何となく感慨深かったので、書いてみた。