【JavaScript】非同期処理を同期にしてコールバック地獄から抜け出す


# 概要
JavaScript(Node.js)でHTTPリクエストを送ったり、ファイルの読取を実装しようとすると、コールバックだらけになって、HTTPリクエストの後に行う処理が実装しづらかったりする。
その場合、async/awaitやPromiseを使って同期的にすることができる。基礎知識については、下記ページを参照。

[async/await 入門（JavaScript） - Qiita](https://qiita.com/soarflat/items/1a9613e023200bbebcb3)

以下、Webサイトからcsvファイルを取得し、パースする処理を例にする。

# コールバックで実装した場合
HTTPリクエストに`https`、csvのパースに`csv-parse`を使う。
どちらもコールバック関数を使用する設計になっているため、ひたすら関数の呼び出しの連鎖が続き、処理の全体が把握しづらくなる。

```javascript:コールバックを使用したサンプル
const https = require('https');
const parse = require('csv-parse');
const moment = require('moment');

(function() {
    getCsv();

    // ※ HTTPリクエストもcsvの処理も非同期なので、真っ先に出力されてしまうログ
    console.log('finish');
}());

function getCsv() {
    // csvファイル取得
    const url = 'https://www.mizuhobank.co.jp/market/csv/quote.csv';
    let csvdata = '';

    https.get(url, function(res) {
        res.on('data', function(data) {
            csvdata += data;
        });

        res.on('end', function() {
            // 次の処理を呼び出す
            parseCsv(csvdata);
        });

    });
}

function parseCsv(csvdata) {
    // csvファイルを配列へ変換する
    let keepdata = [];

    parse(csvdata, {
        skip_empty_lines : true
        ,from_line : 4
    })
    .on('readable', function() {
        let record = null;
        while (record = this.read()) {
            if (filterCsv(record)) {
                keepdata.push(record);
            }
        }
    })
    .on('end', function() {
        // 次の処理を呼び出す
        fillEmptyRecord(keepdata);
    });
}

function fillEmptyRecord(records) {
    // 休日など、空いている日付を翌日のレートで埋める
    let size = records.length;
    let index = size - 1;
    let newRecords = [];

    while (index > 0) {
        let date = moment(records[index][0], 'YYYY/M/D');
        let prevDate = moment(records[index - 1][0], 'YYYY/M/D');
        let diff = date.diff(prevDate, 'days');

        newRecords.splice(0, 0, records[index]);

        while (diff > 1) {
            let cloned = records[index].slice(0, records[index].length);
            date = date.subtract(1, 'days');
            cloned[0] = date.format('YYYY/M/D');

            newRecords.splice(0, 0, cloned);

            diff -= 1;
        }

        index -= 1;
    }

    // ここが処理の一番最後
    for (let record of records) {
        console.log(record);
    }
}
```

# Promiseを使用した場合
先ほどの例では分かりづらい…という問題を解消するために、async/await や Promise を使用する。
こうするとエントリポイントとなる関数に処理の流れを表すことができるため、流れが把握しやすくなるし、実装もしやすい。

```javascript:サンプル
const https = require('https');
const parse = require('csv-parse');
const moment = require('moment');

(async function() {
    const csvdata = await getCsv();
    const keepdata = await parseCsv(csvdata);
    fillEmptyRecord(keepdata);

    // ちゃんと最後に出力される
    console.log('finish');
}());

function getCsv() {
    // csvファイル取得
    const url = 'https://www.mizuhobank.co.jp/market/csv/quote.csv';
    let csvdata = '';

    return new Promise(function(resolve, reject) {
        https.get(url, function(res) {
            res.on('data', function(data) {
                csvdata += data;
            });
    
            // getが終わった後
            res.on('end', function() {
                resolve(csvdata);
            });

            res.on('error', function() {
                reject(null);
            });
        });
    });
}

function parseCsv(csvdata) {
    let keepdata = [];

    return new Promise(function(resolve, reject){
        parse(csvdata, {
            skip_empty_lines : true
            ,from_line : 4
        })
        .on('readable', function() {
            // 略
        })
        .on('end', function() {
            resolve(keepdata);
        });
    });
}

function fillEmptyRecord(records) {
    // 中略
    for (let record of records) {
        console.log(record);
    }
}
```