JavaScript初心者(他言語経験アリ)が非同期処理に苦しんだ話（Node.js製CLIツール作成実践記録）

## 対象読者
- JavaScript初心者
- JavaScriptの非同期処理について学んだけど実際には使ってみたことはない人
- Node.jsで対話式のCLIツールを作ろうとしている人

## 完成したツールを教えてほしい
という人は、[ここまで](#実際に使えるツールに改造する)ジャンプ。（ほぼ最後まで飛びます）
途中はこの記事を書く上での経緯や、日記的な記録を織り交ぜている。
背景も知りたいなあという方は続けてどうぞ。

## 前提とおことわり
まず、この記事ではJavaScriptの非同期処理のすべては解説していない。（は？）
JavaScript初心者が、初心者なりに、実践の中で学んだ内容を解説しているので、
非同期処理の詳細が知りたい方は専門書を読んでください。

あくまで **「非同期処理をより実践で使うにはどうするのか？」** という点にフォーカスした記事ですのでご了承を。

### この記事で書くこと
- CLIツール制作において非同期処理を扱い、組み込む流れ

### この記事で書かないこと
- JavaScript非同期処理についての詳しい解説
- slackアプリトークンの取得方法、設定方法
- シェルスクリプトのパスの通し方

## この記事を書くに至った経緯
JavaScriptの非同期処理について解説した記事は、
とにかく`setTimeout`を例にした記事が出てくる。

しかし、`setTimeout`は実際のプログラムでの使い所は限られていると感じる。
（私がJavaScript初心者だから使い所を知らないだけかも）
（`setTimeout`最高！という方はごめんなさい）

後述するCLIツール作成においては、
より実践的な非同期処理の扱いが必要となる。

`setTimeout`を用いない非同期処理についての
解説記事は少ないのではないかと思い、この記事を書いてみることにした。

## これを書いている人
- JavaScript初心者（業務未経験）
- C言語の業務経験が4年ちょい（Javaも少し）
- VBAやbashのツール開発とかもやってた
- 経験上、プログラムは基本的に上から下へ順番に動くと思っていた

## とある日のこと
私は組み込み系のエンジニアを4年近く経験した後、
フロントエンドエンジニアとしてジョブチェンジした。

研修も終わり、とある部署に配属された。
そこでは退勤時にslackでミニ日報を報告する文化があった。

実際にはこんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/5bb9d9d5-5efc-7456-42ba-4b6e408c4d1f.png)


これをみて、
![internet_gazou_ninsyou_select_man.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/007ae5e9-7e6d-4052-13a8-ca8fc154bd54.png)


と思う人は少なくないのではないだろうか。
じゃあ、やってみよう。

## 先人の知恵に感謝する
どうやって作ろうか考えていたところ、
やはり先人たちも同じ道を通ってきたのだろう。

先輩がこのような記事を書いていた。

https://qiita.com/k_kuroiwa/items/013a98f5ad3486e5837b

### これだ！！！！
私好みのCLIツールで、中身もNode.jsだ。

JavaScript初心者の自分には
ちょうど良い勉強になると思い、
さっそく記事を参考にツール制作に取り掛かった。

<font color="Red">**※実は先輩が書いた記事通りにコードを書いても、(いい感じに省略されているので)非同期処理以前の問題でうまくいかないのですが、それはまた別のお話ということで一部改変しながら進めます。**</font>

## 開発環境
- macOS Monterey バージョン12.5.1
- Node.js 16.19.0

## 試作ツールの概要
今回作る試作ツールで使うパッケージなどを簡単に紹介すると以下の通り。

1. 対話型で入力した内容を反映する
1. データ一覧から取得した選択肢から入力する
1. 入力が終了したらslackチャンネルに投稿する

各ステップの詳細は以下の通り
#### １. 対話型で入力した内容を反映する ： Node.js [readline](https://nodejs.org/api/readline.html)モジュールのquestionメソッドを使用

