May 8th 2019
Callback Hell Article 번역해 보았습니다. http://callbackhell.com/
자바스크립트 비동기 프로그래밍 가이드
비동기 프로그래밍 또는 자바스크립트에서 callback을 사용하는 경우, 코드를 직관적으로 읽기가 쉽지 않습니다. callback을 많이 사용하는 경우 아래와 같은 모양을 띄는 경우가 많습니다.
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);
aspect = values.width / values.height;
widths.forEach(
function(width, widthIndex) {
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안에 callback이 계속 이어지는 모습은 callback hell이라는 이름으로 알려져 있습니다.
자바스크립트에서 Callback Hell이 나타나는 경우는 대게, 코드를 작성할 때 작업이 진행되는 순서를 위에서 아래로 쭉 작성하기 때문이며, 많은 사람들이 이 같은 실수를 합니다. C나 Ruby, Python 같은 다름 프로그래밍 언어는 어떠한 작업이 진행되던, 코드의 둘째 줄이 시작하기 전에 첫 번째 줄의 코드 실행이 종료됩니다. 여러분도 아마 짐작 하셨듯이, 자바스크립트는 이와는 다릅니다.
'Callback'이라는 이름 자체는 단순히 자바스크립트에서 관습적으로 사용하는 이름이며, 특별한 의미는 없습니다. Asynchronous(a.k.a async)는 '지금이 아닌 이후의 어느 시점에'라는 의미가 있습니다. Callback은 주로 다운로드나 파일을 읽을 때, 또는 데이터베이스와 접촉 등과 같은 I/O 진행 시 사용합니다.
일반적인 함수를 호출하면 아래와 같이 함수의 반환 값을 사용할 수 있습니다.
var result = multiplyTwoNumbers(5, 10);
console.log(result);
// 50 gets printed out
하지만, 비동기에서 함수는 callback을 사용하고 바로 결과를 반환하지 않습니다.
var photo = downloadPhoto('http://coolcats.com/cat.gif');
// photo is 'undefined'
위의 경우 사진을 다운로드 하기 위해서 시간이 필요할 것이며, 다운로드하는 시간 동안 여러분의 프로그램이 중단되길 원치 않을 것입니다.
대신, 사진 다운로드가 완료되면 실행될 함수를 보관해 두어야 할 것입니다. 이것이 바로 callback입니다. 'downloadPhoto'라는 함수에 callback을 전달하고, 이 함수는 callback을 다운로드가 완료된 이후의 어떠한 시점에 실행할 것입니다.
downloadphoto('http://coolcats.com/cat.gif', handlePhoto);
function handlePhoto(error, photo) {
if (error) {
console.error('Download error!', error);
} else {
console.log('Download finished', photo);
}
}
console.log('Download started');
callback을 이해하기에 가장 큰 장애물은 프로그램이 실행되는 과정에서 작업이 실행되는 순서를 파악하는 것입니다. 위의 예에서, handlePhoto 함수가 선언됩니다. downloadPhoto 함수가 실행되고, handlePhoto가 callback으로 전달됩니다. 그리고 'Download started'라 프린트 됩니다.
handlePhoto 함수가 아직 실행되지 않았다는 점을 인지해야 합니다. downloadPhoto 함수가 자신의 임무를 완수할 때까지 이 함수는 실행이 되지 않으며, 사진 다운로드 작업이 완료될 때까지 시간이 많이 소요될 것입니다.
이 예는 두 가지 중요한 점을 보여줍니다.
Callback hell은 코드를 작성하는 관행에서 비롯되며, 다행이 아래의 기준을 따르면 이를 바로잡기가 그리 어렵지는 않습니다.
아래는 브라우저에서 서버로 AJAX 요청을 보내는 코드입니다.
var form = document.querySelector('form');
form.onsubmit = function formSubmit(submitEvent) {
var name = document.querySelector('input').value;
request(
{
url: 'http://example.com/upload',
body: name,
method: 'POST'
},
function postResponse(err, response, body) {
var statusMessage = document.querySelector('.status');
if (err) {
return (statusMessage.value = err);
}
statusMessage.value = body;
}
);
};
위의 코드에서 함수에 이름을 붙여 주었는데, 함수에 이름을 지으면 아래와 같은 즉각적인 이익을 얻을 수 있습니다.
함수에 이름이 있기 때문에, 이 함수의 위치를 이동시키도록 하겠습니다.
document.querySelector('form').onsubmit = formSubmit;
function formSubmit(submitEvent) {
var name = document.querySelector('input').value;
request(
{
url: 'http://example.com/upload',
body: name,
method: 'POST'
},
postResponse
);
}
function postResponse(err, response, body) {
var statusMessage = document.querySelecotr('.status');
if (err) {
return (statusMessage.value = err);
}
statusMessage.value = body;
}
함수 선언식은 호이스팅이 되기 때문에 파일의 맨 아래에 위치해 있습니다.
이 부분이 가장 중요합니다. 누구나 모듈(aka libraries)을 쉽게 생성할 수 있습니다. Node.js의 Isaac Schlueter의 "Write small modules that each do one thing, and assemble them into other modules that do a bigger thing. You can't get into callback hell if you don't go there."와 같이 말했습니다.
위 코드를 몇 개의 파일로 나누어 모듈화 해 보도록 하겠습니다.
formuploader.js라는 두 개의 함수를 가지고 있는 파일을 생성했습니다.
module.exports.submit = formSubmit;
function formSubmit (submitEvent) {
var name = document.querySelector('input').value;
request({
url: 'http://example.com/upload',
body; name,
method; 'POST'
}, postResponse);
}
function postResponse (err, response, body) {
var statusMessage = document.querySelctor('.status');
if (err) {
return statusMessage.value = err;
}
statusMessage.value = body;
}
'module.exports' 부분은 node.js 모듈 시스템의 일부이며, browserify를 사용하면 node, Electron, browser에서 모두 작동합니다. 모든 곳에서 작동을 하고 복잡한 설정 절차도 필요없기 때문에, 저는 이러한 방식의 모듈을 선호합니다.
이제 우리는 formuploader.js 파일을 만들었고, 이 파일은 browserified되 후 페이지의 script tag로 로드 돼 있습니다. 우리는 이것을 사용하기만 하면 됩니다.
var formUploader = require('formuploader');
document.querySelector('form').onsubmit = formUploader.submit;
우리의 애플리케이션은 두 줄의 코드로 줄었고, 아래와 같은 장점 또한 생겼습니다.
애플리케이션에는 다양한 에러가 발생할 수 있습니다. 이번 섹션에서는 아래의 세 번째 에러에 대해서만 다루도록 하겠습니다.
첫 번째, 두 번째 사항은 우선적으로 여러분의 코드를 읽기 쉽게 만드는 데에 목적이 있습니다. 하지만 마지막 사항은 여러분의 코드가 안정적으로 작동할 수 있도록 하는데 초점이 있습니다. callback을 다루게 되면 background에서 어떠한 작업을 진행 및 완료하던가 또는 실패로 인해 중단해야 할 것입니다. 어떠한 숙련된 개발자라도 이러한 에러가 언제 발생할 지 알 수 없으니, 에러에 대해 계획을 세워야 한다고 말할것입니다.
에러를 처리하는데 가장 흔히 사용되는 방법은 Node.js 스타일입니다. 이 방법에서 callback의 첫 번째 인자는 error를 위해 사용됩니다.
var fs = require('fs');
fs.readFile('/Does/not/exist', handleFile);
function handleFile(error, file) {
if (error) {
return console.error('Uhoh, there was an error', error);
}
// otherwise, continue on and use 'file' in your code
}
첫 번째 argument로 error를 받는 것은 간단하지만, 우리가 에러 처리를 잊지 않을 수 있도록 하는 유용한 방법입니다. error를 첫 번째 인자로 받지 않을 경우, 에러에 대한 사항이 쉽게 무시될 수 있습니다. (ex, function handleFile (file) {} )
callback hell을 피하는 방법 중 가장 중요한 부분은 함수를 이동시키는 것입니다. 이는 신규 개발자도 코드의 흐름을 읽는데 소요하는 시간을 줄일 수 있게 해 줍니다.
일단 시작은 함수를 파일의 마지막 부분으로 이동하는 것입니다. 그리고 이러한 함수들을 모듈화 시켜보세요. (ex, require('./photo-helpers.js') 그리고 마지막으로 독립적인 모듈을 만들 수도 있습니다. (ex, require('image-resize').
아래는 경험에 근거한 모듈을 생성하는 몇 가지 방법입니다.
고급 솔루션을 찾아보기 전에 자바스크립트에서 callback이 가장 근복적인 부분이란 점을 인지해야 합니다. 상위 해법들도 모두 callback을 이해하는 것에 근간을 두고 있기 때문에, 다음 단계로 나가기 전 callback을 읽고 쓰는 방법을 배워야 합니다. 그리고 좀 더 나은 callback 코드를 쓸 수 있도록 항상 고민해야 합니다.
여러분의 비동기 코드가 위에서 아래로 읽을 수 있도록 작성하기 위해 몇 가지 좋은 기술이 있습니다. 이러한 방법들은 성능 또는 플랫폼 호환성 문제를 야기할 수 있기 때문에 꼭 사용 전 확인하고 사용하기를 제안합니다.
Promise는 top-down방식으로 비동기 코드를 작성할 수 있고 try / catch 스타일의 에러 헨들링을 할 수 있기 때문에 에러 처리에도 좋습니다.
Generator는 전체 프로그램을 멈추지 않고 각 함수의 실행을 멈출 수 있게 해 줍니다. 하지만 top-down 방식으로 작성을 하게 되면 코드 읽기가 좀 더 복잡해 질 수 있습니다.
Async functions는 ES7의 추가되었지만, 많이 사용 되고 있습니다.
개인적으로 저는 코드를 작성할 때 90%의 경우 callback을 사용합니다. 코드가 복잡해 지면 run-parallel, run-series의 개념을 사용합니다. 저는 callback, promise, 또는 다른 방식들이 큰 차이를 가지고 오지 않는다고 생각합니다. 큰 차이는 nested code, 모듈 등이 아닌 코드를 간결하게 작성하는 것에서 온다고 생각합니다.