* 이 글은 Mario Casciaro, Luciano Mammino가 저서한 <Node.js 디자인 패턴 바이블> 서적을 참고한 게시글입니다.
서적 정보 : http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788931464283
Node.js 디자인 패턴 바이블 - 교보문고
검증된 패턴과 기술을 이용한 수준 높은 Node.js | 이 책은 이미 Node.js를 처음 접한 후 이제 생산성, 디자인 품질 및 확장성 측면에서 최대한 활용하고자 하는 개발자를 대상으로 합니다. 이 책은
www.kyobobook.co.kr
콜백의 디자인 패턴에 대해 공부하면서 정리해보았다.
콜백 지옥부터 병렬실행, TaskQueue 적용 정도까지 살펴볼까 한다.
0. Main code : Web Spider (v0)
이번 콜백 패턴과 여정을 함께할 코드를 작성해보았다.
주어진 url의 내용을 로컬 파일로 다운로드하는 간단한 웹 스파이더를 작성해보았다.
// spider.mjs
import fs from "fs";
import path from "path";
// 웹 스파이더에서 사용할 패키지
import superagent from "superagent";
// url을 file 프로토콜에 맞게 변경해서 반환하는 함수
const urlToFilename = (url) => {
const filename = url.split("/").pop();
return filename.split(".")[1] + "/" + filename;
};
// 웹 스파이더
export const spider = (url, callback) => {
const filename = urlToFilename(url);
fs.access(filename, (err) => {
// 만약 파일이 존재하지 않으면 다운로드한 적이 없으므로 다운로드를 수행한다.
if (err && err.code === "ENOENT") {
console.log(`Downloading ${url} into ${filename}`);
superagent.get(url).end((err, res) => {
if (err) {
callback(err);
} else {
// 도메인에 맞는 폴더 생성
fs.mkdir(path.dirname(filename), (err) => {
if (err) {
callback(err);
} else {
// 파일 생성
fs.writeFile(filename, res.text, (err) => {
if (err) {
callback(err);
} else {
callback(null, filename, true);
}
});
}
});
}
});
} else {
callback(null, filename, false);
}
});
};
우선 url은 www.google.com 같이 단순한 도메인만 받는 것을 가정하고 구현했다.
일단 벌써부터 보기가 힘들지만... 차근차근 해결해 나가는 것으로 하자.
그리고 이 spider함수를 사용할 간단한 cli도 구현했다.
import { spider } from "../spider.mjs";
spider(process.argv[2], (err, filename, downloaded) => {
if (err) {
console.error(err);
} else if (downloaded) {
console.log(`Completed the download of "${filename}"`);
} else {
console.log(`"${filename}" was already downloaded`);
}
});
커멘드는 다음과 같이 사용하면 된다.
node --experimental-modules spider-cli.mjs http://www.google.com
시작해보자.
1. 콜백 지옥(Callback Hell)과 해소
이젠 너무나도 친숙한 단어이고, 심지어 요즘은 콜백을 디자인 패턴으로 사용하는 경우가 그리 많은 것 같지는 않다.
많은 클로저(Closure)와 in-place 콜백 정의가 코드의 가독성을 떨어뜨리고 코드 관리를 힘들게 하는 상황을 콜백 지옥(Callback Hell)이라고 하는데, 앞서 보여준 웹 스파이더 (v0)의 경우가 그러하다.
뭐 3 depth 정도야.. 눈감고 넘어가 줄 수는 있지만, 다른 기능이 추가될 수록 같은 패턴으로 작성하면 점점 지옥만 될 뿐이다.
우선 콜백 지옥을 해소하는 방법은 여러가지가 있다고(?) 하는데, 먼저 Return-Early-Pattern을 적용해보자.
Return-Early-Pattern은 사실 대단한 것이 아니다.
우리의 웹 스파이더(v0)의 코드를 살펴보면, 대부분 함수 스코프로 들어서서 다음과 같은 패턴을 가지고 있다.
if (err) {
callback(err);
} else {
// do something
}
여기에 Return-Early-Pattern을 씌워보면,
if (err) {
return callback(err);
}
// do something
짧은 코드에서는 큰 변화가 없어보이지만, depth가 깊어질 수록 효과가 큰 패턴이다.
이 패턴에 대한 굉장히 자세한 설명이 논문 형식으로 나와있는 글이 있어 첨부해본다. (영어)
잘 쓰여져 있어 읽어볼 만 한 것 같다. 장/단점에 대해 모두 자세히 설명해주고 있다.
https://medium.com/swlh/return-early-pattern-3d18a41bba8
Return Early Pattern
A rule that will make your code more readable.
medium.com
두 번째 해소 방법은 함수화이다. 너무 뻔한 방법이지만, 너무 중요한 습관이라고 생각한다.
우리의 웹 스파이더는 saveFile()과 download()라는 함수로 쪼개져서 다시 탄생할 것이다.
// saveFile
const saveFile = (filename, contents, callback) => {
fs.mkdir(path.dirname(filename), (err) => {
if (err) {
return callback(err);
}
fs.writeFile(filename, res.text, callback);
});
};
// download
const download = (url, filename, callback) => {
console.log(`Downloading ${url} into ${filename}`);
superagent.get(url).end((err, res) => {
if (err) {
return callback(err);
}
saveFile(filename, res.text, (err) => {
if (err) {
return callback(err);
}
console.log(`Downloaded and saved: ${url}`);
callback(null, res.text);
});
});
};
기능엔 아무런 변화가 없다.
확실히 해소를 하니 훨씬 깔끔해보인다.
마지막으로 spider도 바꿔보면,
export const spider = (url, callback) => {
const filename = urlToFilename(url);
fs.access(filename, (err) => {
if (!err || err.code !== "ENOENT") {
return callback(null, filename, false);
}
download(url, filename, (err) => {
if (err) {
return callback(err);
}
callback(null, filename, true);
});
});
};
심지어 더 좋아진 것은, saveFile()과 download() 역시 외부로 노출시킨 후 다른 곳에서 재사용할 수 있는 장점이 생겼다는 것이다.
이제 우리의 웹스파이더는 버전 1이 되었고, 리팩토링은 이쯤에서 끝내도록 하겠다.
다음엔 콜백의 비동기 control flow 중 순차 실행과 더불어 우리의 웹 스파이더를 업그레이드 해 보겠다.
'Back-end > Node.js' 카테고리의 다른 글
스트림 코딩 (Stream Coding) - (3) Writable (0) | 2021.08.22 |
---|---|
스트림 코딩 (Stream Coding) - (2) Readable (Flowing vs non-Flowing) (0) | 2021.08.19 |
스트림 코딩 (Stream Coding) - (1) 버퍼(buffer)와 스트림(stream) (0) | 2021.08.17 |
콜백(Callback)의 비동기 Control flow 패턴 (2) - 순차 반복(sequential iteration) (0) | 2021.07.31 |
모듈의 순환 종속성으로 알아보는 require() vs import (1) | 2021.07.29 |