echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Serverless Framework】version3の新機能&知らなかった機能

はじめに

今年(2022年)1月末に、Serverless Frameworkがメジャーバージョンアップされ、version3がリリースされました。

今までなかなか触る機会がなかったのですが、先日少し触る機会があったので、version3の新機能を記載しようと思います。

また、version2から存在した機能についても、私が知らなかったものを記載しようと思います。

注意

  • 全機能を記載しているわけではないです。
    • あくまで「自分が気づいた点」が中心です。
    • あと、重要そうなものを書いています。

参考サイト

version3の新機能

パラメータのstage毎設定

stage毎に異なるパラメータ(=変数)がある場合(ドメイン名、プレフィックスなど)、それらをstage単位で個別に設定るようになりました。

params:
  prd:
    domain: hogehoge.com
  stg:
    domain: stg.hogehoge.com
  dev:
    domain: dev.hogehoge.com

今までだとcustomセクションにstage毎の変数を設定していましたが、これを使うことで指定がシンプルに出来るようになります。

# 例
# 例えば、version2まではこうだったけど
custom:
  stages:
    dev: 
      domain: dev.hogehoge.com
    stg:
      domain: stg.hogehoge.com
    prd: 
      domain: hogehoge.com
  
provider:
  environment:
    APP_DOMAIN: ${self:custom.stages.${sls:stage}.domain}
  
# version3ではこれでよくなる
params:
  prd:
    domain: hogehoge.com
  stg:
    domain: stg.hogehoge.com
  dev:
    domain: dev.hogehoge.com
  
provider:
  environment:
    APP_DOMAIN: ${param:domain}

また上記のserverless.yamlの通り、self:が不要になったみたいです。
(ちなみにver3.22.0から、self:は完全に削除されたみたいです)

CLIのデザイン更新

Version3になって、CLIのデザインが更新され、シンプルになりました。
(といっても、自分はそこまで意識してなくて、下記公式ブログを見て初めて知りました...)

参考:https://www.serverless.com/blog/serverless-framework-v3-is-live (上記ページの「Redesigned CLI experience」を参照)

公式ブログを見る限り、全般的にversion2よりシンプルになった感じがします。

API Gateway v1(Rest API)のフォーマット変更

API Gateway v1(Rest API)について、apiGatewayという項目をprovider直下に指定しないといけなくなりました。
(今までみたいに、provider直下にパラメータを指定できない)

API Gateway v2(HTTP API)が追加されたことによる変更だと思われます。

参考: API Gateway

# 詳細な設定項目は公式サイト(https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml?prod_DOCS_SEARCH%5Bquery%5D=self%3A#provider)を参照
  
provider:
  # version2の定義
  # provider直下に設定を記載する
  apiKeys: ...
  resourcePolicy: ...
  usagePlan: ...
  ..
  
  # version3の定義
  # provider直下apiGatewayキーを定義し、その下に設定を記載する
  apiGateway:
    apiKeys: ...
    resourcePolicy: ...
    usagePlan: ...
    ...
package、及びIAMの定義内容変更

packageセクション及びproviderセクションのIAM定義について、v2の記載がdeprecated(非推奨)となり、v3独自の書き方が推奨されるようになりました。
(ただしRest APIと違い、こちらはdeprecatedなだけで、v3でもv2の記法自体は使えます)
参照:Deprecated features that will be kept in v3

詳細は下記serverless.ymlを参照してください。

# 公式サイトより

# v2での書き方
provider:
  role: ...
  rolePermissionsBoundary: ...
  iamRoleStatements: ...
  iamManagedPolicies: ...
  cfnRole: ...
  
package:
  exclude:
    - 'src/**'
  include:
    - src/function/handler.js
  
# v3での書き方
provider:
  # IAMについて、iamキーの子孫要素として各設定を定義するようになった
  iam:
    role:
      name: ...
      permissionsBoundary: ...
      statements: ...
      managedPolicies: ...
    deploymentRole: ...
  
# packageについては、exclude/includeが廃止され、
# 正規表現のルールに沿って書くようになった
package:
  patterns:
    - '!src/**'
    - src/function/handler.js

v2からあるけど知らなかった機能

.envファイル使用

dotenv(=npmモジュール)の機能を用いて、各種の値.env(または.env.{stage名})ファイルから読み込んで使用することが可能になりました。

使用するにはuseDotenvプロパティをtrueにすればOKです。(デフォルトはfalse)

useDotEnv: true

また.env.{stage名}ファイルを用意した場合、該当ステージのdeployなどが実施されると、そのファイルが読み込まれます。(デフォルトはstage名=dev)

API Gateway以外のLambdaトリガ用リソースが指定可能に

今まで(Lambdaトリガ用として)providerセクションに定義できるのはAPI Gatewayだけでしたが、それ以外のリソースもproviderで定義可能になりました。(今まではresourcesセクションにまとめて定義していた)

※IAMも定義可能ですが、Lambdaトリガ用ではないです。

下記リソースが定義可能です。

  • ALB
  • ECR
  • CloudFront
  • VPC
  • S3バケット
  • Cloudwatch Logs(上記に関するログ)

詳細な定義は公式サイトの各セクションを参考にしてください。(公式サイトがかなり詳しく&分かりやすく書いてあるので、そちらを見るのがおススメですので...)

providerでAPI Gateway v2(HTTP API)が指定可能に

API Gatewayのv2(=以下HTTP API)もproviderセクションで指定可能になりました。

provider直下にhttpApiという項目を設定し、その下に各種パラメータの値を設定すればOKです。

先程v1(Rest API)もapiGatewayという項目をprovider直下に指定しないといけなくなったと書きましたが、HTTP API追加の影響もあるでしょうね。
参考: API Gateway

# 詳細な設定項目は公式サイト(https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml?prod_DOCS_SEARCH%5Bquery%5D=self%3A#provider)を参照
  
provider:
  # v2(HTTP APIの定義)
  httpApi:
    name: hogehoge 
    ...
  
  # v1(Rest APIの定義)
  apiGateway:
    apiName: fugafufa
    ...
functions(Lambda定義)でもAPI Gateway v2(HTTP API)が指定可能に

provider同様、Lambda関数のトリガ元としてHTTP APIが指定可能になりました。

下記のように、HTTP APIの場合eventsに「httpApi」を指定します。

