LHJ

I'm a FE developer.

14.4.1 1보 전진과 2보 후퇴?

21 May 2020 » js_lj

14.4.1 1보 전진과 2보 후퇴?

const fs = require('fs');

function nfcall(f, ...args) {
    return new Promise(function(resolve, reject) {
        f.call(null, ...args, function(err, ...args) {
            if(err) return reject(err);
            resolve(args.length < 2 ? args[0] : args);
        })
    })
}

function ptimeout(delay) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, delay)
    })
}

function grun(g) {
    const it = g();
    (function iterate(val) {
        const x = it.next(val);
        if (!x.done) {
            if (x.value instanceof Promise) {
                x.value.then(iterate).catch(err => it.throw(err));
            } else {
                // 세번째 이후 매개변수 : 타이머가 만료되고 func에 전달되는 추가적인 매개변수들입니다.
                setTimeout(iterate, 0, x.value);
            }
        }
    })();
}

function* theFuntureIsNow() {
    const dataA = yield nfcall(fs.readFile, 'a.txt');
    const dataB = yield nfcall(fs.readFile, 'b.txt');
    const dataC = yield nfcall(fs.readFile, 'c.txt');
    yield ptimeout(60*1000);
    yield nfcall(fs.writeFile, 'd.txt', dataA + dataB + dataC);
}

grun(theFuntureIsNow);

“그냥 세 개의 파일을 동시에 읽으면 더 효율적이지 않나?” 예리한 질문을 하는 독자도 있을 겁니다.
그 질문에 대한 답은 문제에 따라, 자바스크립트 엔진에 따라, 운영체제에 따라, 파일시스템에 따라 크게 다를 수 있습니다.
하지만 복잡한 부분은 잠시 미뤄두고, 세 파일을 읽는 순서는 상관이 없다는 것, 그리고 설령 세 파일을 동시에 읽었다 한들 과연 효율적일지는 의문스럽다는 점을 상기합시다.
theFutureIsNow 함수를 이런 식으로 만든 것은, 이 방법이 이해하기 쉽고 단순해 보였기 때문입니다.

Promise에는 all 메서드가 있습니다.
이 메서드는 배열로 받은 프로미스가 모두 완료될 때 완료되며, 가능하다면 비동기적 코드를 동시에 실행합니다.
theFutureIsNow 함수가 Promise.all을 사용하도록 수정하기만 하면 됩니다.

const fs = require('fs');

function nfcall(f, ...args) {
    return new Promise(function(resolve, reject) {
        f.call(null, ...args, function(err, ...args) {
            if(err) return reject(err);
            resolve(args.length < 2 ? args[0] : args);
        })
    })
}

function ptimeout(delay) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, delay)
    })
}

function grun(g) {
    const it = g();
    (function iterate(val) {
        const x = it.next(val);
        if (!x.done) {
            if (x.value instanceof Promise) {
                x.value.then(iterate).catch(err => it.throw(err));
            } else {
                // 세번째 이후 매개변수 : 타이머가 만료되고 func에 전달되는 추가적인 매개변수들입니다.
                setTimeout(iterate, 0, x.value);
            }
        }
    })();
}

function* theFuntureIsNow() {
    const data = yield Promise.all([
        nfcall(fs.readFile, 'a.txt'),
        nfcall(fs.readFile, 'b.txt'),
        nfcall(fs.readFile, 'c.txt'),
    ])
    yield ptimeout(60*1000);
    yield nfcall(fs.writeFile, 'd.txt', data[0] + data[1] + data[3]);
}

grun(theFuntureIsNow);

Promise.all이 반환하는 프로미스에는 매개변수로 주어진 각 프로미스의 완료 값이 배열에 들어있었던 순서대로 들어있습니다.
c.txt를 a.txt보다 먼저 읽더라도 data[0]에는 a.txt의 내용이, data[2]에는 c.txt의 내용이 들어 있습니다.

Promise.all은 편리한 도구이고 알아두면 좋지만, 이 섹션에서 가장 중요한 것은 Promise.all이 아닙니다.
이 섹션에서 가장 중요한 것은 프로그램에서 어떤 부분을 동시에 실행할 수 있고 어떤 부분은 동시에 실행할 수 없는지를 판단하는 것이어야 합니다.
이 예제에서는 파일을 읽는 것과 타임아웃이 동시에 실행되더라도 상관없습니다.
어떤 부분을 동시에 실행할 수 있고 어떤 부분은 동시에 실행할 수 없는지를 판단하는 것은 문제에 따라 다릅니다.
세파일을 읽은 다음에 60초를 기다리고 그 다음에 병합 결과를 파일에 저장하는 것이 중요하다면 그 답은 이미 예제에 들어있습니다.
반면 세 파일을 읽는 것과 무관하게 60초 이상이 흐른 다음 네 번째 파일에 결과를 저장하는 것이 중요하다면 타임아웃을 Promise.all로 옮기는 편이 좋을 겁니다.