비동기 프로그래밍에 대해 알아보기 앞서,
이해를 돕기 위해 반대되는 개념인 동기(synchronous) 프로그래밍에 대해 먼저 알아보자.
단어가 생소할 수는 있어도 아주 쉬운 개념이다.
아래 코드는 여태까지 우리가 작성한 일반적인 코드의 모습이다.
const name = "Rin";
const greeting = `Hello, my name is ${name}!`;
console.log(greeting); // "Hello, my name is Rin!"
이 코드에서는
1. name이라는 문자열을 선언하고,
2. name을 사용하여 greeting이라는 문자열을 선언하고,
3. 출력했다.
각 라인의 코드는 이전 라인의 결과에 의존하고 있고, 프로그램을 작성한 순서대로 한 줄씩 실행된다.
이것이 바로 동기프로그래밍이다.
이러한 동기 방식은 간단하고 직관적이지만, 작업이 오래걸리거나 응답이 늦어지는 경우 문제가 생긴다.
예를들어 함수 하나를 호출한 후 다른 작업을 하고 싶은데, 만약 그 함수가 실행이 완료되기까지 엄청 오래걸리는 함수였다면?
(자바스크립트는 싱글 스레드 언어이기 때문에 한 번에 하나의 작업만 수행할 수 있다.)
우리는 그 함수에게 응답이 올 때까지 다른 작업을 하지 못하고 대기해야한다.
(예를들면 서버에 데이터를 요청하는 작업처럼 말이다.)
그래서 우리는 비동기(asynchronous)프로그래밍을 한다.
비동기(asynchronous)프로그래밍
원하는 때에 동작이 시작하도록 하는 프로그래밍
콜백(callback) 또는 프로미스(Promise)로 구현한다.
자바스크립트에서 비동기:동시성 프로그래밍을 하는 방법은 크게 callback(나중에 호출할 함수)과 Promise 두가지이다.
하지만 대부분의 최신 비동기 API에서는 콜백을 사용하지 않고 Promise를 사용한다.
(콜백 지옥이라고 들어보았는가?)
무언가를 비동기적으로 수행하는 함수는
함수 내 동작이 모두 처리된 후 실행되어야 하는 함수가 들어갈 callback함수를 인수로 반드시 제공해야한다.
그런데 처리해야할 중첩 콜백이 많다면?
콜백지옥에 빠지게 된다. 그리고 각각의 에러처리도 힘들다.
아래 코드는 콜백 기반 코드 절망편.
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// 모든 스크립트가 로딩된 후, 실행 흐름이 이어집니다. (*)
}
});
}
})
}
});
그래서 우리는 프로미스(Promise)를 사용한다.
프로미스(Promise)
비동기 프로그래밍을 위한 대리자로,
프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 return할 수 있다.
미래의 어떤 시점에 결과를 제공하겠다는 '약속'(Promise)을 반환한다.
Promise는 다음 중 하나의 상태를 가진다.
1. 대기(pending): 이행하지도, 거부하지도 않은 초기 상태.
2. 이행(fulfilled): 연산이 성공적으로 완료됨.
3. 거부(rejected): 연산이 실패함.
프로미스가 이행(fulfill)되거나 거부(rejected)될 때,
프로미스의 then 메서드에 의해 대기열(큐)에 추가된 처리기들이 호출된다.
프로미스 객체는 아래와 같은 문법으로 만들 수 있다. (생성자)
let promise = new Promise(function(resolve, reject) {
// executor 제작 코드(producing code).
//원격에서 스크립트를 불러오는 것 같은 시간이 걸리는 일을 한다.
});
executor(실행자, 실행함수)는 Promise가 만들어질 때 자동으로 실행되는데,
결과를 최종적으로 만들어내는 제작 코드를 포함한다.
1line) executor의 인수 resolve와 reject는 자바스크립트에서 자체 제공하는콜백이다.
성공하면 결과값과 함께 resolve가 호출되고, 에러가 발생하면 error값과 함께 reject가 호출된다.
(기본값은 undefined.)
우리는 건들이지 않고 executor 안에 코드만 작성하면된다.
Promise객체의 생성자 state는
처음엔 "pending"(보류)이다가 resolve가 호출되면 "fulfilled", reject가 호출되면 "rejected"로 변한다.
(그림1 참고)
Promise의 executor는 다음과 같이 resolve나 reject 중 하나만 반드시 호출하면 된다.
let promiseSuccess = new Promise(function(resolve, reject) {
// 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result는 '완료'가 됩니다.
setTimeout(() => resolve("완료"), 1000);
});
let promiseFail = new Promise(function(resolve, reject) {
// 1초 뒤에 에러와 함께 실행이 종료되었다는 신호를 보냅니다.
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});
executor는 new Promise에 의해 자동으로, 즉각적으로 호출되며
resolve나 reject에 의해 한번 변경된 상태는 더 이상 변하지 않는다.
(executor에 resolve가 여러개 있어도 최종 결과는 처음 resolve에 대한 결과.)
여태까지 프로미스 객체의 executor(제작 코드)를 봤으니
이제 제작코드의 결과나 에러를 받을 소비 함수에 대해 알아보자.
소비함수는 .then, .catch, .finally 메서드를 사용해 등록된다.
(자바 등 다른 언어의 try..catch문과 finally와 비슷한 개념이다.)
.then()
프로미스에서 가장 중요하고 기본이 되는 메서드
프로미스가 이행되었을 때와 거부되었을 때의 함수를 모두 포함할 수 있다.
문법은 다음과 같다.
promise.then(
function(result) { // 결과(result)를 다룸 },
function(error) { // 에러(error)를 다룸 }
);
간단히 작성한 다음 코드를 실행해 보자.
//then 사용 예
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("완료!"), 1000);
});
// resolve 함수가 .then의 첫 번째 함수(인수)를 실행
promise.then(
result => alert(result), // 1초 후 "완료!"를 출력
error => alert(error) // 실행되지 않음
);
.catch()
에러가 발생한 경우만 다루고 싶을 때 사용하는 메서드
.catch(f)는 문법이 간결하다는 점만 빼고 .then(null,f)와 완벽하게 동일하다.
에러가 발생한 경우만 다루고 싶다면 위에서 배운 then에
.then(null, errorHandlingFunction)같이 null을 첫 번째 인수로 전달하면 된다.
catch는 이와 동일하게 작동한다.
//catch 사용 예
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});
// .catch(f)는 promise.then(null, f)과 동일하게 작동
promise.catch(alert); // 1초 뒤 "Error: 에러 발생!" 출력
.finally()
프로미스가 처리되면(이행이나 거부에 상관없이) f가 항상 실행된다.