# 公式サイトより抜粋
functions:
  hello:
    handler: index.handler
    events:
      # こちらはHTTP API
      - httpApi:
          method: GET
          path: /some-get-path/{param}
          
      # こっちはRest API(こちらは変更なし)
      - http:
          path: users/create
          method: get
AWS Lambda Function URL対応

今年4月に発表された、AWS Lambda Function URL(=Lambda関数URL)に対応しました。

単にAWS Lambda Function URLに対応される場合、urlキーにtrueを設定するだけでOKです。

また、authorizer及びcorsを設定すると、オーソライザーやCORSの細かい設定ができます。(詳細は下記サイトを参照)

参考:Lambda Function URLs

# 公式サイトより抜粋
functions:
  hello:
    handler: index.handler
    #単に対応させるだけなら、url:trueを設定するだけでOK
    url: true
  
  hello2:
    handler: index2.handler
    url: 
      # 追加項目として、オーソライザやCORSが設定可能
      authorizer: aws_iam
      cors:true

まとめ

以上がversion3の新機能&知らなかった機能(の一部)になります。

こちらに破壊的変更(Breaking Changes)の一覧が記載されていますが、見た限りそこまで致命的な変更は無いように感じました。(serviceやAPI Gateway v1くらいですが、修正はあっという間に終わりそう)

また、今回は紹介しきれなかった機能もありますので、興味がある方はぜひ調べてみてください。

それでは、今回はこの辺で。

【JAWS DAYS】JAWS DAYS 2021登壇体験記

今回の記事

Japan AWS User Group(JAWS-UG)が開催するイベント、JAWS DAYSが今年も開催されることが決まりました。(公式サイトは下記)

note.com

それに関して、運営の方から「今年登壇を検討している方向けに、昨年登壇した体験談を書いてほしい」という依頼がありました。

そこで今回は、JAWS DAYS 2021の登壇体験談を書こうと思います。

ちなみに、私のJAWS DAYS 2021での登壇内容は下記の通りです。

www.slideshare.net

アジェンダ

  • CFPを出そうと思った動機
  • 登壇に向けてどんな準備をしたか
  • 当日登壇してみてどうだったかとか
  • JAWS DAYS2021の印象や感想

CFPを出そうと思った動機

自分の中で、CFPを出そうと思った動機は以下の理由からです。

  • JAWS UG、及び他のイベント(=主にNode.jsやVS Codeなど)での登壇も何回か行っており、AWSについても何か発表をしたかった
  • 自分の知見が、他の人(特に初心者の人)に役立てればいいなと思った
  • 登壇(=アウトプット)を行うことで、結果的にインプットも行う形になり、自分の為にもなるから
  • 少し前のイベント(これもAWS関連の何か大きなイベント。詳細は失念)でCFPを出したが落選したので、今回こそは採用されたいという思いがあった

ただ、あまり難しいことを考えずとも、少しでも「興味があった」「登壇したいと思った」と感じたならば、動機としてはそれで十分だと思います。

大事なのは、とにかく「やってみる」ことだと思います。

登壇に向けてどんな準備をしたか

登壇に向けた準備ですが、基本的には以下のことです。(ありきたりですが...)

発表資料を作る

当然と言えば当然ですが、発表資料を作らないと始まりません。
なので、自分の中で「何を書くか」や「資料の構成」をある程度整理したら、とりあえず発表資料を書きました。

自分は時間ギリギリで慌てて書くのは苦手なので、毎日少しづつでいいので、コツコツ作成するようにしていました。
またそういうやり方にすると、書いている途中で「やっぱりこうしよう」「これも書いたら分かりやすいかも」みたいなアイデアが結構浮かんでくるので、個人的にはコツコツ作成するのをお勧めします。(もちろん本人次第ですが...)

手を動かす(裏を取る)

書いた内容について、間違いや認識違いがないかを、実際に手を動かして確認することも重要です。

以前触った技術だったとしても、アップデートなどで仕様が変わっていたり、実は自分の認識違いがあったりする場合もあるので、手を動かして書いた内容の裏を取ることも大事だと思います。

また、そうすることで新たな知見を得ることもできるので、自分のスキルアップにもつながります。(これが「アウトプットはインプットにもなる」と言われる理由)

リハーサルをする(重要!)

あとは、ちゃんとリハーサルをすること。
というか、リハーサルはかなり重要だと思います。

ちゃんと時間を区切ってリハーサルを行うことで、事前に下記の確認を行えます。

  • 自分が詰まりやすいポイントを事前に把握し、対策がとれる
  • 実際の登壇環境でどうなるかを確認できる
  • 資料の長さや発表にかかる時間を把握できる

特に最後の「発表にかかる時間」は重要で、JAWS DAYSなどの大きなイベントでは、基本時間オーバーは許されません。(途中でも配信が切られる)
なので、ちゃんと時間内に終わるかどうかというのを事前に確認しておくのはすごく重要です。(早く終わる分には問題ないです)

また、当日は緊張したりしてリハーサルほどうまく話せない...というケースもあるので、多少余裕をもって終われるか...というのを事前に確認しておいた方が良いかもしれません。

(というかJAWS DAYSに限らず、基本的にどんな勉強会でも、時間オーバーはすべきではないと個人的には思います。
配信の都合や1~2分程度は仕方ないと思いますが、たまに5~10分以上もオーバーして、当たり前のように発表を続けている人を見ると、疑問を感じてしまいます)

また「実際の登壇環境」についても重要です。

JAWS DAYS 2022はまだどうなるか不明ですが、例えばJAWS PANKRATION 2021ではPocketalkによる同時翻訳がありました。
そういった場合に、事前にリハーサルをやることで「Pocketalkがうまく翻訳できないor認識しない言葉はないか(例えばAWSの専門用語とか)」などの確認ができますし、それに合わせて発表内容を変えたりすることもできます。

当日登壇してどうだったか

当日登壇した感想としては、下記のような感じです。

  • 環境は問題ないか、運営側と事前に確認することが大事
  • 緊張してしまい、ちょっと早口になってしまった
  • 100%リハーサル通り完璧には発表できない
  • でも終わったときは「登壇してよかった」と思った

やっぱりどんだけリハーサルをやっても、100%リハーサル通りにはいかないので、そういう意味でも事前にしっかりリハーサルをやるのは重要だなあ、と感じました。

