비동기 작업이란 어떤 한 작업이 완료될 때까지 기다리지 않고 다른 작업을 동시에 시작하는 것을 의미한다. 이렇게 함으로써 이전 작업이 진행되는 동안에도 다음 작업이 시작하거나 완료될 수 있다.
반대로 동기 작업이란, 어떤 한 작업이 완료된 후에야 다음 작업을 시작하는 것이다. 작성된 코드를 한 줄씩 순서대로 실행하는 자바스크립트에서는, 이전 함수가 값을 반환할 때까지 기다려야 함수 이후의 코드를 실행할 수 있다.
비동기 작업이 등장한 이유는, 동기 프로그래밍에 의해 작성된 코드가 진행되는 동안 브라우저에서는 다른 행동(클릭, 입력 등)을 할 수 없다는 불편이 야기되기 때문이다.
어떤 함수 doStep1()
이 비동기적으로 작동하도록 만들어졌고, 만들고자 하는 기능이나 애플리케이션의 로직 상 doStep1()
이 끝난 후 이어서doStep2()
함수를 실행해야 한다고 하자. 이때, doStep1()
함수에 doStep1(callback_function)
와 같이 '함수'의 형태로 doStep2()
를 전달한 뒤, doStep1(callback_function)
함수 내부에서 원하는 위치에서 doStep2()
를 실행한다.
이와 같이, 연쇄되는 두 가지 작업이 각각 함수로 표현될 때, 앞선 작업을 담당하는 함수에 인자로서 전달되는 '이후 작업을 담당하는 함수'를 콜백(또는 콜백함수)
라고 부른다.
그러나 세 가지 이상의 작업을 연쇄하여 진행하는 경우, 콜백 내부에서 콜백을 호출해야 하는 경우가 생긴다. 이렇게 되면 코드의 깊이가 깊어지기 떄문에 디버깅 및 에러 처리가 어려워지는 단점이 있다. 이를 "콜백 지옥" 이라고 부르기도 한다.
이 같은 이유 때문에, 최신 자바스크립트 비동기 API에서는 콜백 대신 Promise
를 사용한다.
Promise는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타낸다. Promise를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있다.
다만 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 '약속'(Promise)을 반환한다.
Promise를 사용하면 Callback을 사용할 때처럼 코드가 깊어지는 것을 방지할 수 있다.
let promise = new Promise(function(resolve, reject) {
// ...do something
});
Promise에는 executer(resolve, reject)
라는 함수가 전달되는데, 이는 Promise가 생성될 때 즉각적으로 자동 실행되는 함수이다. 이 executer()안에서 수행하고자 하는 작업을 코드로 작성하면 된다.
작업이 끝나면, executer()는 성공/실패 여부에 따라 resolve 혹은 reject라는 자체 콜백을 호출한다(작업이 끝나면 반드시 둘 중 하나를 호출한다).
이 콜백이 호출될 때, Promise 객체의 상태가 다음과 같이 변화한다.
Promise의 내부 프로퍼티인 status는 다음 중 하나의 상태를 가진다.
Promise의 status는 처음에는 pending
이었다가 resolve가 호출되면 "fullfilled"
상태로, reject가 호출되면 "rejected"
로 변한다.
참고로 이행 혹은 거부된 상태의 프라미스를 처리된(settled) 프라미스라고 부른다.
또한, 한 번 변경된 상태는 더 이상 변하지 않는다. 즉, resolve나 reject 중 하나의 인수가 한 번 호출되고 나면, 나머지 인수는 무시된다.
Promise의 내부 프로퍼티인 result는 다음과 같은 값을 가진다.
처음에 undefined
이었다가 resolve가 호출되면 "value"
로, reject가 호출되면 "error"
로 변한다.
Promise 객체는 executer()와 결과나 에러를 받을 소비함수를 이어주는 역할을 한다. 소비함수는 then
, catch
, finally
메서드를 통해 등록(구독)된다.
.then
은 다음과 같이 사용한다.
promise.then(
function(result) { /* 결과(result)를 다룸. */ },
function(error) { /* 에러(error)를 다룸. */ }
);
첫 번째 인수 function(result)
는 프라미스가 이행(fullfilled)되었을 때 실행되는 함수이고, 여기서 실행 결과를 받는다. 이때, 두 번째 인수는 무시되어 실행되지 않는다.
두 번째 인수 function(error)
는 프라미스가 거부되었을 때 실행되는 함수이고, 여기서 에러를 받는다. 이때, 첫 번째 인수는 무시되어 실행되지 않는다.
만약 작업이 성공적으로 처리된 경우만 다루고자 한다면, .then에 인수를 하나만 전달하면 된다.
let promise = new Promise(resolve => {
setTimeout(() => resolve("완료!"), 1000);
});
promise.then(alert); // 1초 뒤 "완료!" 출력
작업이 실패(=에러 발생)한 경우만 다루고 싶다면, then
을 사용하되 .then(null, errorHandlingFunction)
같이 null
을 첫 번째 인수로 전달하거나, catch
를 사용하면 된다. .catch(errorHandlingFunction)
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});
promise.catch(alert); // 1초 뒤 "Error: 에러 발생!" 출력
.catch(f)
는 promise.then(null, f)
과 동일하게 작동함.
프라미스의 이행(fullfilled)과 거부(rejected) 여부에 상관없이 항상 실행되는 부분을 나타내는 메서드이다. 결과가 어떻든 간에 마무리를 해야 하는, 즉 '보편적 동작'을 해야 하는 상황(ex. 로딩 인디케이터를 멈추거나, 모달을 숨겨야 하는 경우)에 사용할 수 있다.
.then(fullfilled, rejected)
와 기능이 비슷하지만, 그와 달리 finally에는 인수가 없다. 또한 finally에서는 프라미스가 이행되었는지, 거부되었는지 알 수 없다.
Promise의 결과를 '처리하는' 부분이라기보단, Promise의 결과를 '전달받아' 어떻게 할 지를 결정하는 부분으로 사용하면 된다.
finally 메서드는 반드시 맨 마지막에 사용되어야 하는 것은 아니다. finally를 먼저 쓰고, 결과나 에러를 then 혹은 catch 메서드를 사용한 다음 핸들러에 전달하여 사용해도 된다.
new Promise((resolve, reject) => {
setTimeout(() => resolve("결과"), 2000)
})
.finally(() => alert("프라미스가 준비되었습니다."))
.then(result => alert(result)); // <-- .then에서 result를 다룰 수 있음
----------------------------------------
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
})
.finally(() => alert("프라미스가 준비되었습니다."))
.catch(err => alert(err)); // <-- .catch에서 에러 객체를 다룰 수 있음
출처