日本語カラムを自動的に英字カラムに変換するReactのWebアプリの実装説明

前回の記事 [日本語カラムを自動的に英字カラムに変換するReactのWebアプリを作ってみた](https://qiita.com/yasudakn/items/2e6a69a11845a9bcdc33) の実装について簡単に説明します。

### 何を使って実装しているか

コードをすべて公開してます。
https://github.com/yasudakn/column-name-auto-generator

ここでは、いくつかの設計のポイントについて簡単に説明します。

:::note info
設計のポイントで上げている以下の詳細は、コードを追っていただいたら把握出来ます。
Reactはまだ初学者であり、多々分かりにくいことがあるかと思います。
改善やバグなどはissueやプルリクはいただけるとありがたいです。
:::

#### フロントエンドとバックエンドの役割分担

* スキーマ定義のインタラクティブな画面操作をフロントエンド(React)
* データロードをバックエンド(Nodejs)

BigQueryのテーブル定義（スキーマ）はフロントエンドで自動変換や編集で決めた後に、データをローディングするのはバックエンドで行なっています。
フロントエンドにBigQueryへのデータロードの機能を持たせると、ユーザのGCPへのアクセス権をOAuthで認可をさせたりと手間を考慮して、実装しやすさから、バックエンドでサービスアカウントを持たせてBigQueryの操作する構成にしています。

結果的にフロントエンドとバックエンドに分離したことで、ロードする先のDBを変えたい場合に分担が明確で対応しやすくなるというメリットが出来た。:seedling:

トップにReact Appのコード、server以下にNode.jsのコードがまとまっています。
それぞれで、npm startなどして実行します。

#### [codic API](https://codic.jp/docs/api) :point_left:

日本語を英数字の変数に変換するAPIとして、codicというサービスがあり、ちょうど良い  も提供されていることで、利用しています。
この変換機能は他のクラウドサービスにある翻訳APIと組合せても可能かもしれません。

カラム名の変換には、[codic](https://codic.jp/)という変数のネーミングで日本語から英字の変数名を自動的に作ってくれるサービスを使っています。IDEなどエディタからの利用を想定して、Web APIが提供されています。
そこで提供されている [translate API](https://codic.jp/docs/api/engine/translate) を使っています。

先のほどの変換で一部出来ていない文字があります。実はcodicでカスタム辞書も利用できます。同じ文字が繰り返される場合は、そちらで登録させた方が効率が良いです。


#### React.js

codicで完全に自動変換するのは難しいことがわかっていたため、ユーザが微調整出来るようにすることを想定した画面を作ることにしました。
画面は、シンプルなReactの１つのコンポーネントで収まる範囲で作っています。

#### 非同期処理の並列化 Promise.all
1カラムづつcodic APIを呼び出しています。そこでは並列に非同期でリクエストを行って、帰ってきたレスポンスから画面に表示させてます。カラム順は元の順を保つようにインデックスでソートをかけてます。

```{javascript}
     Promise.all(
        Object.keys(originHeaders[0]).map((item, index) => {return {origin: item, index: index}}).filter(
          x => x.origin.trim().length > 0).map(x => translate(x.origin, x.index)));
        }
```

```{javascript}
  const translate = useMemo(() => {
    return (item: string, index: number) => {
    let error = item.length > requestMaxLength ?
      `max length ${requestMaxLength}` : undefined;
    request.text = item.slice(0, requestMaxLength);
    axios.post(urls.translate, request)
      .then(res => {
        let data = JSON.parse(JSON.stringify(res.data[0]))
        console.log(`${index} ${data.translated_text}`);
        setTranslatedHeaders(prevBuffer => 
          [...prevBuffer, { index: index, origin: item, translated_text: data.translated_text, error: error, dtype: 'string' }].sort((a, b) => a.index - b.index));
      }).catch(e => {
        console.error(e);
      });
    }
  },[urls.translate, request, requestMaxLength]);
```

#### 副作用フック [useEffect](https://ja.reactjs.org/docs/hooks-effect.html)

codic API呼び出しでレスポンスが帰ってきたら画面に表示させるところで、useEffectを使ってステートが変わったら表示するに反映するように

```{javascript}
  useEffect(() => { 
    if (originHeaders.length > 0 && 
        originHeaders.length > translatedHeaders.length) {
      outputEventUpdate(originHeaders);
      setOriginHeaders([]);
    }
  }, [outputEventUpdate, originHeaders, translatedHeaders.length]);
```

#### フロントエンドとバックエンドのデータの受け渡し

multipart/form-dataで、スキーマ情報(json)とデータファイル(csv)の複数ファイルをバックエンドへ送信している。

```{javascript}
    await axios({
      method: 'post',
      url: urls.createTable,
      data: createFormData(),
      headers: {'Content-Type': 'multipart/form-data'}
    })
      .then(res => {
        let data = JSON.parse(JSON.stringify(res.data));
        console.log(data);
        setCreateTableStatus(`success! 作成したテーブルは${data?.datasetId}:${data?.tableId}`);
      })
      .catch(e => {
        console.log(e);
        setCreateTableStatus(`fail! ${e}`);
      });
```

#### バックエンドのデータインポートは非同期処理
nodejs側で、データがある程度大きい場合を考慮して、非同期でインポートするようにしてます。

```{javascript}
        try{
            writeFileSync(dest_filepath, file, {flag: 'w', encoding: 'utf-8'});
            const table_info = await createTable(header_info.header);
            if(table_info.tableId){
                importToTable(
                    dest_filepath,
                    table_info.datasetId,
                    table_info.tableId,
                    table_info.fields);
            }
            res.send(JSON.stringify(table_info));
        }catch(e){
            console.error(e);
        }
```

#### 環境変数
実行時の環境が変わりそうな値は環境変数(dotenv)で使う。
主に次の値を環境変数にしてます。

* codic api
* gcp周り

Reactは特にインストール要らず、次でアクセス ` process.env.REACT_APP_CODIC_PROJECT_ID `

Node.jsはdotenvを入れて、

```{javascript}
import config from 'dotenv';

config.config(); // load .env
```

` process.env.PROJECT_ID ` でアクセス

### ハマったところ

最後に普段バックエンドで作業している人間から今回のフロントエンド開発の設計や実装で、少し苦労したことを共有します。

#### 入力フォームのフォーカスのズレ

onChangeCellでテーブルのキーを更新するとフォーカスがズレる。
ファイル毎に順序固定のインデックスをキーとして、割り当てることで対応。

#### 外部APIコールのCORS対応

nodejsのsetupProxy.jsと、http-proxy-middlewareを使って対応。
tsconfig.jsonで、es2015を使う。

```{javascript}
const { createProxyMiddleware } = require('http-proxy-middleware');

const codic_token = process.env.REACT_APP_CODIC_API_TOKEN;
const codic_headers  = {
    "Content-Type": "application/json;charset=utf-8",
    "Authorization": `Bearer ${codic_token}`
}
module.exports = function(app) {
    app.use(
      '/v1',
      createProxyMiddleware({
        target: process.env.REACT_APP_CODIC_API_URL,
        changeOrigin: true,
        secure: false,
        headers: codic_headers,
        logLevel: 'debug'
      })
    );
  };
```

#### 副作用フックの無限ループ

よくある副作用フックの動作で、ステートの変更条件をしっかり指定しないと無限ループに陥るというものです。
まだバグがありそうな気もします。。。

---
### まとめというか感想

Reactは結構取っつきやすい。
久しぶりにフロントエンド触りましたが特に技術の壁を感じることもなく動かせた。
シンプルな機能のみであるため、柔軟に様々な設計方針に対応できそうだなと。また、必要なライブラリは好きに選んで組み合わせられたところも良かった。:thumbsup:

非同期処理の並列化やステートの副作用フックなど、無駄に実装を凝ったところがある意味楽しめました。:laughing:
このような非同期処理で画面を動的に更新する機能は、次のシーンで使えそうだと思います。

* オンラインの推論APIなどを呼び出して、まとまったデータを評価・モニタリングする機能
* データを少し直しながら、非同期処理を回し、結果を順次反映させる
* 負荷テストツール