ただ登壇を終わってやり切った際には、本当に「登壇してよかった」と強く感じました。(これは本当に)

JAWS DAYS2021の印象や感想

これについては、本当に大きいイベントだし、一ユーザーグループのイベントとしては、最大レベルの規模なのではないかなと思います。
てか、こういう場所を提供していただいた運営者の方々には本当に感謝です。

というか、ユーザーグループのイベントでここまで大規模なイベントが開催されるって、本当にすごいですよね。

まとめ

と、私のJAWS DAYS 2021登壇体験記を書きました。

色々書きましたが、やはり大事なのは「一歩踏み出す」ことかなあと思います。
なので少しでも興味を持ったならば、まずはCFPを出してみましょう。

いろいろ大変かもしれませんが、きっと得られるものも大きいと思います。

というわけで、私のJAWS DAYS 2021登壇体験記は以上となります。

【AWS】AWS SDK for JavaScript v3でLambdaを書く(S3編)

今回の内容

前回、AWS SDK for JavaScript v3(以下v3)でDynamoDBを扱うLambdaを書くというブログを投稿しました。
今回はその第二弾で、S3を扱うLambdaを書いてみます。

S3はAWSのリソースの中でも扱う機会がトップクラスに多い(と思う)ので、覚えて損はないと思います。

なお今回はPutObjectCommand(ファイル作成)、およびGetObject(ファイル読み込み)を扱います。

参考サイト

PutObjectCommand(ファイル作成)

S3もDynamoDB同様、v3のお作法通り

  1. 各リソースのクライアントを定義する
  2. 各リソースの操作コマンド(=これがv2のAPI関数に相当)を定義する
  3. 1のクライアントのsendメソッドで、2のコマンドを送信する
  4. 3の結果を取得する。(これがAPIの戻り値に相当)

という流れでソースを書けばOKです。

というわけで、まずはファイル作成のソースを書いてみます。

まずは事前に@aws-sdk/client-s3をインストールしておきます。
また前回同様、Lambdaの型定義用に@types/aws-lambdaもインストールしておきます。

import { Context, APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda';
import { S3Client, PutObjectCommand, PutObjectCommandInput } from '@aws-sdk/client-s3';

const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
  
  // 書き込むテキスト
  const contents = '隣の客はよく客食う客だ';

  // S3クライアントを定義
  const client = new S3Client({});
  
  // 作成するファイル情報
  const putParams: PutObjectCommandInput = {
    Bucket: 'your-bucket-name',
    Key: 'sample.txt',
    Body: contents,
  }
  
  // コマンドを定義&送信
  // 今回は戻り値の取得は不要。(成功or失敗が分かればOK)
  const putCommand = new PutObjectCommand(putParams);
  await client.send(putCommand);
  
  const response: APIGatewayProxyResult = {
    statusCode: 200,
    body: ’成功’,
  };
  
  return response;
};
  
module.exports = { handler };

特に問題ないですね。
ソースもv3のお作法通りだし、PutObjectCommandの引数(PutObjectCommandInput)の内容も、v2のputObjectと同じでOKです。

実際にこのソースと試すと、(設定に問題がなければ)指定したバケットに「sample.txt」というファイルが作成され、その中身は「隣の客はよく客食う客だ」となっているはずです。

では、次はファイルの読み込みです。

GetObjectCommand(ファイル読み込み)

といっても、これも基本はv3のお作法通りでOKです。
またGetObjectCommandの引数(GetObjectCommandInput)の内容も、これまたv2のgetObjectと同じでOKです。

// ネタバレ:このコードは正しく動きません
import { Context, APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda';
import { S3Client, GetObjectCommand, GetObjectCommandInput } from '@aws-sdk/client-s3';

const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
  
  // クライアント定義
  const client = new S3Client({});
  
  // 取得するファイル情報
  const getParams: GetObjectCommandInput = {
    Bucket: 'your-bucket-name',
    Key: 'sample.txt',
  };
  
  // コマンド定義&送信
  const getCommand = new GetObjectCommand(getParams);
  const res = await client.send(getCommand);

  // res.Bodyに読み込んだファイル内容が格納される
  const response: APIGatewayProxyResult = {
    statusCode: 200,
    body: JSON.stringify({
      contents: res.Body
    }),
  };

  return response;
};  
  
module.exports = { handler };

...そう思っていた時期が、俺にもありました。(バギ並感)

Bodyの型が違う

コメントに書いた通り、上記ソースはエラーになってしまい、正しく動きません。
最後のresponseを設定する部分のcontents: res.Body の部分がエラーになります。

実はv3では、Bodyの型がNode.js Stream APIのReadable(=Stream.Readable)型になってます。(Bufferではない)
そのため、Bodyを直で参照しても、ファイルの内容は取得できません。

ファイルの内容を取得するためには、Streamから文字列を取得する処理が必要になります。

というわけで、それを反映したのが下記ソースになります。

なおNode.js Stream APIの型定義を扱うために、@types/readable-streamをインストールしておくと便利です。

import { Context, APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda';
import { Readable } from 'readable-stream';
import { S3Client, GetObjectCommand, GetObjectCommandInput } from '@aws-sdk/client-s3';

const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
  
  const client = new S3Client({});
  
  const getParams: GetObjectCommandInput = {
    Bucket: 'suzukima-s3-local-test-bucket2',
    Key: 'sample.txt',
  };
  
  const getCommand = new GetObjectCommand(getParams);
  const res = await client.send(getCommand);  
  
  // Stream.Readable型に変換 
  const body = res.Body as Readable;
  
  // Stream.Readableを文字列に変換する
  let fileContents = '';
  for await (const chunk of body) {
    fileContents += chunk;
  }
  
  //  こっちのやり方でもOK(公式サイトはこっちのやり方)
  // const streamToString = (stream:Readable) =>
  //   new Promise((resolve, reject) => {
  //     const chunks:Uint8Array[] = [];
  //     stream.on("data", (chunk) => chunks.push(chunk));
  //     stream.on("error", reject);
  //     stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
  // });
  // 
  // fileContents = await streamToString(body);  
  
  const response: APIGatewayProxyResult = {
    statusCode: 200,
    body: JSON.stringify({
      contents: fileContents
    }),
  };
  
  return response;
};
  
module.exports = { handler };

