3. 콜백을 사용한 비동기 제어 흐름 패턴
깃헙주소
https://github.com/PacktPublishing/Node.js_Design_Patterns_Second_Edition_Code.git
Node.js는 비동기, 연속전달방식
을 일반적으로 사용하는 플랫폼.
일반적인 문제, 실수중 하나 콜백지옥
-> 가독성 결여, 실수 발생
제어 흐름 라이브러리
를 사용하여 콜백패턴을 단순화하고 깔끔하게 만들기 목표
3.1 비동기 프로그래밍의 어려움
클로저와 인플레이스정의(in place definitions)
의 활용이 관건
KISS(Keep in simple, stupid)
간단함을 유지해 멍청아ㅋㅋ
클로저 -> 비동기보다는 규칙문제가 더중요한 경우
3.1.1 간단한 웹스파이더 만들기
간단 예제
npm 라이브러리 request, mkdirp
사용
// 깃헙 : https://github.com/PacktPublishing/Node.js_Design_Patterns_Second_Edition_Code/blob/master/Chapter03/01_web_spider/package.json
{
"name": "crawler",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"mkdirp": "^0.5.1",
"request": "^2.67.0",
"slug": "^0.9.1"
}
}
// index.js
const request = require('request');
const fs = require('fs');
const mkdirp = require('mkdirp');
const path = require('path');
const utilities = require('./utilities');
function spider(url, callback) {
const filename = utilities.urlToFilename(url);
fs.exists(filename, exists => { //[1] 콜백 : 파일 생성여부 판단.
if(!exists) {
console.log(`Downloading ${url}`);
request(url, (err, response, body) => { //[2] 콜백 : 파일 없는 경우 -> 다운로드
if(err) {
callback(err);
} else {
mkdirp(path.dirname(filename), err => { //[3] 콜백 : 파일저장 디렉토리 확인
if(err) {
callback(err);
} else {
fs.writeFile(filename, body, err => { //[4] 콜백 : 파일쓰기
if(err) {
callback(err);
} else {
callback(null, filename, true);
}
});
}
});
}
});
} else {
callback(null, filename, false);
}
});
}
// 컨맨드 라인 3번째에서 URL 읽음
spider(process.argv[2], (err, filename, downloaded) => {
if(err) {
console.log(err);
} else if(downloaded){
console.log(`Completed the download of "${filename}"`);
} else {
console.log(`"${filename}" was already downloaded`);
}
});
// utitlities.js
"use strict";
const urlParse = require('url').parse;
const slug = require('slug');
const path = require('path');
module.exports.urlToFilename = function urlToFilename(url) {
const parsedUrl = urlParse(url);
const urlPath = parsedUrl.path.split('/')
.filter(function(component) {
return component !== '';
})
.map(function(component) {
return slug(component, { remove: null });
})
.join('/');
let filename = path.join(parsedUrl.hostname, urlPath);
if(!path.extname(filename).match(/htm/)) {
filename += '.html';
}
return filename;
};
콜백 지옥, 죽음의 피라미드
-> 가독성이 떨어짐
비동기 CPS(직접전달방식), 클로저 잘못 사용? -> 큰 재앙
클로저가 참조하는 컨텍스트가 가비지 수집시 유지된다는 사실... -> 잘못사용하면 메모리 누수
3.2 일반 JavaScript의 사용
비동기 코드 작성시 또다른 주의 사항
특정 패턴과 기법을 사용해야하는 상황? -> 일반 JS를 사용할 때 특히
음... ㅇㅋ 일단 패스
여기서는 일반 JS를 사용하여 일반적인 제어흐름 패턴 구현
3.2.1 콜백 규치
콜백을 정의할때, 함부로 클로저를 사용하지 않는 것
콜백 지옥 문제 해결 -> 간단한 일반 상식이면 충분.
중첩 수준을 낮게 유지하는 팁
가능한 빨리 종료
. if문 수 줄이기. ex return, break, continue 문 활용
콜백을 위해 명명된 함수(익명말고)를 생성하여, 클로저 바깥에 배치하며, 중간 결과를 인자로 전달. 명명함수 -> 스택추적에서 잘보임
코드 모듈화
. 코드를 작고 재사용 가능하게 함수 구현
3.2.2 콜백 규칙 적용
if~else문 제거 -> 오류 검사 패턴 재구성 (상태패턴에서 활용 되기도 함)
if(err){
callback(err);
} else {
// 정상 : working
}
// 변경 후
if(err){
return callback(err); // return을 통해 즉시 종료.
}
// 정상 : working
// spider에서 다음 부분을 함수로 빼서 모듈화
function saveFile(filename, contents, callback){
mkdirp(path.dirname(filename), err => {
if(err) {
return callback(err);
}
fs.writeFile(filename, contents, callback);
});
}
function download(url, filename, callback){
console.log(`Downloading ${url}`);
request(url, (err, response, body) => {
if(err){
return callback(err);
}
saveFile(filename, body, err => {
if(err){
return callback(err);
}
console.log(`Downloaded and saved : ${url}`);
callback(null, body);
});
});
}
// index.js spider 부분 (리팩토링)
function spider(url, callback) {
const filename = utilities.urlToFilename(url);
fs.exists(filename, exists => { //[1] 콜백 : 파일 생성여부 판단.
if (exists) {
return callback(null, filename, false);
}
download(url, filename, err => {
if (err) {
return callback(err);
}
callback(null, filename, true);
});
});
}
3.2.3 순차실행
순서가 중요한 경우 순서에 따라 IN->OUT -> IN->OUT 으로 결과값이 입력값으로 가는 파이프형태
순차실행 적용 : 직접방식의 블로킹 API에서는 간단, 비동기 CRS 사용하여 구현 -> 콜백지옥의 원인
// 책임사슬 패턴과 유사,
// 비동기 코드를 처리하는데 꼭 클로저가 필요 없을음 보여줌..
function asyncOperation(callback) {
process.nextTick(callback);
}
function task1(callback) {
asyncOperation(() => {
task2(callback);
});
}
function task2(callback) {
asyncOperation(() => {
task3(callback);
});
}
function task3(callback) {
asyncOperation(() => {
callback();
});
}
task1(() => {
console.log('task 1,2 and 3 executed');
});
순차반복
비동기적으로 배열을 반복 실행하게끔 구현되어 있음.
```javascript
// index.js
"use strict";
const request = require('request'); const fs = require('fs'); const mkdirp = require('mkdirp'); const path = require('path'); const utilities = require('./utilities3');
function spiderLinks(currentUrl, body, nesting, callback) { if(nesting === 0) { return process.nextTick(callback); }
let links = utilities.getPageLinks(currentUrl, body); //[1] 내부 링크 다가져옴
function iterate(index) { //[2] 반복 패턴, 페이지 링크 수와 인덱스가 같아질때까지 반복 if(index === links.length) { return callback(); }
spider(links[index], nesting - 1, function(err) { //[3] : 중첩레벨을 줄여 반복 실행
if(err) {
return callback(err);
}
iterate(index + 1); // 반복적인 재귀 실행 구조
});
} iterate(0); //[4] 최초 0부터 시작, 재귀시작 }
function saveFile(filename, contents, callback) { mkdirp(path.dirname(filename), err => { if(err) { return callback(err); } fs.writeFile(filename, contents, callback); }); }
function download(url, filename, callback) { console.log(Downloading ${url}
); request(url, (err, response, body) => { if(err) { return callback(err); } saveFile(filename, body, err => { if(err) { return callback(err); } console.log(Downloaded and saved: ${url}
); callback(null, body); }); }); }
function spider(url, nesting, callback) { const filename = utilities.urlToFilename(url); fs.readFile(filename, 'utf8', function(err, body) { if(err) { if(err.code !== 'ENOENT') { return callback(err); }
return download(url, filename, function(err, body) {
if(err) {
return callback(err);
}
spiderLinks(url, body, nesting, callback);
});
}
spiderLinks(url, body, nesting, callback);
}); }
spider(process.argv[2], 1, (err) => { if(err) { console.log(err); process.exit(); } else { console.log('Download complete'); } });
// utilities.js "use strict";
const urlParse = require('url').parse; const urlResolve = require('url').resolve; const slug = require('slug'); const path = require('path'); const cheerio = require('cheerio');
module.exports.urlToFilename = function urlToFilename(url) { const parsedUrl = urlParse(url); const urlPath = parsedUrl.path.split('/') .filter(function(component) { return component !== ''; }) .map(function(component) { return slug(component); }) .join('/'); let filename = path.join(parsedUrl.hostname, urlPath); if(!path.extname(filename).match(/htm/)) { filename += '.html'; } return filename; };
module.exports.getLinkUrl = function getLinkUrl(currentUrl, element) { const link = urlResolve(currentUrl, element.attribs.href || ""); const parsedLink = urlParse(link); const currentParsedUrl = urlParse(currentUrl); if(parsedLink.hostname !== currentParsedUrl.hostname || !parsedLink.pathname) { return null; } return link; };
module.exports.getPageLinks = function getPageLinks(currentUrl, body) { return [].slice.call(cheerio.load(body)('a')) .map(function(element) { return module.exports.getLinkUrl(currentUrl, element); }) .filter(function(element) { return !!element; }); };
- iterate 일반화
`reduce`연산 알고리즘의 기반베이스
```javascript
function iterate(index){
if(index === tasks.length){
return finish();
}
const task = tasks[index];
task(function(){
iterate(index + 1);
});
}
function finish(){
// 반복 작업이 완료된 후 처리
}
iterate(0);
3.2.4 병렬실행
단일스레드 인데 병렬 가능?
-> 논블로킹 때문에 그렇게 보이는 것 -> 논블로킹 위에서 실행되고 있기 때문 가능 -> 정확히는 논블로킹 API위에서 실행되고, 이벤트 루프에 의해 인터리브 된다는 것을 의미
동기(블로킹)작업 -> 비동기로 처리, setTimeout(), setImmediate()로 지연 시키지 않는 한 동시에 실행 안됨 (9장에서 다룸)
웹스파이더 3버전 병렬성을 띄어 성능을 향상 시킴
"use strict";
const request = require('request');
const fs = require('fs');
const mkdirp = require('mkdirp');
const path = require('path');
const utilities = require('./utilities3');
function spiderLinks(currentUrl, body, nesting, callback) {
if(nesting === 0) {
return process.nextTick(callback);
}
const links = utilities.getPageLinks(currentUrl, body);
if(links.length === 0) {
return process.nextTick(callback);
}
let completed = 0, hasErrors = false;
function done(err) { // 단순 증가를 위한 헬퍼 함수
if(err) {
hasErrors = true;
return callback(err);
}
if(++completed === links.length && !hasErrors) { // 전위증가로 카운트 증가시킴
return callback(); // 최종 콜백 실행 -> 작업 완료
}
}
// 실행 루프
links.forEach(function(link) {
spider(link, nesting - 1, done); // 실행하면서 카운트를 증가시켜서 확인 어케보면 done에 의해 completed는 클로저
});
}
function saveFile(filename, contents, callback) {
mkdirp(path.dirname(filename), err => {
if(err) {
return callback(err);
}
fs.writeFile(filename, contents, callback);
});
}
function download(url, filename, callback) {
console.log(`Downloading ${url}`);
request(url, (err, response, body) => {
if(err) {
return callback(err);
}
saveFile(filename, body, err => {
if(err) {
return callback(err);
}
console.log(`Downloaded and saved: ${url}`);
callback(null, body);
});
});
}
let spidering = new Map();
function spider(url, nesting, callback) {
if(spidering.has(url)) {
return process.nextTick(callback);
}
spidering.set(url, true);
const filename = utilities.urlToFilename(url);
fs.readFile(filename, 'utf8', function(err, body) {
if(err) {
if(err.code !== 'ENOENT') {
return callback(err);
}
return download(url, filename, function(err, body) {
if(err) {
return callback(err);
}
spiderLinks(url, body, nesting, callback);
});
}
spiderLinks(url, body, nesting, callback);
});
}
spider(process.argv[2], 2, (err) => {
if(err) {
console.log(err);
process.exit();
} else {
console.log('Download complete');
}
});
패턴 일반화
```javascript
const tasks = [];
let completed = 0;
tasks.forEach(task => {
if(++completed === tasks.length){
finish();
}
})
function finish() { // work }
경쟁조건... 이 부분은 특이해서 학습이 더 필요해보임
### 3.2.5 제한된 병렬 실행
너무 많은 요청에 의한 병렬실행은 리소스 부족현상을 발생시킴.
따라서, 동시실행 작업수를 제한하는 것이 좋음.
```javascript
// 큐
module.exports = class TaskQueue {
constructor (concurrency) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
pushTask (task) {
this.queue.push(task);
this.next();
}
next() {
while (this.running < this.concurrency && this.queue.length) {
const task = this.queue.shift();
task (() => {
this.running--;
this.next();
});
this.running++;
}
}
};
// index.js
const request = require('request');
const fs = require('fs');
const mkdirp = require('mkdirp');
const path = require('path');
const utilities = require('./utilities5');
const TaskQueue = require('./taskQueue'); // 큐사용
let downloadQueue = new TaskQueue(2); // 2개까지로 설정
function spiderLinks(currentUrl, body, nesting, callback) {
if(nesting === 0) {
return process.nextTick(callback);
}
const links = utilities.getPageLinks(currentUrl, body);
if(links.length === 0) {
return process.nextTick(callback);
}
let completed = 0, hasErrors = false;
links.forEach(link => {
downloadQueue.pushTask(done => { // 이 부분 Task 등록
spider(link, nesting - 1, err => {
if(err) {
hasErrors = true;
return callback(err);
}
if(++completed === links.length && !hasErrors) {
callback();
}
done();
});
});
});
}
function saveFile(filename, contents, callback) {
mkdirp(path.dirname(filename), err => {
if(err) {
return callback(err);
}
fs.writeFile(filename, contents, callback);
});
}
function download(url, filename, callback) {
console.log(`Downloading ${url}`);
request(url, (err, response, body) => {
if(err) {
return callback(err);
}
saveFile(filename, body, err => {
if(err) {
return callback(err);
}
console.log(`Downloaded and saved: ${url}`);
callback(null, body);
});
});
}
let spidering = new Map();
function spider(url, nesting, callback) {
if(spidering.has(url)) {
return process.nextTick(callback);
}
spidering.set(url, true);
const filename = utilities.urlToFilename(url);
fs.readFile(filename, 'utf8', function(err, body) {
if(err) {
if(err.code !== 'ENOENT') {
return callback(err);
}
return download(url, filename, function(err, body) {
if(err) {
return callback(err);
}
spiderLinks(url, body, nesting, callback);
});
}
spiderLinks(url, body, nesting, callback);
});
}
spider(process.argv[2], 1, (err) => {
if(err) {
console.log(err);
process.exit();
} else {
console.log('Download complete');
}
});
3.3 Async 라이브러리