자바스크립트는 비동기 처리를 위해 콜백 함수를 활용한다. 하지만 콜백 함수를 계속해서 사용하면 콜백 헬로 인해 가독성이 떨어지고 에러를 처리하기도 어려워진다. ES6에서는 이를 해결하기 위해 Promise를 도입하였다. 이전에 동기와 비동기에 대해 공부를 하면서 Promise를 간략하게 알아봤는데 프로젝트를 하면서 Promise를 사용하게 되었고 확실히 개념을 잡고 정리를 해야할 필요성이 생겼다.
먼저 비동기가 무엇인지 알아보자.
자바스크립트 엔진은 하나의 태스크만 실행할 수 있는 싱글 스레드로 작동하기 때문에 한 번에 하나의 함수만 실행할 수 있다. 다음 코드를 보자. 싱글 스레드이기 때문에 apple 함수를 호출하면 apple 함수가 실행이 될 동안 banana 함수는 실행되지 않는다. "apple"을 출력한 후에 apple 함수에서 banana 함수를 호출하면서 banana 함수가 실행되기 시작한다.
function apple() {
console.log("apple");
banana();
}
function banana() {
console.log("banana");
peach();
}
function peach() {
console.log("peach");
}
apple();
console.log("finish");
위의 코드처럼 "finish"가 출력되기 전까지 앞에 있는 함수들에 의해 블로킹이 되어 가장 마지막에 실행이 된다. 현재 실행되고 있는 태스크에 의해 뒤에 있는 태스크가 실행되지 못하고 기다리는 것을 동기 처리라고 한다.
그렇다면 위의 코드를 어떻게 비동기 처리할 수 있을까? apple함수를 1초 뒤에 실행되도록 setTimeout으로 설정해보자. 그렇다면 동기 처리와는 다르게 "finish"가 출력되고 이후에 apple, banana, peach가 출력된다.
function apple() {
console.log("apple");
banana();
}
function banana() {
console.log("banana");
peach();
}
function peach() {
console.log("peach");
}
setTimeout(apple, 1000);
console.log("finish");
위의 코드처럼 현재 실행중인 태스크(apple 함수)가 종료되지 않았지만 뒤의 태스크가 실행("finish")되는 것을 비동기 처리라고 한다.
위처럼 비동기 처리가 가능한 것은 브라우저 또는 Node.js와 같은 런타임 환경이 있기 때문이다. 자바스크립트 엔진은 콜 스택을 통해 함수가 호출되었을 때 함수가 종료되면 POP하고 다른 함수가 PUSH되어 실행되지만 HTTP 요청, setTimeout과 같은 Web API 등은 태스크 큐에 저장되어 이벤트 루프에 의해 콜 스택으로 옮겨진다. 이벤트 루프는 콜 스택이 비어있는 것을 확인하고 태스크 큐에 있는 태스크를 콜 스택으로 옮겨 실행시킨다. 이를 통해 비동기 처리를 할 수 있게 되었다.
비동기 처리를 할 때의 문제점
▶ 콜백 헬이 발생한다.
먼저 API 요청과 같은 비동기 처리를 할 때 요청 후에 응답이 왔을 때 다음 코드를 실행해야한다. 하지만 API 요청은 비동기로 동작하기 때문에 응답이 오지 않더라도 다음 코드를 실행한다. 따라서 API 요청의 결과를 받아서 다음 API 요청을 할 때 계속해서 콜백 함수를 사용하게 될 것이며 최악의 상황에서 콜백 헬이 발생할 것이다.
▶ 에러 처리가 힘들다
아래의 코드는 1초 후에 에러를 발생시키는 코드이다. 만약 try 구문에서 에러가 발생했을 때 catch 구문이 실행될 것이다. 하지만 아래의 코드는 catch구문이 실행되지 않는다. 왜 그런 것일까?
try {
setTimeout(() => {
throw new Error("Error!!!");
}, 1000);
} catch (e) {
console.error("Hello Error");
}
먼저 setTimeout이 호출되어 콜 스택에 쌓인다. setTimeout은 비동기로 동작하기 때문에 콜백 함수(에러를 발생시키는 구문)를 기다리지않고 바로 종료가된다. 이후 1초 후에 에러를 발생시키게되는데 catch구문은 동작하지 않는다. 왜냐하면 에러는 호출자 방향으로 전파되는데 콜백 함수를 호출한 것은 setTimeout이 아니기 때문에 catch 구문에서 해당 오류가 캐치되지 않는 것이다.
다시 정리를 해보면, try 구문의 setTimeout이 실행된다. try 구문에서 에러가 발생하면 catch가 실행되는데 에러를 일으키는 setTimeout의 콜백함수는 setTimeout이 이미 제거된 이후에 동작하고 setTimeout에 의해 호출된 것이 아니기 때문에 에러가 전파되지 않는다. 이처럼 기존의 비동기 처리는 에러 처리가 힘들다는 문제가 존재한다.
Promise의 생성
Promise는 new 연산자와 함께 호출할 수 있다. 이때 Promise의 생성자 함수는 resolve와 reject 함수를 인수로 전달받는다. 아래의 형태로 Promise를 생성하여 사용할 수 있다.
const promise = new Promise((resolve, reject) => {
if (success) {
resolve('success')
}
else {
reject("fail")
}
})
위의 코드는 아래와 같은 그림으로 표현할 수 있다. Pormise를 생성하면 전달되는 실행 함수에 의해 상태가 변경된다. 초기의 Promise는 "pending"이라는 상태를 갖는다. 이후 resolve가 호출되면 "fulfilled"가 되고 반환 값은 "success"가 저장된다. reject가 호출되면 "rejected" 상태가되고 반환 값은 "fail"이 저장된다.
그리고 위와 같이 Promise 객체를 생성 후에 실행하면 .then, .catch, .finally를 통해 구독할 수 있어 Promise의 상태에 따라 후속으로 처리할 함수를 구성할 수 있다. 또한 이렇게 Promise가 실행된 상태를 "settled"라 한다.
promise.then(res => {
console.log(res)
})
Pormise를 적용하여 위에서 에러 처리가 힘들다는 예시의 코드를 다시 구성해보자. 아래의 코드에서는 1초 후에 promise의 상태가 rejected가 되어 reject 함수가 실행되어 "Hello Error"가 출력된다.
let promise = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error("Hello Error")), 1000);
});
'프로그래밍 > JavaScript' 카테고리의 다른 글
[JS] this 키워드는 어떻게 동작할까? (0) | 2021.08.30 |
---|---|
[JS] null과 undefined의 차이 (0) | 2021.08.23 |
[ES6] 제너레이터 (0) | 2021.08.04 |
[JS] 브라우저의 렌더링 과정 (0) | 2021.08.03 |
[JS] 함수 (0) | 2021.07.19 |