上記を実施すると、下記レスポンスを取得できると思います。

{
  "contents": "隣の客はよく柿食う客だ"
}

なおローカルなどで実行していて「Stream.Readableの内容をファイルに保存したい」という場合、下記のようにします。

import fs from 'fs'  
  
// (const body = res.Body as Readable までは同じなので省略)
  
// Stream.WriteStreamを作成する。
const ws = fs.createWriteStream('sample2.txt'); 
  
// Stream.WriteStreamにStream.ReadStreamの内容を少しづつ書き込む。
body.pipe(ws);

ちなみにNode.js Stream APIについては、「参考サイト」に記載したサイトが詳しく説明してくださっています。

ちなみに

ファイル内容が大量ならともかく、ちょっとした文字列ですらStream.Readableを文字列に変換するのはやっぱりアレということで(?)、下記issueが立ってました。
S3.GetObject no longer returns the result as a string · Issue #1877 · aws/aws-sdk-js-v3 · GitHub

また上記issueのリプによると、get-streamというnpmモジュールを使えば、変換処理を1行で書けるそうです。
https://github.com/aws/aws-sdk-js-v3/issues/1877#issuecomment-799697205

import getStream from 'get-stream';
  
fileContents = await getStream(body);  

まとめ

以上、AWS SDK for JavaScript v3でS3を扱う方法でした。

GetObjectCommandのBodyの扱いだけが変わっているので、そこさえ気を付ければ、あとはv2と同じ方法でOKです。

また毎回Stream.Readableを文字列に変換する処理を書くのが面倒な場合、何か専用の共通処理を書いてもいいかもしれませんね。

それでは、今回はこの辺で

【AWS】AWS SDK for JavaScript v3でLambdaを書く(DynamoDB編)

今回の内容

  • AWS LambdaをAWS SDK for JavaScript v3(以下「v3」と記載)で書く
    • 現在メジャー(だと思う)のはAWS SDK for JavaScript v2(以下「v2」と記載)
  • サンプルとして、DynamoDBのデータを取得するコードを書く

参考サイト

v3について&v2と違う点

大きく変わったのは下記2点だと思います。

各リソースごとに個別にインストールする

v3は各リソース(DynamoDB, S3など)のソースがモジュール単位で独立しています。(v2は全リソースのソースが単一パッケージに入っている)

そのため、例えば「S3のみ扱いたい」ような場合に、S3のモジュールのみ個別にインストールする形になりました。

APIの変更

(実際のソースは後述しますが)API(の実施方法)がv2とは完全に別物です。

v3では、どのリソースも基本的に下記の流れでAPIを実施します。

  1. 各リソースのクライアントを定義する
  2. 各リソースの操作コマンド(=これがv2のAPI関数に相当)を定義する
  3. 1のクライアントのsendメソッドで、2のコマンドを送信する
  4. 3の結果を取得する。(これがAPIの戻り値に相当)

上記を見てもわかる通り、v2とは完全に別物(≒破壊的変更クラス)なので、最初は戸惑うかもしれません。

v3のメリット・デメリット

メリット:サイズが小さくなる

先述の通り、v3ではリソース単位で個別にインストールします。
そのため、不要なリソースのモジュールが入らない分、最終的なサイズがv2より少なくなります。

デメリット1:LambdaのNode.jsランタイムに入っていない

v3はAWS LambdaのNode.jsランタイムにプレインストールされていません。(v2はされている)
そのため、v2とは違いAWS上で実行する場合、何らかの方法でデプロイパッケージに含める必要があります。(webpackなりLambda Layerなり)

先程メリットで「サイズが小さくなる」と書きましたが、上記の理由により、AWS上で動かす場合はむしろ最終的なサイズはv2に比べて増えてしまいます。

どっちとも言えない1:APIが完全に別物

v2とはソースが完全に別物なので、最初は戸惑ったり毛嫌いしてしまうかもしれません。
が、こればっかりは仕方がないので、慣れるしかない...というのが正直な感想です。

今回自分もv3のソースを書きましたが、実際v3の書き方でもそこまで不便さや書きにくさは感じなかったので、多分毛嫌いさえしなければ時間と経験が解決してくれるんではないかと思います。

どっちとも言えない2:情報が少ない

まだまだv2の方がメジャー(だと思う)ので、情報量は圧倒的にv2の方が多いです。

ただ、それでもv3の有益な情報はたくさんありますし、なにより公式の情報が結構充実しているので、そこまで情報が不足している...とまでは感じませんでした。

前提:今回使用するAPI

今回使用するAPIですが、過去にも出てきた、自作の「ドルアーガの塔 宝物取得API」を使用しています。
内容については「パスパラメータfloorで指定した階の宝物情報を返す」というものです。

もし気になった方は「ドルアーガの塔 宝物 一覧」などでググってみると良いかもしれません。

実際のソース

では、公式サイトの説明に沿って、ソースを書いてきます。
事前に@aws-sdk/client-dynamodbをインストールしておきます。

またTypeScriptで書く場合、合わせて@types/aws-lambdaもインストールしておくと、Lambdaトリガーが扱いやすくなります。

// AWS CDK v3のソース
import { Context, APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient, DynamoDBClientConfig, GetItemCommand, GetItemCommandInput } from '@aws-sdk/client-dynamodb';
  
const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
  const floor = event.pathParameters?.floor ? Number(event.pathParameters.floor) : -1;
  
  // 1. クライアントを定義する
  // なおconfigの定義は、v2のものとだいたい同じ
  const config: DynamoDBClientConfig = {};
  const client = new DynamoDBClient(config);
  
  // 2. 操作コマンドを定義する。  
  // 今回はGetItemCommand(v2のGetItemに該当)を定義  
  // paramの定義も、v2のものとだいたい同じ
  const param:GetItemCommandInput = {
    TableName : 'tower-of-druaga',
    Key: {
      Type: {'S': 'treasure'},
      Floor: {'N': `${floor}`}, 
    },
  };
  const command = new GetItemCommand(param);
  
  // 3. 1のクライアントのsendメソッドで、2のコマンドを送信する  
  // 4. 3の結果を取得する。(これがAPIの戻り値に相当)
  const data = await client.send(command);
     
  const response: APIGatewayProxyResult = {
    statusCode: 200,
    body: JSON.stringify({
      treasures: data,
    }),
  };

  return response;
};
  
