Callback Hell 리팩토링하기
Intro
앞에서 Callback Hell의 다음 예시를 보았고,
const fs = require("fs");
const gm = require("gm");
const source = "./images/in/"
const widths = [100, 200, 300]
const dest = "./images/out/"
fs.readdir(source, function (err, files) {
if (err) {
console.log("Error finding files: " + err);
} else {
files.forEach(
function (filename, fileIndex) {
console.log(filename);
gm(source + filename).size(
function (err, values) {
if (err) {
console.log("Error identifying file size: " + err);
} else {
console.log(filename + " : " + values);
const aspect = values.width / values.height;
widths.forEach(
function (width, widthIndex) {
const height = Math.round(width / aspect);
console.log("resizing " + filename + " to " + height + "x" + height);
this.resize(width, height).write(
dest + "w" + width + "_" + filename,
function (err) {
if (err) console.log("Error writing file: " + err);
}
);
}.bind(this)
);
}
}
);
}
);
}
});
Callback을 빼주는 것만으로는 함수들의 재사용이 어렵다는 것을 보았다.
그 이유 2가지는 다음과 같았다.
- 각 작업이 뒤의 작업에 의존하고 있으며, 그 의존성이 함수 본문에 hard-coding되어 있음.
- 각 작업의 error handling이 함수 본문에 hard-coding되어 있음.
이를 리팩토링하고자 한다.
Continuation-Passing Style로 정리하기
먼저 files.forEach(callback)
까지만 앞에 따로 빼 보자.
function forEachFilenameInDir({ sourceDir }, next) {
fs.readdir(sourceDir, function (err, filenames) {
if (err) {
console.log("Error finding files: " + err);
} else {
filenames.forEach(next);
}
});
}
forEachFilenameInDir({ sourceDir: source }, function (filename) {
console.log(filename);
...
}
그 다음 size를 구하는 부분만 따로 떼면 좋을 것 같다.
여기서 안쪽에서 this
를 사용하기 때문에, 그에 해당되는 값도 같이 전해주자.
function getImageSize({ filename, sourceDir }, next) {
console.log(filename);
const image = gm(sourceDir + filename);
image.size(function (err, values) {
if (err) {
console.log("Error identifying file size: " + err);
} else {
next({ image, values });
}
});
}
그 다음 나머지 부분을 통째로 빼주면 다음과 같다.
function saveImage({ filename, values, destDir, image }, next) {
console.log(filename + " : " + values);
const aspect = values.width / values.height;
widths.forEach(function (width, widthIndex) {
const height = Math.round(width / aspect);
console.log("resizing " + filename + " to " + height + "x" + height);
const destFilename = destDir + "w" + width + "_" + filename;
image.resize(width, height).write(destFilename, next);
});
}
함수를 실행해주는 부분은 이렇게 된다.
const next = function (err) {
if (err) {
console.log(err);
}
};
forEachFilenameInDir({ sourceDir: source }, function (filename) {
getImageSize({ filename, sourceDir: source }, function ({ image, values }) {
saveImage({ filename, values, destDir: dest, image }, next);
});
});
각 작업은 뒤의 작업에 의존하고 있지 않고 서로 분리가 가능하다.
추가 리팩토링
지금은 error handling 코드가 고정되어 있다고 가정하고 함수 내에 고정시켜놓았다. 이를 함수와 분리시켜 보자.
forEachFilenameInDir
는 다음과 같이 분해가 가능하다.
function forFilenamesInDir({ sourceDir }, next) {
fs.readdir(sourceDir, next);
}
function handleFileError(err, next) {
if (err) {
console.log("Error finding files: " + err);
} else {
next();
}
}
호출 부분은 이렇게 되는데...
forFilenamesInDir({ sourceDir: source }, function (err, filenames) {
handleFileError(err, function () {
filenames.forEach(function (filename) {
getImageSize(
...
);
});
});
});
여기서 forFilenamesInDir
는 하는 일이 없기 때문에 다시 fs.readdir
로 바꾸어주자.
fs.readdir(source, function (err, filenames) {
handleFileError(err, function () {
filenames.forEach(function (filename) {
getImageSize(
{ filename, sourceDir: source },
function ({ image, values }) {
saveImage({ filename, values, destDir: dest, image }, next);
}
);
});
});
});
마찬가지로 다른 보조함수들도 쪼개줄 수 있으며 그러면 다음과 같게 된다.
const fs = require("fs");
const gm = require("gm");
const sourceDir = "./images/in/";
const newWidths = [100, 200, 300];
const destDir = "./images/out/";
function handleFileError(err, next) {
if (err) {
console.log("Error finding files: " + err);
} else {
next();
}
}
function getImageSize({ filename, sourceDir }, next) {
console.log(filename);
const image = gm(sourceDir + filename);
image.size(function (err, values) {
next(err, { image, values });
});
}
function handleImageSizeError(err, next) {
if (err) {
console.log("Error identifying file size: " + err);
} else {
next();
}
}
function resizeAndSaveImage({ image, aspectRatio, newWidth, filename }, next) {
const newHeight = newWidth / aspectRatio;
const message = "resizing " + filename + " to " + newHeight + "x" + newHeight;
console.log(message);
const destFilename = destDir + "w" + newWidth + "_" + filename;
image.resize(newWidth, newHeight).write(destFilename, next);
}
fs.readdir(sourceDir, function (err, filenames) {
handleFileError(err, function () {
filenames.forEach(function (filename) {
getImageSize({ filename, sourceDir }, function (err, { image, values }) {
handleImageSizeError(err, function () {
console.log(filename + " : " + values);
const aspectRatio = values.width / values.height;
newWidths.forEach(function (newWidth) {
resizeAndSaveImage({ image, aspectRatio, newWidth, filename }, function (err) {
if (err) {
console.log(err);
}
});
});
});
});
});
});
});
안쪽의 함수들을 inline해주었더니 약간 지저분해지기는 했지만 공통 부분은 다시 잘 조합하면 해결되는 문제인 것 같다.
어쨌든 이제는 Continuation-passing style을 따라서 읽으면 자연스럽게 읽히는 것을 확인할 수가 있다.
Author And Source
이 문제에 관하여(Callback Hell 리팩토링하기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@nightlyherb/Callback-Hell-리팩토링하기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)