특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 자바스크립트의 특성을 말한다. 자바스크립트의 대부분의 DOM 이벤트 핸들러와 Timer 함수(setTimeout, setInterval), Ajax 요청은 비동기로 동작한다.
비동기적(Asynchronous) 으로 서버와 브라우저가 데이터를 교환할 수 있는 통신 방식이다.
서버로부터 웹페이지가 반환되면 화면 전체를 갱신해야 하는데 페이지 일부만을 갱신하고도 동일한 효과를 볼 수 있도록 하는 것이 Ajax이다. 이 때 XMLHttpRequest객체를 통해 서버에 요청하는데 Json이나 xml형태로 필요한 데이터만 받아 갱신하기 때문에 부드러운 화면전환이 가능하다.
XMLHttpRequest객체를 이용한 통신방식의 복잡성때문에 가독성이 떨어지는 점에 불편함을 느껴 jQuery를 통해 Ajax를 구현하기 시작했다. fetch API가 ES2015 표준으로 등장하면서 이제는 일반적으로 Fetch API를 통해 구현한다.
console.log(1)
setTimeout(function() {
console.log(2);
}, 0);
console.log(3)
위의 코드를 실행하면 그 결과는 1 2 3
가 아니라 1 3 2
다. setTimeout가 비동기로 동작하기 때문이다. interval을 0으로 설정해도 setTimeout의 콜백함수는 바로 실행되는 것이 아니라 다음 코드로 먼저 넘어간 후에 실행된다. 이것에 대한 과정을 자세히 알고 싶으면 여기로
비동기 처리를 위한 패턴으로 콜백함수
를 많이 이용해왔다. 하지만 콜백함수를 통해 비동기 처리를 하게 되면 중첩된 로직으로 에러, 예외처리가 어렵고 중첩으로 인한 복잡도가 증가하게 된다. 이러한 단점들을 해결하기 위해 ES6에서는 Promise
를 도입했다. 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.
jQuery로 Ajax구현 시 콜백함수로 비동기 처리하는 예시이다.
function getData() {
var tableData;
$.get('https://domain.com/products/1', function(response) {
tableData = response;
});
return tableData;
}
console.log(getData()); // undefined
Ajax통신을 통해 받는 데이터를 변수 tableData에 저장하여 반환한다. 하지만 비동기 특성때문에 응답 데이터를 받지 못하고 undefined
를 표시하게 된다. 비동기 함수의 처리 결과(tableData)에 대한 처리는 비동기 함수의 콜백 함수 내에서 처리해야 한다.
function getData(callbackFunc) {
$.get('https://domain.com/products/1', function(response) {
callbackFunc(response);
// 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
});
}
getData(function(tableData) {
console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});
이처럼 Ajax통신 함수 내의 콜백함수를 통해서 통신 결과를 받을 수 있다.
비동기 함수의 처리 결과를 가지고 다른 비동기 함수를 호출해야 하는 경우가 있다. 비동기 처리를 위해 콜백함수안에 콜백함수를 사용하는 콜백의 중첩이 생기게 되는데 이와 같은 구조를 Callback Hell(콜백지옥)
이라고 한다. 이러한 구조는 코드 가독성이 떨어지고 복잡도가 증가해 코드 수정도 어렵고 에러 처리하기도 힘들다. 아래는 콜백함수를 연속해서 사용한 Callback Hell(콜백지옥)
구조이다.
step1(function(value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
step5(value4, function(value5) {
// value5를 사용하는 처리
});
});
});
});
});
try {
setTimeout(() => { throw new Error('Error!'); }, 1000);
} catch (e) {
console.log('에러를 캐치하지 못한다..');
console.log(e);
}
비동기 함수(setTimeout)의 콜백함수에서 예외를 발생시켰다. setTimeout이 실행되면 setTimeout의 콜백함수가 실행될 때를 기다리지 않고 호출스택에서 제거된다. 즉, setTimeout의 콜백함수를 호출한 함수는 setTimeout이 아니다.
try 블록의 코드에서 예외가 발생하면 즉시 실행을 멈추고 catch 블록으로 이동하여 예외처리 코드를 실행해야한다. 하지만 setTimeout을 실행했을때 setTimeout의 콜백함수는 실행되지 않은 상태로 예외처리 코드를 실행하지 않는다. 예외가 발생했을 때(setTimeout의 콜백함수가 실행됐을 때)는 try내에서 함수호출에 의해 발생한 것이 아니다. 따라서 setTimeout의 콜백 함수 내에서 발생시킨 에러는 catch 블록에서 캐치되지 않아 (catch블록의 코드는 실행되지 않고) 프로세스는 종료된다.
프로미스는 Promise 생성자 함수new Promise
를 통해 인스턴스화한다. Promise 생성자 함수는 비동기 작업을 수행할 콜백 함수를 인자로 전달받는데 이 콜백 함수는 resolve
와 reject
함수를 인자로 전달받는다.
// Promise 객체의 생성
const promise = new Promise((resolve, reject) => {
// 비동기 작업을 수행한다.
if (/* 비동기 작업 수행 성공 */) {
resolve('result');
}
else { /* 비동기 작업 수행 실패 */
reject('failure reason');
}
});
Promise로 구현된 비동기 함수는 Promise 객체를 반환하여야 한다. Promise로 구현된 비동기 함수를 호출할 때 Promise 객체의 후속 처리 메소드(then, catch) 를 통해 비동기 처리 결과를 처리한다.
then
then 메소드는 두 개의 콜백 함수를 인자로 전달 받는다. 첫 번째 콜백 함수는 성공(fulfilled, resolve 함수가 호출된 상태) 시 호출되고 두 번째 함수는 실패(rejected, reject 함수가 호출된 상태) 시 호출된다.
catch
예외(비동기 처리에서 발생한 에러와 then 메소드에서 발생한 에러)가 발생하면 호출된다.
promise.then(res => console.log(res), err => console.error(err))
위의 코드처럼 에러를 처리하면 첫 번째 콜백함수에서 발생한 에러는 캐치하지 못하고 가독성이 좋지 않다.
비동기 처리에서 발생한 에러는 catch를 사용해서 처리할 수도 있다. catch를 사용하면 비동기 처리에서 발생한 에러(reject 함수가 호출된 상태)와 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있다.
promise
.then(res => console.log(res))
.catch(err => console.error(err));
비동기 함수의 결과를 활용해 다른 비동기 함수를 호출해야 하는 경우 콜백 헬이 발생하는데 promise chaining을 통해서 이를 해결할 수 있다.
// 포스트 id가 1인 포스트를 검색하고 프로미스를 반환한다.
promiseAjax('GET', `${url}/1`)
// 포스트 id가 1인 포스트를 작성한 사용자의 아이디로 작성된 모든 포스트를 검색하고 프로미스를 반환한다.
.then(res => promiseAjax('GET', `${url}?userId=${JSON.parse(res).userId}`))
.then(JSON.parse)
.then(render)
.catch(console.error);
참고
Promise