module.exports = { handler };

コメントで補足しましたが、「APIの変更」に記載した流れに沿って書きました。
今回はDynamoDBを例にしましたが、v3では他のリソースも大体この流れになります。

あと、client.send()の戻り値はPromise<T> 型なので(最初からPromise型で返ってくる)、最後に.promise()を実施しなくてよくなったのは、地味ながらv3の便利な点だと思います。

上記を実施すると、下記結果が返ります。

「$metadata」にメタデータ情報、Itemに該当データの情報がちゃんと格納されています。

{
    "$metadata": {
        "httpStatusCode": 200,
        "requestId": "QL08LADH1ME1QFG4(以下省略)",
        "attempts": 1,
        "totalRetryDelay": 0
    },
    "Item": {
        "Necessary": {
            "BOOL": false
        },
        "Floor": {
            "N": "1"
        },
        "Type": {
            "S": "treasure"
        },
        "Detail": {
            "L": [
                {
                    "M": {
                        "Condition": {
                            "S": "グリーンスライムを3匹倒す"
                        },
                        "Effect": {
                            "S": "壁を宝箱を取る前後1回ずつ壊せる"
                        },
                        "Memo": {
                            "S": ""
                        },
                        "Name": {
                            "S": "カッパーマトック"
                        }
                    }
                }
            ]
        }
    }
}

DocumentClientを使う

ただDynamoDBのデータを扱う場合、DocumentClientで扱った方が便利です。
なので、次はDocumentClientを使ってみます。

なおDocumentClientを扱う場合、追加で@aws-sdk/lib-dynamodb, および@aws-sdk/util-dynamodbが必要になるので、これもインストールしておきます。

※@aws-sdk/client-dynamodbも必要です。

ただ下記ソースの通り、DocumentClientクライアントを生成する箇所以外、基本は変わらないです。(コマンドの名前や型以外)

import { Context, APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient, DynamoDBClientConfig } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, GetCommandInput } from '@aws-sdk/lib-dynamodb'
  
const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
  const floor = event.pathParameters?.floor ? Number(event.pathParameters.floor) : -1;
  
  const config: DynamoDBClientConfig = {};
  const client = new DynamoDBClient(config);  
  
  // ここでDocumentClientクライアントを生成する。
  const dcClient = DynamoDBDocumentClient.from(client);
  
  // paramの定義も、v2のgetのものとだいたい同じ
  const param:GetCommandInput = {
    TableName : 'tower-of-druaga',
    Key: {
      Type: 'treasure',
      Floor: floor, 
    },
  };
 
  // クライアントのsendを実行するのは同じ
  const command = new GetCommand(param);
  const data = await dcClient.send(command);
  
  const response: APIGatewayProxyResult = {
    statusCode: 200,
    body: JSON.stringify({
      treasures: data.Item,
    }),
  };
  
  return response;
};
  
module.exports = { handler };
{
    "$metadata": {
        "httpStatusCode": 200,
        "requestId": "OMUB8IN7COJVLBG7(以下省略)",
        "attempts": 1,
        "totalRetryDelay": 0
    },
    "Item": {
        "Necessary": false,
        "Floor": 1,
        "Type": "treasure",
        "Detail": [
            {
                "Condition": "グリーンスライムを3匹倒す",
                "Effect": "壁を宝箱を取る前後1回ずつ壊せる",
                "Memo": "",
                "Name": "カッパーマトック"
            }
        ]
    }
}

まとめ

以上、AWS SDK for JavaScript v3でLambdaを書いてみた結果でした。

書いた感想として、とにかく慣れかな...と感じました。
ただ先述の通り、v2と比べてもそれほど不便さや使いにくさは感じませんでした。

なお、AWS公式はv3を推奨しています。
また今のところv2もサポート終了という話はありませんが、v2が登場から8年も経っているので、(v3が登場してある程度経ったこともあり)何かあってもおかしくはないかもしれません。

なので、これからはv3を作成するか、あるいはv3への移行も視野に入れた方が良いのかもしれません。

それでは、今回はこの辺で

【Node.js】「みんなの自動翻訳」APIを使ってみた

はじめに

みなさん、翻訳ツールって何を使ってますか?
多くはGoogle翻訳やDeepLとかではないかと思います。

そんな中、機械学習型の「みんなの自動翻訳」というサイトがあったので、こちらを試してみました。

みんなの自動翻訳@TexTra®

参考サイト

コピペ翻訳機を作る 目標〜翻訳APIの利用|Erectronでマルチプラットフォームなデスクトップアプリケーションを作ろう

みんなの自動翻訳の特徴

  • 国立研究開発法人情報通信研究機構NICT)が開発した自動翻訳サイト
  • 最新の自動翻訳研究に基づく「高精度自動翻訳エンジン」が無料で使える
  • 翻訳WEB APIが無料公開されている
  • VS Code用の拡張機能も公開されている

使用にはサインアップが必要ですが、翻訳やWeb API, VS Code拡張機能もすべて無料で使用できます。

そして個人的には「無料で翻訳WEB APIが使える」ところが大きいので、今回はそれを試してみました。

Web APIについて

ログイン後、Web API一覧→自動翻訳リクエストとページ移動すると、下記画面が表示されます。
そこで「API」の「i」アイコンをクリックすると、APIの詳細仕様や言語別のソースコードが表示されます。


そこで今回は「汎用NT【英語・日本語】」のWeb APIをNode.jsで実行します。

  • ログインすると「API Key」「API Secret」「ログインID」の3つが発行されます。
    • この3つがWeb API実行に必須となります。
  • 認可形式はOAuth/OAuth2です。
  • 通信形式はPOSTです。

実行してみた

というわけで、Node.jsで書いてみました。

ちなみに、せっかく前回ES2022の正式機能を紹介したので、今回はES2022の正式機能に採用されたクラスの機能(フィールド&private要素)を使って書いてみました。

またOAuth2のクライアント認証に、axios-oauth-clientを使用しています

// 翻訳を実行するクラス
// 詳細は公式ページのソースを参照。
// なお公式ページはrequestを使ってますが、今回はaxiosで記載
const axios = require('axios');
const oauth = require('axios-oauth-client');
const axiosFetcher = axios.default;
  
