くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログです

Node.js18を20にアップデートして、jestの実行速度を3倍にした

こんにちは!バックエンドエンジニアのハラノです。
くらしのマーケットのシステムの中には、Node.js(NestJS)を使用したマイクロサービスが存在しており、今回 Node.js のバージョンアップを行いました。
バージョンアップの方針及び、実際にアップデートを行う際に出てきた問題とその対策をご紹介します。

バージョンアップの方針

まずは、Active LTS になった Node.js を 20 に追従し、負債化しないようにすることが目的でした。
また、TypeScript もバージョンが少し古くなっていたため、ミニマムに進めるためにも当初は Node.js と TypeScript のみをバージョンアップしようと考えていました。
しかし、Node.js と TypeScript のバージョンアップを行ったところ、NestJS のバージョンが古く互換性がないようで、コンパイルが通らなくなってしまい NestJS のバージョンアップも必要になってしまいました。
この時点で、当初考えていた規模より大きくなったため、チームメンバーと話し合い、痛みを伴ってでも主に使用しているライブラリのバージョンアップを行うことで負債を残さないようにする、ということを決めました。
そのため、今回は Node.js 自体のバージョンアップに加え、TypeScript、NestJS、TypeORM などの主に使用しているライブラリのバージョンアップも行うことにしました。

バージョンアップの結果

最終的には主に以下をバージョンアップすることにしました。
また、この他にも devDependencies なライブラリ(Prettier, ESLint) も現時点の最新バージョンに更新しています。

ライブラリ名 バージョンアップ前 バージョンアップ後
Node.js v18 v20
NestJS v6 v10
TypeScript v3.9 v5.3
TypeORM v0.2 v0.3
Jest, ts-jest v26 v29, ts-jest は@swc/jest に置き換えました

以下では、バージョンアップなどの対応を行うにあたり発生した問題などを共有いたします。

各種対応において、発生した問題と対応

TypeScript のバージョンアップ

TypeScript のバージョンアップを行ったところ以下のような部分で、コンパイルエラーが発生するようになりました。

try {
    throw new Error("test");
} catch (e) {
    // ここでエラーが発生する
    console.log(e.message);
}

原因は、TypeScript4.4 で追加されたオプションであるuseUnknownInCatchVariablesです。ドキュメント
このオプションは、catch 節での変数の型を unknown にするかどうかを指定するもので、strict を true にしている場合デフォルトで true になります。
e は本来 unknown 型になるのが適切ですが、e が any に推論されてる前提で実装されており、e.messageを logging するなどの処理が大量にあり、今回は全てを変更する必要はないと判断したため、以下のように tsconfig.json で false に設定することで回避しました。

{
+   "useUnknownInCatchVariables": true
}

NestJS のバージョンアップ

@nestjs/common から HttpService, HttpModule が削除された

@nestjs/commonからHttpService,HttpModuleが削除されました。
@nestjs/axiosに移動しており、interface の変更はなかったため 以下のように import を変更することで解決できました。

-import { HttpModule } from '@nestjs/common';
-import { HttpService } from '@nestjs/common';
+import { HttpModule } from '@nestjs/axios';
+import { HttpService } from '@nestjs/axios';

Inject にInject(TestRepository.name)のように、クラス名を渡している部分について、依存関係の解決が行えなくなっていた

依存の解決について、厳密に行うようになっていたようで、@Inject(TestRepository.name)のような記述をしている部分で、依存関係の解決が行えなくなっていました。
以下のように、クラスを渡すように変更することで解決しました。

@Injectable()
export class TestLogic {
-   constructor(@Inject(TestRepository.name) private testRepository: ITestRepository) {}
+   constructor(@Inject(TestRepository) private testRepository: ITestRepository) {}
}

RxJS のtoPromiseが Deprecated になった

NestJS は非同期処理に RxJS を利用していますが、RxJS のtoPromiseが Deprecated になっていました。
現在の使い方であれば、lastValueFromを使って解決できることが、ドキュメントに記載されていたため、以下のようにlastValueFromを使うように変更することで解決しました。

As a replacement to the deprecated toPromise() method, you should use one of the two built in static conversion functions firstValueFrom or lastValueFrom. ※ドキュメントより引用

class TestApiClass {
    @Inject() private httpService!: HttpService;

    async post(payload: any) {
        const url = ''; // 省略
-       return this.httpService.post(url, payload).toPromise();
+       return lastValueFrom(this.httpService.post(url, payload));
    }
}

TypeORM のバージョンアップ

Connection が Deprecated になった