```javascript:readline.js
import readline from "readline";

// インターフェースを設定
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

function askFood () {
    rl.question('好きな食べ物はなんですか？', (answer) => {
        console.log(`なるほど、${answer}が好きなんですね。`)
        rl.close(); // closeメソッドでインターフェイスを閉じます。
    });
};

export {askFood};
```

#### ２. データ一覧から取得した選択肢から入力する : Node.js [enquirer](https://www.npmjs.com/package/enquirer)モジュールの[MultiSelect Prompt](https://www.npmjs.com/package/enquirer#multiselect-prompt)を使用
```javascript:multiSelect.js
import pkg from 'enquirer';
const {MultiSelect} = pkg;

const selectFood = (question) => {
  const prompt = new MultiSelect({
    message: question,
    choices: [
      { name: '寿司' },
      { name: '焼肉' },
      { name: 'カレー' },
      { name: '唐揚げ' },
      { name: 'ハンバーグ' },
      { name: 'ピザ' }
    ]
  });

  return prompt
    .run()
    .then((answer) => {
      return `${answer.join(",")}`;
    })
    .catch(console.error);
};

export {selectFood};


// ['寿司', 'カレー', 'ピザ']
```

#### ３.Node.js [@slack/bolt](https://www.npmjs.com/package/@slack/bolt)モジュールのclient.chat.postMessageメソッドを使用
※[Slack API](https://api.slack.com/)との連携の詳細やアプリの設置手順等は割愛するため、他の記事を参考にしていただきたい。

```javascript:postTextToSlack.js
import slack from "../bin/slack.json" assert { type: "json" };
import pkg from "@slack/bolt";
const { App } = pkg;

const app = new App({
  token: 'ワークスペースに設置したアプリのトークン',
  signingSecret: 'ワークスペースに設置したアプリのシークレットID',
});

const channelId = slack.postChannelId;

const postTextToSlack = (param) => {
  try {
    app.client.chat.postMessage({
      channel: channelId,
      text: param,
    });
  } catch (error) {
    console.error(error);
  }
  console.log('slackに投稿しました！');
};

export {postTextToSlack};
```



各パーツは完成。

それぞれ動かしてみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/01726eae-589c-4a11-a3fa-de21cd6ac1da.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/cc82e0b7-afdd-21bd-354f-caf06bc4b4a1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/f8144e9b-c272-5a4c-a142-f0072522f49e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/fbd7365c-2da4-7add-ef52-ca9f28d30907.png)

（へぇ〜jsonモジュールのインポートは実験的な機能なんだ・・・と思いつつ）
うまくいってそう。
次に上記3つのソースを呼び出すmain関数を追加。


```javascript:main.js
import {askFood}  from "./js/readline.js";
import {selectFood} from "./js/selectFood.js";
import {postTextToSlack} from "./js/postTextToSlack.js";

const main = () => {
  console.log("★ ★ ★ tool start ★ ★ ★");
  
  // 好きな食べ物を入力する
  const input = askFood("");
  
  // 好きな食べ物を選択肢から選ぶ
  const select = selectFood("好きな食べ物はなんですか？");
  
  // slackへ投稿
  const text = "```" + input + "\n" + select + "```";
  postTextToSlack(text);
  console.log("★ ★ ★ tool end ★ ★ ★");
};

main();
```

現時点のディレクトリ構造
```
├── bin
│     ├── report     ★「report」コマンドで起動するためのbashシェルスクリプト
│     └── slack.json ★slack APIとの連携情報（トークン、チャンネルIDなど）
├── js
│     ├── postTextToSlack.js
│     ├── readline.js
│     └── selectFood.js
└── main.js
```

## よしできた！動かしてみよう
※「report」というコマンドでCLIツールを起動するためにはパスを通す必要があるがここでは割愛する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/3cad2fd9-a7d7-b8ca-e781-53ca9c920ace.png)

<font color="Red">**・・・あれ？**</font>
何も入力、選択していないのにslackに投稿したという出力が・・・
なんかうまくいってないっぽいのはわかる。