class MinnaTransClass {
    // これが全APIのベースとなるURL
    #BASE_URL = 'https://mt-auto-minhon-mlt.ucri.jgn-x.jp';
      
    #key = '';       // API Key
    #secret = ';'   // API Secret
    #name = '';   // ログインID
    #token= '';    // アクセストークン
    #expire = 0;  // 有効期限(今回はほとんど使ってないです)
    
    constructor(key, secret, name) {
        this.#key = key;
        this.#secret = secret;
        this.#name = name;
    }
    
    // 英→日翻訳
    async translateEn2Ja(text) {
        if(this.#isNeedNewToken()) {
            await this.#getAuthorizationCode();
        }
        
        return (await this.#request('mt/generalNT_en_ja/', text));
    };
    
    // 日→英翻訳
    async translateJa2En(text) {
        if(this.#isNeedNewToken()) {
            await this.#getAuthorizationCode();
        }
        
        return (await this.#request('mt/generalNT_ja_en/', text));
    };
    
    // アクセストークン取得要否を判定する。  
    // 有効期限5分前を切ったら再取得する。  
    // (今回使うことはないけど)
    #isNeedNewToken() {
        return this.#token === '' || this.#expire < Date.now();
    }
    
    // OAuth2認可を行い、アクセストークンを取得する関数
    // urlとurlAccessTokenにはOAuth2認可の固定URLを設定する。
    // grant_typeはclient_credentials固定。  
    // client_idとclient_secretはそれぞれAPI KeyとAPI Secretを指定する。
    async #getAuthorizationCode() {
        const oauthFunc = oauth.client(axios.create(), {
            url: `${this.#BASE_URL}/oauth2/token.php`,
            grant_type: 'client_credentials',
            client_id: this.#key,
            client_secret: this.#secret,
            urlAccessToken: `${this.#BASE_URL}/oauth2/token.php`
        });
  
        const auth = await oauthFunc();
        this.#token = String(auth.access_token) || '';
        this.#expire = Number(auth.expires_in) + Date.now(); 
        return;
    } 
    
    // 実際に翻訳APIを実行する関数
    // postするパラメータは下記コメント参照
    // また詳細はAPIの詳細仕様を参照  
    // ちなみにtype以外は必須パラメータ
    async #request(path, text) {
        const pathElements = path.split('/');
        const params = {
            access_token: this.#token,  // アクセストークン
            key: this.#key,  // API Key                  
            api_name: pathElements[0], // 固定文字列(汎用NTの場合'mt'固定)
            api_param: pathElements[1], // 英→日および日→英で固定の文字列 
            name: this.#name,  // ログインID
            text,  // 翻訳するテキスト
            type: 'json',  // レスポンス形式(xml/json, デフォルトはxml)
        };
        
        // URLSearchParamsにしないと、ログインID認証エラーが発生した  
        var searchParams = new URLSearchParams();
        for (let key in params) {
            searchParams.append(key, params[key]);
        }
        
        const res = await axiosFetcher.post(`${this.#BASE_URL}/api/`, searchParams);
        
        // 翻訳テキストはrespose.bodyのresultset.result.textに格納される
        return res.data.resultset.result.text;
    }
};
  
module.exports = {
    MinnaTransClass: MinnaTransClass,
};
// 上記クラスを使用して、実際に翻訳APIを呼び出す
import { MinnaTransClass } from './minnaTransClass.js';

const minnaTrans = new MinnaTransClass('(API Key)', '(API Secret)', '(ログインID)');
  
const resource = '隣の客はよく柿食う客だ';
console.log(`原文:${resource}`);
const result = await minnaTrans.translateJa2En(resource);
console.log(result);

const result2 = await minnaTrans.translateEn2Ja(result);
console.log(result2);

結果、こうなりました。

翻訳としては、十分OKではないでしょうか?(customerを2回使ってたり、whoがちょっとくどかったり、細かい所はあるけど)

もちろん翻訳としても十分使えますし、何より動作確認などの目的で使える無料のWeb APIがある、というのが大きいです。

VS Code拡張機能

みんなの自動翻訳はVS Code拡張機能としても公開されています。

TexTra - Visual Studio Marketplace

公式サイトの動画でも紹介されていますが、下記手順で実行します。(ほかにもコマンドはあるので、それは公式サイトを参照)

  • コマンドパレットで「TexTra: API Settings」を選択し、API Key, API Secret, ユーザーIDを設定する。
  • 翻訳したいテキストを選択して、コマンドパレットで「Textra: Translate Selected Text」を選択する。
    • この際、翻訳結果を選択テキストの直下に貼り付けるか、クリップボードにコピーするかを選べます。

まとめ

以上、みんなの自動翻訳の紹介でした。

翻訳ツールとしてもそうですが、Web APIとしてもなかなか便利で、良いなあと思いました。
これからちょくちょく利用しようと思います。

では、今回はこの辺で

【JavaScript】ES2022で正式採用される機能について

本題

JavaScriptの仕様であるECMAScript(=ES)について、そろそろ最新版のES2022が承認される、というのをICSさんのブログ(「参考サイト」参照)で知りました。

なので今回は、ES2022で正式採用される機能について、コード例を挙げて説明したいと思います。

なお今回紹介する機能ですが、Node.jsの場合、Node Greenを見るとわかる通り、2022/6/18現在のLTSであるver16.15.1では、普通に全機能が使用可能です。(ブラウザでもほとんどの機能は使用可能の模様)

参考サイト

紹介する機能一覧

  • クラス以外の機能
    • top-level await
    • .at() method on the built-in indexables
    • Object.hasOwn
    • Error.cause property
    • RegExp Match Indices
  • クラス関連
    • フィールド・メソッド・アクセスレベル
      • クラスフィールド、static、private
    • Ergonomic brand checks for private fields

クラス以外の機能

top-level await

モジュールの最上位ソースに限り、asyncなどをつけなくても、直接awaitをつけることができるようになりました。
これにより最上位ソースで非同期処理を扱いやすくなりました。

なお、top-level awaitはモジュール(※)限定なので、そうではないJSソースで実行してもエラーになります。
またあくまで最上位ソース限定なので、そうではない場所(例えば関数内など)で実行する場合は、今まで通りasync定義が必要です。

※下記に該当するソ-ス

  • 拡張子*.mjsのファイル
  • package.json"type":"module"が定義されたファイル
  • <script type="module"> が定義されたHTMLスクリプト
