동기처리(Synchronous processing)와 비동기처리(Asynchronous processing)
자바스크립트의 콜백(callback)과 프로미스(Promise)등을 이야기 하려면 우선 동기처리와 비동기처리의 개념에 대해서 알고 가야 한다.
동기처리란 직렬적으로 작업을 수행한다는 의미로, 모든 작업이 순차적으로 이루어지는 것을 의미한다. 그리고 앞의 작업이 끝나기 전까지 뒤에 작업들은 대기를 하게 된다. 실생활과 관련된 예를 들면 카페에서 줄을 서있는데, 내 앞사람의 주문이 끝나고 주문한 커피를 받을때까지 계속 대기해야 하는 상황과 같다. 즉, 모든일을 순차적으로 처리해야 한다. 이를 앞선 작업이 끝날때까지 다른 작업들이 블로킹(Blocking)된다고 표현한다. 보통의 자바스크립트 함수는 동기식 처리 모델로 동작한다.
function foo1() {
console.log('foo1')
foo2()
}
function foo2() {
console.log('foo2')
foo3()
}
function foo3() {
console.log('foo3')
foo4()
}
function foo4() {
console.log('foo4')
}
foo1()
//
foo1
foo2
foo3
foo4 의 순으로 출력된다.
//
위의 예시를 보면 모든 작업들이 순차적으로 이뤄진다는 것을 알 수 있다.
비동기처리란 병렬적으로 작업을 수행한다는 의미로, 동기처리와는 다르게 모든 작업이 순차적으로 이루어지지 않는다. 그리고 앞의 작업이 끝나기 전이라도 대기하지 않고 뒤에 작업들을 실행한다. 실생활과 관련된 예를 들면 카페에서 줄을 서있는데, 앞사람의 주문이 끝나면 주문한 사람은 진동벨을 가지고 자신의 자리로 돌아가 볼일을 보고 뒤에 있던 사람은 다시 이 과정을 반복하는 것을 예로들 수 있다. 즉, 모든일을 순차적으로 하지 않고(순번이 앞섰더라도 더 만들기 쉬운 음료수를 시킨사람의 음료수가 먼저 나올 수 있다.), 앞선 일이 종료되지 않았더라도 대기하지 않고(앞사람이 음료수를 아직 받지 않아도 다음 사람은 주문을 할 수 있다.) 다음 작업을 실행하는 것을 의미한다. 이를 앞선 작업의 종료 유무와 관련없이 블로킹(Blocking)되지 않고 다음 작업을 진행한다고 해서 논블로킹(Non-Blocking)이라고 한다. 자바스크립트 대부분의 DOM 이벤트 핸들러와 Timer 함수(setTimeout, setInterval), Ajax 요청은 비동기식 처리 모델로 동작한다.
function foo1() {
console.log('foo1')
foo2()
}
function foo2() {
setTimeout(() => {
console.log('foo2')
}, 500);
foo3()
}
function foo3() {
console.log('foo3')
foo4()
}
function foo4() {
console.log('foo4')
}
foo1()
//
foo1
foo3
foo4
foo2 의 순으로 출력된다.
//
위의 예시를 보면 foo2의 콘솔에 setTimeout 함수를 사용한것을 알 수 있다. 다른 함수들은 setTimeout에서 설정한 시간까지 대기하지 않고 먼저 자신의 일을 수행한다. 그래서 결과가 순서대로 나오지 않고 실행이 끝난 순서대로 출력된다.
동기와 비동기를 설명하면서 블로킹과 논블로킹도 같이 언급을 했는데, 둘의 관계가 항상 동기-블로킹, 비동기-논블로킹인 것 만은 아니다. 반대의 경우도 있는데 여기서는 다루지 않고 나중에 동기와 비동기 블로킹과 논블로킹에 관련된 글을 따로 써서 거기서 다뤄볼 예정이다. 어쨌든 이정도 개념까지는 알고 콜백과 프로미스에 대한 설명으로 넘어가려 한다.
자바스크립트 콜백(Callback)
자바스크립트에서는 상당히 많은 기능들이 비동기로 작동한다. 앞서 예시로 들었던 ajax, timer, DOM 이벤트 핸들러까지 상당히 빈번하게 사용된다. 그래서 콜백을 사용하여 비동기처리의 순서를 보장해준다. 콜백은 간단히 말해서 함수의 parameter로 또 다른 함수를 설정하는 형태라고 할 수 있다.
stage1(function (value1) {
stage2(value1, function (value2) {
});
});
위의 예시와 같은 방식으로 함수를 계속 인자로 넘겨주게 되면 처리 순서를 보장 받을 수 있다. 예를 들어서 사용자가 임의의 버튼을 클릭하면 서버에서 사용자 정보를 가져와서 화면에 표시해주는 기능이 있다고 생각해보자.ajax 요청은 비동기로 작동하기 때문에 사용자가 버튼을 누르고 받은 정보를 띄워주는 프로세스에서 아직 서버 정보를 받아오지 못해 텅텅 빈 화면을 보여줄 수도 있다. 그래서 사용자가 버튼을 클릭했을때 그 뒤 기능들의 처리 순서를 보장해줘야 한다. 그럴려면 우선 ajax가 서버에서 사용자 정보를 받아오는 과정을 기다려야 할 것이다. 이렇게 서버에서 뿌려준 정보를 받아온 다음 화면에 보여주려면 앞선 ajax의 반환값을 가지고 후속 처리를 해야한다. 그런데 이를 그냥 반환하게되면 처리순서 보장이 안되어서 앞서 말한 오류가 발생한다. 즉 비동기 함수의 결과값을 다시 처리하려면 비동기 함수의 콜백함수 내에서 처리를 해야 한다.
하지만 콜백 함수는 몇 가지 치명적인 문제를 가지고 있다.
위의 예시는 간단한 예시라서 콜백이 많이 겹치지 않지만 실행순서를 보장해야할 함수가 엄청 많다면 어떻게 될까?
stage1(function (value1) {
stage2(value1, function (value2) {
stage3(value2, function (value3) {
stage4(value3, function (value4) {
stage5(value4, function (value5) {
});
});
});
});
});
보기만해도 함수가 어디서 시작하고 어디서 끝나는지 알아보기가 힘들고 복잡해 보인다. 이렇듯 계속 함수가 네스팅 되어서 가독성이 떨어지고 복잡도가 높아지는 현상을 콜백지옥(Callback Hell)이라고 한다.
이렇듯 콜백은 가독성을 나쁘께하고 지나친 중첩으로 인해 실수를 유발한다. 또한 에러처리를 어렵게 한다.
try {
setTimeout(function() {
throw 'Error';
}, 1000);
} catch(e) {
console.log(e,'이런 에러가 발생했다');
}
위 예시를 살펴보자. try, catch 구문을 사용하여 에러를 케치하려고 한다. 위의 예시에서 setTimeout에서 throw 하는 에러를 캐치 할 수 있을까? 정답은 '아니다' 이다. setTimeout안에 선언된 함수는 엄밀히 말하면 try, catch 구문과 전혀 상관이 없다. 왜냐하면 결국 setTimeout안에 정의된 함수를 실행시키는 건 timer 이벤트이기 때문이다. 즉, try, catch 구문안에서 일어난 오류가 아니기 때문에 catch가 불가능한 것이다.
참고자료 - https://poiemaweb.com/js-async
참고자료 - https://poiemaweb.com/es6-promise