そのままenterを押して進むと、slackには以下の投稿が。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/bb8fb14d-95cc-1aad-e595-f8dab6390179.png)

うーん・・・やっぱりダメみたい。
では何がダメなのか？

## どう動いて欲しいか
>経験上、プログラムは基本的に上から下へ順番に動くと思っていた

### これに尽きる。

つまり
##### main関数実行
↓
##### 1. `askFood`で入力待ち→入力値を変数に格納
##### 2. `selectFood`で入力待ち→選択値を変数に格納
##### 3. `postTextToSlack`でslackに投稿

この順番で動いて欲しいのだ。というかこう動くと思っていた。
もう少し詳しく書くと、
**「上から順番通り、入力待ちなどが発生したらその処理が完了してから次の処理が動く」**
と思っていた。

どうやら、入力待ちをすっ飛ばして次の処理が動いた結果、未入力状態のままslackに送信されているようだ。
実際に、それぞれの関数を個別で動かした時は、うまくいったのに、なぜ・・・


## 非同期処理との出会い

やっと本題です。お待たせしました。

入力待ちを飛ばして次の処理が動く現象は、
調べるとどうやらJavaScriptが親切に<font color="blue">**非同期処理**</font>をしてくれていたみたいだ。
※「非同期」とはJavaScript特有のものではなく、コンピューターサイエンス全般における概念である

<font color="Red">**同期処理**</font>
**「上から順番通りに動いて欲しい」「入力待ちなどは、飛ばさずに待って欲しい」** というような、
コードに記載されている処理を上から下に順番に実行すること。

<font color="blue">**非同期処理**</font>
**「入力待ちなどを飛ばして次の処理が動く」** というような、
コードに記載されている処理を上から下に順番に実行していく中で、ある処理が終了するのを待たずに別の処理を実行すること。

## 非同期処理の挙動

つまりどういうことかというと、先ほどのmain関数を例に見ると以下の通り。

>##### 1. `askFood`で入力待ち→入力値を変数に格納
　　→したいが、入力完了まで待たずに次の処理を実行
>##### 2. `selectFood`で入力待ち→選択値を変数に格納
　　→したいが、入力完了まで待たずに次の処理を実行
>##### 3. `postTextToSlack`でslackに投稿
　　→処理が終了したので飛ばした処理に戻る

ということが裏で行われていたのだ。

なるほど。
時間がかかるとわかっているから、処理を後回しにしてくれていたんですね。
親切だなぁ・・・（親切でない時もある）

## 今回は同期的に動かしたいんだ！
そう、今回は処理を飛ばさずに待ってほしい。
同期的に動かす方法は・・・ **ありました！**

- **コールバック地獄**
- **`Promise`**
- **`async` / `await`**

これらは、JavaScriptの非同期処理について調べると必ず出てくるワードなので、すでに知っている人も多いだろう。
冒頭にも記載したが、詳しい内容は専門書などを参考にしてほしい。

今回は、**`async` / `await`** を使って、ツールを完成させたいと思う。

## async / await
こちらの記事を参考にして`async` / `await`を入れ込んでいく。

https://qiita.com/soarflat/items/1a9613e023200bbebcb3

まず、1. `askFood`と2. `selectFood`を入力が完了するまで実行待ちさせたいので、`await`を追加する。
```javascript
  // 好きな食べ物を入力する
  const input = await askFood("");
  
  // 好きな食べ物を選択肢から選ぶ
  const select = await selectFood("好きな食べ物はなんですか？");
```

次に、`await`は`async`関数の中でしか使えないので、`main`関数を書き換える。
今回はアロー関数で書いているので、引数の()の前に追加する。