接続方法がConnectionを使う方法から、DataSourceを使う方法に標準が変更されました。
Connectionで設定している内容をDataSourceで設定するように変更することで、対応しました。
また、作成した Connection を使用して Repostiory に Inject するようにしていたため、Repository 全体で DataSource を使うように変更したうえで、型も変更しました。

@Injectable()
export class TestRepository {
-   constructor(@Inject(ProdDBConnection) private connection: Connection) {}
+   constructor(@Inject(ProdDataSource) private dataSource: DataSource) {}
}

ormconfig のサポートが無くなった

TypeORM 0.3 から ormconfig のサポートが無くなりました。

現在は非推奨になっただけなため、影響はありませんが 0.4.0 で削除されるため、対応を行わない場合 cli から呼び出しが行えなくなります。

そのため、前述した DataSource を使用するように変更したうえで、cli から呼び出す際は ormconfig で設定していた情報をオプションとしてつけることで対応しました。

-npm run typeorm migration:create -- -n
+npm run typeorm migration:generate -- -d ${マイグレーションを乗せるdirectory} -n

グレイスフルシャットダウンの処理が正常に完了しない

npm のバージョンアップを行った際に、グレイスフルシャットダウンの処理が正常に完了しない問題が発生しました。

今回対応したマイクロサービスは Http Request の他 Rabbit MQ を subscribe して、非同期処理を行う Worker も動作しており ECS にデプロイされていました。
Worker について、デプロイ時古いタスクを停止するために、 SIGTERM が発行される -> Rabbit MQ から新しい Event を取得しないようにする -> 現在実行している処理がすべて完了したら Rabbit MQ の Connection を閉じてシャットダウンする、という処理を行っていました。

しかし、npm のバージョンアップによって、SIGTERM が発行されても、Node.js 側でそれを受け取ることができず、一定時間後に SIGKILL が発行されて、強制的に終了するようになってしまいました。

Issue を探してみると 10.3.0 で解決したとの記述がありましたが実際に試しても治りませんでした。

npm 経由で呼び出しをしていることで発生していたため、一時的にnodeコマンドで直接呼び出すように変更し、SIGTERM を受け取ることができるようになりました。

Deprecated なコードの利用を削除

ライブラリのバージョンアップをした際に、deprecated なコードの利用が多く残っていることに気づきました。
一つ一つ探して削除するのは大変なため、deprecated なものを使用しているとエラーにしてくれる ESLint 拡張を追加し、呼び出しを探して推奨される対応を行い、進めました。

ユニットテストの実行時間を三分の一にした

ユニットテスト実行の際は ts-jest でトランスパイルしてから実行する形になっています。
しかし、ts-jest はトランスパイルに時間がかかり、最初のテスト実行までに 3~40 秒程度かかっていました。

ライブラリをバージョンアップしてテストを実行してというサイクルを回していたのですが、非常に時間がかかっていたので調べたところ、ユニットテスト高速化のために、@swc/jest などの選択肢があることを知りました。
esbuild-jestを使う選択肢もありましたが、@swc/jestNestJS の公式ドキュメントにも記載されており、@swc/jestを選択しました。
@swc/jestを使用してユニットテストを行ったところ、複数箇所でエラーが発生するようになってしまったので、以下の部分を変更することでテストが正常に動作するようになり、最初のテスト実行までに数秒しかかからなくなり、全体としては三分の一程度で完了するようになりました。

  • TypeORM でリレーションをしているような型についてRelation<Profile>に変更する
    • swc のほうが厳格にチェックしており、Initialize される前に型定義で使用すると、エラーになってしまうためのようです。
  • tsconfig.json の esModuleInterop を true に変更する
    • swc では esModuleInterop を true にしたような動きをしており、tsconfig.json 側を変更しないと行けない状況でした
    • この変更に伴い、CommonJS modules で書かれたものの import 方法が変わりました

移行自体は行えましたが、@swc/jestでは正しくカバレッジを計測できない問題が発生しました。
CI でカバレッジが一定以下だと落ちるようにしており、対応の必要があります。
そのため、カバレッジ計測の場合は現在も ts-jest を使用しています。

まとめ

今回、Node.js のバージョンアップを行い、それに伴うライブラリのバージョンアップを行いました。
コアで使用しているライブラリのバージョンアップを行い、持続的に開発を行うために負債を残さないようにできたと考えています。
また、ユニットテストの実行時間を三分の一にすることができ、開発効率の向上にもつながりました。
一方、当初より規模が大きくなってしまった点は、改善の余地があると考えており、次に活かすためにも学んでいきたいと思いました。

最後にみんなのマーケットでは、くらしのマーケットのサービス開発を一緒に盛り上げてくれるエンジニアを募集しています! 詳しくは、こちらをご覧ください。