제너레이터(Generator)
세미코루틴(semi-coroutines)이라고도 함. 함수와 비슷하지만(yield 문을 사용하여) 일시적으로 실행의 흐름을 중지시켰다가 다시 시작시킬 수 있다. 제너레이터는 반복자(Iterator)를 구현할 때 특히 유용함.
제너레이터의 기본
문법은 다음과 같다. function키워드에 * 연산자를 추가하여 선언.
{// 함수 본문}{'Hello World'; // 실행을 일시 중시 후 전달된 값을 호출자에게 반환console;}
next() 메소드 : 제너레이터의 실행을 시작/재시작하는데 사용되며, value, done 객체 반환
fruitGenerator.js
{'apple';'oragne';return 'watermelon';}const newFruitGenerator = ;console; // {value : 'apple', done : false}console; // {value : 'orange', done : false}console; // {value : 'watermelon', done : true}
반복자(Iterator) 로서의 제너레이터(Generator)
{forlet i = 0; i < arrlength; i++arri;}const iterator = ;let currentItem = iteratornext;while!currentItemdoneconsole; // apple// orange// watermeloncurrentItem = iteratornext;
값을 제너레이터로 전달하기
next()메소드의 인자로 값을 전달할 수 있다. 이 값이 제너레이터 내부의 yield문의 반환값으로 제공된다.
{const what = null;console;}const twoWay = ;twoWaynext; // 첫 yield문에 도달한 다음 일시중지 상태twoWaynext'world';// throw 메소드를 사용할 수 있음.const twoWay = ;twoWaynext;twoWay; // yield문에서 값이 반환되는 순간 예외 처리함.
제너레이터를 사용한 비동기 제어 흐름
const fs = ;const path = ;{{if errreturn generator;const results = slice;generatornextresultslength > 1 ? results : results0;}const generator = ;generatornext;};
각 비동기 함수에 전달된 callback의 역할은 해당 비동기 작업이 종료되자마자 제너레이터를 다시 시작시키는 것이다. yield를 지정하여 반환받을 수 있는 객체의 유형으로 Promise, thunk를 사용하는 두 가지 변형 기술이 있다. thunk는 콜백을 제외한 원래 함수의 모든 인자들을 받아 콜백 만을 인자로 가지는 함수를 리턴하는 함수.
{return {fs;}}
Node.js 스타일의 함수를 thunk로 변환하기 위한 라이브러리 thunkify
const thunkify = ;const mkdirp = ;const nextTick = ;
co를 사용한 제너레이터 기반의 제어 흐름
co가 지원하는 yield를 지정할 수 있는 객체 : Thunks, Promises, Arrays, Objects, Generators, Generator functions
co는 다음과 같은 패키지의 자체적인 생태계를 가지고 있다.
- 웹 프레임워크koa
- 특정 제어 흐름 패턴을 구현한 라이브러리
- co를 지원하기 위해 널리 사용되는 API를 랩핑한 라이브러리
순차 실행
const path = require('path'); | |
const utilities = require('./utilities'); | |
const thunkify = require('thunkify'); | |
const co = require('co'); | |
const request = thunkify(require('request')); // 코드를 thunkified | |
const fs = require('fs'); | |
const mkdirp = thunkify(require('mkdirp')); | |
const readFile = thunkify(fs.readFile); | |
const writeFile = thunkify(fs.writeFile); | |
const nextTick = thunkify(process.nextTick); | |
function* spiderLinks(currentUrl, body, nesting) { | |
if(nesting === 0) { | |
return nextTick(); | |
} | |
const links = utilities.getPageLinks(currentUrl, body); | |
for(let i = 0; i < links.length; i++) { | |
yield spider(links[i], nesting - 1); | |
} | |
} | |
function* download(url, filename) { | |
console.log('Downloading ' + url); | |
const response = yield request(url); | |
const body = response[1]; | |
yield mkdirp(path.dirname(filename)); | |
yield writeFile(filename, body); | |
console.log(`Downloaded and saved: ${url}`); | |
return body; | |
} | |
function* spider(url, nesting) { | |
const filename = utilities.urlToFilename(url); | |
let body; | |
try { | |
body = yield readFile(filename, 'utf8'); | |
} catch(err) { | |
if(err.code !== 'ENOENT') { | |
throw err; | |
} | |
body = yield download(url, filename); | |
//co가 yield를 지정가능 하고 다른 제너레이터를 지원하기에 사용 가능. | |
} | |
yield spiderLinks(url, body, nesting); | |
} | |
// entry point | |
// co는 yield문에 전달하는 모든 제너레이터(함수 or 객체)를 감싼다. | |
co(function* () { | |
try { | |
yield spider(process.argv[2], 1); | |
console.log('Download complete'); | |
} catch(err) { | |
console.log(err); | |
} | |
}); |
병렬 실행
function* spiderLinks(currentUrl, body, nesting) { | |
if(nesting === 0) { | |
return nextTick(); | |
} | |
//배열에서 일시 정지(yield)할 수 있는 co의 특징을 이용한 방법 | |
const links = utilities.getPageLinks(currentUrl, body); | |
const tasks = links.map(link => spider(link, nesting - 1)); // 호출을 병렬로 변환한 부분 | |
yield tasks; | |
// 위의 코드를 callback 사용하여 구현한 코드. returns a thunk | |
return callback => { | |
let completed = 0, hasErrors = false; | |
const links = utilities.getPageLinks(currentUrl, body); | |
if (links.length === 0) { | |
return process.nextTick(callback); | |
} | |
function done(err, result) { | |
if(err && !hasErrors) { | |
hasErrors = true; | |
return callback(err); | |
} | |
if(++completed === links.length && !hasErrors) { | |
callback(); | |
} | |
} | |
for(let i = 0; i < links.length; i++) { | |
co(spider(links[i], nesting - 1)).then(done); | |
// spider를 병렬로 실행. resolve되면 done함수 호출. | |
} | |
} | |
} |
제한된 병렬 실행
동시 다운로드 작업의 수에 제한을 둔다.
- co-limiter 사용.
- co-limiter 패턴 : 생산자 - 소비자 패턴(producer-consumer)을 기반으로 구현.
목표는 queue를 활용하여 우리가 설정하려는 동시 실행 수만큼의 고정된 수의 worker들을 공급하는 것.
const co = require('co'); class TaskQueue { constructor(concurrency) { this.concurrency = concurrency; this.running = 0; this.taskQueue = []; this.consumerQueue = []; this.spawnWorkers(concurrency); // worker 시작 } pushTask(task) { if (this.consumerQueue.length !== 0) { this.consumerQueue.shift()(null, task); // 대기중인 첫 번째 콜백을 호출함으로써 자례대로 worker의 차단을 해제한다. } else { this.taskQueue.push(task); // 모든 worker가 작업을 실행 중. } } spawnWorkers(concurrency) { const self = this; for(let i = 0; i < concurrency; i++) { // 즉시 실행되어 병렬 처리됨. co(function* () { while(true) { // 무한 loop에서 블록(yield)되어 큐에서 새로운 작업을 기다린다. const task = yield self.nextTask(); yield task; } }); } } nextTask() { // co 라이브러리를 통해 yieldable thunk를 반환한다. return callback => { if(this.taskQueue.length !== 0) { return callback(null, this.taskQueue.shift()); // 즉시 worker의 yield가 해제되어 작업을 수행할 수 있다. } this.consumerQueue.push(callback); // 큐에 작업이 없는 경우. } } } module.exports = TaskQueue;
다운로드 작업 동시성 제한
{//...return {//...{//...}links;}}
각 작업에서 다운로드가 완료된 직후에 done() 함수를 호출하므로 다운로드된 링크 수를 계산하여 모두 완료되었을 때 thunk의 콜백을 호출할 수 있다.
Babel을 사용한 async await
-
ECMA2017(ES8) 사양으로 소개 된 async/await
async 함수의 정의에 async와 await라는 두 가지 새로운 키워드를 언어에 도입함으로써 비동기 코드 작성을 위한 모델을 언어수준에서 크게 향상 시키는 것을 목표로 한다.
const request = ;{return {;};}{const html = await ;console;};console;
async/await는 셋트로 묶여다님. getPageHtml을 호출하기 전에 await 키워드를 사용하면 javascript인터프리터가 getPageHtml에서 반환한 Promise의 resolve를 기다리면서 다음 명령을 계속 진행하라는 것. 이렇게 하면 main함수는 프로그램의 나머지 부분의 실행을 차단하지 않고 비동기 코드가 완료될 때까지 내부적으로 일시 중지된다. ES8에서 사용할 수 있는 문법이기에 호환 가능한 코드로 변환해 주는 컴파일러 Babel을 이용해서 ES8을 지원하지 않는 환경에서도 사용할 수 있다.
npm install --save-dev babel-cli
async/await의 분석과 변환을 지원하기 위해 확장 기능을 설치해야 한다.
npm install --save-dev babel-plugin-syntax-async-functionsbabel-plugin-transform-async-to-generator
노드에서 실행시킬 때
node_modules/.bin/babel-none --plugins"syntax-async-functions, transform-async-to-generator" index.js
index.js의 소스코드를 변환하여 새로운 하위 호환성 코드는 메모리에 저장되어 Node.js runtime에서 즉시 실행된다.
비교
해결책 | 장점 | 단점 |
---|---|---|
일반 javascript | 1. 추가적인 라이브러리나 기술이 필요하지 않음 2. 최고의 성능을 제공함 3. 다른 라이브러리들과 최상의 호환성을 제공 4. 즉석에서 고급 알고리즘의 생성이 가능 |
1. 많은 코드와 비교적 복잡한 알고리즘이 필요할 수 있음 |
Async(라이브러리) | 1. 가장 일반적인 제어 흐름 패턴들을 단순화 2. 여전히 콜백 기반의 솔루션 3. 좋은 성능 |
1. 외부 종속성 2. 복잡한 제어 흐름에 충분하지 않을 수 있음 |
Promise | 1. 일반적인 제어 흐름의 패턴을 크게 단순화 2. 강력한 오류 처리 3. ES2015 사양의 일부 4. OnFulfilled 및 OnRejected의 지연 호출 보장 |
1. Promise화 콜백 기반의 API가 필요 2. 다소 낮은 성능 |
제너레이터 | 1. 논 블로킹 API를 블로킹과 유사하게 사용 2. 오류 처리 단순화 3. ES2015 사양의 일부 |
1. 보완적인 제어 흐름 라이브러리가 필요 2. 비순차적 흐름을 구현할 콜백 또는 Promise가 필요 3. thunk화 또는 Promise화가 필요 |
Async Await | 1. 논 블로킹 API를 블로킹과 유사하게 사용 2. 깨끗하고 직관적인 구문 |
1. JavaScript및 Node.js에서 기본적으로 사용할 수 없음 2. Babel 또는 transpiler 및 일부 설정들이 필요함 |