import * as axios from 'axios';

// モジュールファイルの最上位のみ、asyncが不要。  
// 下記ソースは正常動作する。
const x = await axios.default.get("https://hogehoge.com/users/1");
console.log(JSON.stringify(x.data));  
  
// 関数内など、トップレベル以外ではasyncが必要。  
// なので下記ソースはエラーになる。 
function badExampleFuncAsync() {
  const x = await axios.default.get("https://hogehoge.com/users/1");
  console.log(JSON.stringify(x.data));  
}  
  

.at() method on the built-in indexables

String及びArray, TypedArrayの要素に、.at(index)でindexの要素にアクセスできます。
「indexの要素にアクセス」自体は元からあるのですが、substr関数のように、負数を指定することで末尾からのindexを指定できます。

これにより、例えば配列の末尾要素を指定する際に、今までのようなarray[array.length -1]というソースは不要になります。

const str = 'hello,es2022';
const array = ['hello', 'es', '2022'];
  
// これは今までと同じ。
console.log(str.at(6));  // e
console.log(array.at(2));  // 2022  
  
// 負数を指定することで、末尾からの要素を指定できる。  
// (-1が末尾要素のindex)
console.log(str.at(-8));  // o
console.log(array.at(-3));  // hello

Object.hasOwn

オブジェクトが該当プロパティを持っているかを判定できる関数です。
というか実際にはObject.prototype.hasOwnProperty.callのショートハンドになります。

ただhasOwnPropertyにはプロトタイプ汚染の問題がある&上記が長くて煩わしいので、それを防ぐ目的だと思われます。

プロトタイプ汚染については、こちらのサイトで詳しく記載されています。

const obj = {
  x: 10,
  y: 20,
};
  
console.log(Object.hasOwn(obj, "x"));  // true
console.log(Object.hasOwn(obj, "z"));  // false

Error.cause property

親子関係(≒依存関係)があるソースでエラーが発生した際、エラーの発生源をたどれるようになる仕組みです。
これはなかなか文章で説明しにくいので、具体的なソースを見た方が早いと思います。

function parent() {
    try {
        child();
    } catch (e) {
        // ここのeはchild()からthrowされたもの。  
        console.log(`e is ${e.message}`);    
        console.log(e.cause.message);     
        console.log(e.cause.cause.message);
    }
}
  
function child() {
    try {
        grandChild();
    } catch (e) {  
        // ここのeはgrandChild()からthrowされたもの。
        // 発生源にe(=grandChild Error)を指定
        throw new Error("child Error", {cause: e});
    }
}
  
function grandChild() {
    try {
        nextGrandChild();
    } catch (e) {
        // ここのeはnextGrandChild()からthrowされたもの。
        // 発生源にe(=nextGrandChild Error)を指定
        throw new Error("grandChild Error", {cause: e});
    }
}
  
function nextGrandChild() {
    // ここがエラー発生源
    throw new Error("nextGrandChild Error");
}
  
parent();

上記ソースを実行すると、下記ログが出力されます。(不要な部分はカット)

// childでthrowされたエラーのメッセージ
e is child Error  
  
// childでthrowされたエラーのcauseに設定されたエラー  
// (=grandChildでthrowされたエラー)のメッセージ
grandChild Error  
  
// grandChildでthrowされたエラーのcauseに設定されたエラー  
// (=nextGrandChildでthrowされたエラー)のメッセージ
nextGrandChild Error

RegExp Match Indices

正規表現でのマッチングについて、dフラグを付けることで、一致した位置の情報を付加することができるようになりました。
一致位置情報は戻り値のindicesプロパティで確認できます。

const result = /cd/d.exec('abcdefabcdefg');
console.log(result);

const result2 = /cd(ef)/d.exec('abcdefabcdefg');
console.log(result2);

上記の実行結果がこちら。

// resultの結果
[
  'cd',
  index: 2,
  input: 'abcdefabcdefg',
  groups: undefined,
  indices: [ [ 2, 4 ], groups: undefined ]
]
  
// result2の結果
[
  'cdef',
  'ef',
  index: 2,
  input: 'abcdefabcdefg',
  groups: undefined,
  indices: [ [ 2, 6 ], [ 4, 6 ], groups: undefined ]
]

indicesのindex[0]に、一致位置の開始位置&終了位置が配列で入ってきます。
なお終了位置はsilce関数で指定するものと同じで、「一致文字列の最終文字のindex+1」となります。
(resultの例(=「cd」)で言えば、「d」のindexである3ではなく、4になる)

indicesのindex[1]以降では、キャプチャグループがある場合にキャプチャグループとの一致位置の開始位置&終了位置が配列入ってきます。
今回の場合、result2でキャプチャグループ「ef」の一致位置情報である配列[4, 6]が返ります。

クラス関連

ここからは、クラス関連の機能になります。
ES2022はクラス関連の機能が多く、やっとクラスの基本的な機能が多言語に追いついたという印象です。

フィールド・メソッド・アクセスレベル(まとめて説明)

クラスの基本的な要素であるフィールド・メソッド・アクセスレベルについて、下記機能が追加されました。

  • クラスフィールドを設定可能になった。
    • メソッド同様、静的(static)フィールドも設定可能
  • クラスメソッドに静的メソッドを設定可能になった
  • privateなフィールド/メソッドを設定可能になった
    • 当然、クラス外からはアクセス不可
    • これでやっと現場レベルのルールで設定する必要がなくなる
class Sample {
    x = 1;  
    // 変数名の前に#をつけると、privateなフィールド/メソッドになる。
    #y = 2;
    
    // staticをつけると、静的フィールド/メソッドになる。
    static sx = 10;
    static #sy = 20;
    
    constructor() {}
  
    // getter  
    // ちなみにgetter/setterもprivateに設定可能。  
    // ただ、利用機会は少ないかも...
    getY() {
        return this.#y;
    }
    
    // privateフィールド&メソッドは、クラス内からのみ参照可能
    getprivateMethodValue() {
        return this.#privateMethod();
    }
    