```javascript:main.js
import {askFood}  from "./js/readline.js";
import {selectFood} from "./js/selectFood.js";
import {postTextToSlack} from "./js/postTextToSlack.js";

const main = async () => {
  console.log("★ ★ ★ tool start ★ ★ ★");
  
  // 好きな食べ物を入力する
  const input = await askFood("");
  
  // 好きな食べ物を選択肢から選ぶ
  const select = await selectFood("好きな食べ物はなんですか？");
  
  // slackへ投稿
  const text = "```" + input + "\n" + select + "```";
  postTextToSlack(text);
  console.log("★ ★ ★ tool end ★ ★ ★");
};

main();
```

もちろんこれだけではダメで、呼び出し先の`askFood`と`selectFood`で`Promise`オブジェクトが返ってくるようにしないといけない。

ここで思い出したいのが、最初にslackに投稿されてしまったテキストだ。
`askFood`はundefinedであったが、`selectFood`の方は`Promise`オブジェクトが返ってきていることがわかる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/bb8fb14d-95cc-1aad-e595-f8dab6390179.png)

なので、`askFood`の方で`Promise`オブジェクトが返ってくるように書き換える。

```javascript:readline.js
import readline from "readline";

// インターフェースを設定
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

function askFood () {
  return new Promise(resolve => {
    rl.question('好きな食べ物はなんですか？', (answer) => {
        console.log(`なるほど、${answer}が好きなんですね。`)
        rl.close(); // closeメソッドでインターフェイスを閉じます。
        resolve(`${answer}が好き`);
    });
  });
};

export {askFood};
```

## 再度動かしてみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/2e6e6a51-43c6-c8a4-f8d6-c2e13eea2a7e.png)

**お！処理が止まった！**
良さそうなので続けて入力してみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/5dc96aaf-75f7-5207-0ea0-7fb09ef6d14f.png)

`await`をつけたことにより、`selectFood`のところで処理が止まっていることがわかる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/117a8855-a4cc-8804-1d84-5c1af4ed6398.png)

ログはよさそうだ！
では肝心のslack投稿内容は・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/4b18bd29-abb9-d3ea-eca1-b7bbe036bdfd.png)

うまくいっている！
これでやりたかったことは最低限できるようになった。

## 実際に使えるツールに改造する
私が作りたいのは食べ物を投稿するツールではない。
日報報告ツールを作りたいのだ。

と、いうことで改造してみたのがこちら↓

https://github.com/KMMThmurakami/finishReportTool_qiita

ざっくり解説すると以下の通り

```
.
├── README.md
├── bin
│   ├── report
│   └── slack.json
├── config
│   └── workList.js           ★タスクの選択肢をオブジェクトにまとめたファイル
├── js
│   ├── addOtherItems.js      ★その他共有事項を自由入力できるようにするための関数
│   ├── addOtherText.js       ★登録済みのタスク以外に自由入力できるようにするための関数
│   ├── calcWorkingHours.js   ★ツールを実行した時間から勤務時間を計算する関数群
│   ├── checkPosting.js       ★slackに投稿してよいかチェックする関数
│   ├── inputRestTime.js      ★休憩時間を入力する関数
│   ├── inputStartTime.js     ★勤務開始時間を入力する関数
│   ├── makePostingText.js    ★slackに投稿する文章を整形する関数
│   ├── postTextToSlack.js
│   └── selectWork.js         ★config/workList.jsに記載されているタスクを選択する関数
├── main.js
├── package-lock.json
└── package.json
```

実際に使ってみたのがこんな感じ
※一部は業務内容が含まれているので隠しています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/c08c5ba7-49db-8233-844e-aedf78c66428.png)

興味ある方はクローン等して自分でも作ってみてください。

## まとめ
途中かなり解説を省略してしまったが、無事ツールを完成させることができた。
まだまだ改善できそうなところはあるので、隙を見て直していきたい。
※タスク一覧をローカルを持っているのではなく、外部のプロジェクト管理ツールなどから取得する、など

また、JavaScriptの非同期処理という落とし穴に早いうちに気づけてよかったと思っている。
この記事が、JavaScript初心者のみなさんの非同期処理についての悩みの手助けになれば幸いだ。