비동기 처리란, 일반적으로 입출력이나 네트워크 등 시간이 오래 걸리는 작업을 수행할 때, 해당 작업이 완료될 때까지 기다리지 않고 다음 코드를 실행하는 방법을 말한다. 그렇기 때문에 코드 흐름과 실제 제어 흐름이 다르다.
비동기 처리 방법의 기본인 콜백 함수에 대해 설명하려 한다. 콜백 함수란 함수의 인자로 전달되는 함수로, 비동기 처리가 끝난 후 결과를 처리하기 위해 호출되는 함수이다.
let a = "Hello World";
setTimeout(() => {
console.log(a);
});
console.log("Welcome!");
콜백 함수인 setTimeout을 사용한 코드이다. 위 코드를 실행하면 결과가 어떻게 나올까?
Welcome!
Hello World
위와 같이 결과가 나온다. 왜 이런 결과가 나오는 것일까?
그것은 비동기 코드를 처리하는 모듈이 자바스크립트 엔진 외부에 있기 때문이다. 이 엔진은 이벤트 루프, 태스크 큐(task queue), 잡 큐(job queue)등으로 구성된다.
API 모듈은 비동기 요청을 처리 후 태스크 큐에 콜백 함수를 넣는다. 자바스크립트 엔진은 콜 스택이 비워지면, 태스크 큐의 콜백 함수를 실행한다.
이 콜백 함수를 중첩해서 사용하면 소위 말하는 콜백 지옥에 빠질 수 있다.
setTimeout(function() {
console.log("Task 1");
setTimeout(function() {
console.log("Task 2");
setTimeout(function() {
console.log("Task 3");
setTimeout(function() {
console.log("Task 4");
}, 1000);
}, 1000);
}, 1000);
}, 1000);
위의 예시와 같은 콜백 지옥을 해결하기 위한 개념이 등장했다. 그것이 바로 프로미스다.
이 프로미스는 비동기 처리 결과를 나타내는 객체이다. 이 프로미스는 비동기 처리 작업이 완료되면 resolve 함수를 호출하여 결과를 반환하고, 실패하면 reject를 호출하여 에러를 반환한다.
프로미스는 태스크 큐 (task queue)가 아닌 잡큐(job queue 혹은 microtask queue)를 사용한다.
setTimeout(() => {
console.log("타임아웃1");
}, 0);
Promise.resolve().then(() => console.log("프로미스1"));
setTimeout(() => {
console.log("타임아웃2");
}, 0);
Promise.resolve().then(() => console.log("프로미스2"));
위와 같은 프로미스 코드는 어떤 결과가 나올까?
프로미스1
프로미스2
타임아웃1
타임아웃2
이렇게 결과값이 반환되는 이유는 마이크로태스크 큐의 우선순위가 태스크 큐보다 높기 때문이다. 그렇기 때문에 마이크로태스크 큐에 있는 Promise의 콜백 함수가 먼저 실행 된 후, 타이머 큐 (setTimeout)에 있는 콜백 함수가 실행된다.
프로미스는 then, catch, finally 메서드를 가지고 있다.
- then() 메서드는 성공했을 때 실행할 콜백 함수를 인자로 넘긴다.
- catch() 메서드는 실패했을 때 실행할 콜백 함수를 인자로 넘긴다.
- finally() 메서드는 성공/실패 여부와 상관없이 모두 실행할 콜백 함수를 인자로 넘긴다.
doSomething(0)
.then(doSomething)
.then(doSomething)
.then(doSomething)
.then(doSomething)
.then(doAnother)
.then(num => {
console.log('num', num);
});
다음과 같이 동일한 객체에 메서드를 연결시킬 수 있다. 이것을 체이닝(chaining)이라 한다. 이 것은 함수를 호출한 주체가 함수를 끝낸 뒤 자기 자신을 리턴하도록 하여 구현한다.
Promise를 활용한 비동기 코드를 간결하게 작성하는 문법이다. async함수는 await 키워드를 사용하며, await 키워드는 반드시 async함수 내에서만 사용해야 한다. 또한 async로 선언된 함수는 반드시 Promise를 리턴한다.
function tick(duration) {
return new Promise(resolve => {
setTimeout(resolve, duration);
});
}
async function test() {
console.log("Hello world");
await tick(1000);
console.log("Wait...");
await tick(2000);
console.log("Ok. Here we go!");
}
이 async 함수는 비동기 코드에 쉽게 순서를 부여할 수 있다. async 함수 내에서 오류를 처리해야 하는 경우에는 try/catch를 사용할 수 있다.