    // static/privateは、もちろんメソッドにも設定可能
    #privateMethod() {
        return 'I am private method';
    }
    
    static staticPublicFunc(){
        return this.#staticPrivateFunc();
    };
    
    static #staticPrivateFunc(){
        return this.#sy;
    }
}

  
const cls = new Sample();
console.log(cls.x);  // 1
console.log(cls.#y);  // エラー
console.log(cls.getY());  // 2
  
console.log(Sample.sx);  // 10
Console.log(Sample.#sy);  // エラー
  
console.log(cls.#privateMethod());  // エラー
console.log(cls.getprivateMethodValue());  // I am private method
console.log(Sample.#staticPrivateFunc());  // エラー
console.log(Sample.staticPublicFunc());  // 20

なおVSCodeでは、プライベートフィールド/メソッドに外部からアクセスしようとしたところ、ちゃんと怒られました。

Ergonomic brand checks for private fields

「人間工学に基づくプライベートフィールドのブランドチェック」ってなんのこっちゃ?という感じですが、オブジェクトのinプロパティを使って、プライベートフィールド&メソッドを持っているかをクラスレベルで調べられる機能のことらしいです。

これもソースを見た方が早いので、ソースをどうぞ。

class Sample {
    #y = 2;
    constructor(y = null) {
        if(y ?? null) this.#y = y;
    };

    static isSample(obj){  
        // #y in objで、obj内のプライベートフィールド#yを評価する。  
        // なお#yは正確にはthis.#yなので「Sampleクラスの#yフィールドを
        // 持っているかどうか」を判定する。
        // ここではprivateフィールドで行っていますが、privateメソッド及び  
        // 両方を組み合わせることも可能です。  
        return #y in obj;
    }
}

class Sample2 {
    #y = 2;
    constructor() {}
}  
  
// true(クラスが同じなので)
console.log(Sample.isSampleByField(new Sample()));  
  
// false(クラスが違うので)
console.log(Sample.isSampleByField(new Sample2()));  
  
// true(クラスが同じなので。#yの値そのものは問わない)
console.log(Sample.isSampleByField(new Sample(10))); 

感想

今回の感想としては、

  • クラス以外の機能
    • あればあるで便利な機能が増えた
  • クラス関連の機能
    • 基本的な機能が多言語に追いついた

という感じです。

ただtop-level awaitや.at()は地味ですが個人的にはうれしい機能だなと思いました。

またクラスに関しては、やっと普通に使えるようになったなあ...という感じです。(protectedとかあるともっといいんですけどね)

それでは、今回はこの辺で

【Next.js】ブラウザ側で環境変数が参照できない場合の対処法

本題(今回は前置きは省略)

Next.jsで環境変数(.env.*ファイルやコンテナに設定した)をブラウザ側(クライアント側、端的にいえばReactソース上)で参照しようとしても、値が参照できない

TL;DR

内容

Next.jsで、環境変数をブラウザ側(≒クライアント側)で利用しようと思っても、そのままでは利用できません。

例えば下記ソースをブラウザで表示しても、process.env.TEST_ENV_VALUEの部分がundefinedになっているのが分かります。(stateの部分は無視してください)

#.envで環境変数を設定しても...
TEST_ENV_VALUE=hogehoge
export const ComponentDruaga: VFC<ComponentDruagaProps> = ({treasures}) => {
    const {state, changeState}: UseMyStateType = useMyState();

    return (
      <div css={myCss}>
        <div>{`stateは${state}です。`}</div>  
         // ここのprocess.env.TEST_ENV_VALUEがundefinedになっている
        <div>{`TEST_ENV_VALUEは${process.env.TEST_ENV_VALUE}です。`}</div>
        <button onClick={changeState}>stateを変える</button>
      </div>
    )
}

原因

原因ですが、Next.jsではデフォルトでブラウザ側には環境変数が公開されないからです。

ちなみに、これはれっきとしたNext.jsの仕様であり、公式ドキュメントにも明記されています。(SSRだからということもあるのかな?)

https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser

ただ、ブラウザ側でも環境変数の値を参照させたいケースは出てくると思うので、対策をする必要があります。

対策

で、対策なのですが、具体的には下記2つの方法があります。(先程の公式ドキュメント内にも記載されています)

環境変数名のプレフィックスに「NEXT_PUBLIC_」をつける

環境変数名のプレフィックスに「NEXT_PUBLIC_」をつけることで、その環境変数はブラウザ側にも公開されますので、ブラウザ側で参照することができます。

サーバーで実行される関数から環境変数を渡す

デフォルトで環境変数はサーバー側にのみ公開されるので(server-only secrets safe)、サーバー側で実行される関数から環境変数の値をpropsなどで渡してあげれば、値そのものはブラウザ側でも参照することができます。

なお「サーバーで実行される関数」には以下のものがあります。(ただし「APIルート用の関数」はAPIの呼び出しに使うもの(=APIを呼び出さないと使わない)ので、今回の目的には不向きだと思います。

  • getServerSideProps関数
  • getStaticProps関数
  • APIルート用の関数(pages/api以下)
#.envで環境変数を設定して
TEST_ENV_VALUE=hogehoge
NEXT_PUBLIC_TEST_ENV_VALUE=fugafuga
// 例えば、getServerSidePropsでこのように設定する
export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext) => {
  
    return {
      props: {
        // ここで環境変数の値を設定したキーを返す
        testEnvValue: process.env.TEST_ENV_VALUE
      }
    }
}
  
// Propsには{testEnvValue: string}が設定されているものとする。
const Druaga: VFC<Props> = ({testEnvValue}) => {
    return (
      <>
        <ComponentDruaga testEnvValue={testEnvValue}/>
      </>
    )
}
  
export default Druaga
  
  
export const ComponentDruaga: VFC<Props> = ({testEnvValue}) => {
  
  const {state, changeState}: UseMyStateType = useMyState();
  
  // 下記のように環境変数の参照を変える。  
  //  (環境変数以外の部分は、無視してください)
    return (
     <div css={myCss}>
        <div>{`stateは${state}です。`}</div>
        <div>{`TEST_ENV_VALUEは${testEnvValue}です。`}</div>
        <div>{`NEXT_PUBLIC_TEST_ENV_VALUEは${process.env.NEXT_PUBLIC_TEST_ENV_VALUE}です。`}</div>
        <button onClick={changeState}>stateを変える</button>
      </div>
    )
}

上記のように変更すると、ちゃんと環境変数の値がブラウザ側でも参照できているのが分かります。

締めの言葉

ちょっと簡素ですが、今回は以上です。