ES6에서 도입된 비동기 처리를 위한 패턴으로 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.
const get = url => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.send(); xhr.onload = () => { if (xhr.status === 200) { console.log(JSON.parse(xhr.response)); // undefined } else { console.error(`${xhr.status} ${xhr.statusText}`); } }; }; // id가 1인 post를 취득 get('https://jsonplaceholder.typicode.com/posts/1');
const get = url => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.send(); xhr.onload = () => { if (xhr.status === 200) { console.log(JSON.parse(xhr.response)); // undefined } else { console.error(`${xhr.status} ${xhr.statusText}`); } }; }; // id가 1인 post를 취득 get('https://jsonplaceholder.typicode.com/posts/1');
get 함수는 비동기 함수이다. get 함수가 비동기 함수인 이유는 get 함수 내부의 onload 이벤트 핸들러가 비동기로 작동하기 때문이다. get 함수를 호출하면 GET 요청하고 onload 이벤트 핸들러를 등록한 다음 undefined를 반환하고 즉시 종료된다.
즉, 비동기 함수 내부의 비동기로 작동하는 코드는 비동기 함수가 완료된 이후에 완료된다. 따라서 비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않는다.
비동기 함수 : 함수 내부에 비동기로 작동하는 코드를 포함한 함수를 말한다. 비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않았다 해도 기다리지 않고 종료된다.
비동기 함수 get이 호출되면 함수 코드를 평가하는 과정에서 get 함수의 실행 컨텍스트가 생성되고 콜 스택에 푸시된다. 이후 함수 코드 실행 과정에서 xhr.onload 이벤트 핸들러 프로퍼티에 이벤트 핸들러가 바인딩 된다. get 함수가 종료되면 get 함수의 실행 컨텍스트가 콜 스택에서 제거되고, 곧 바로 console.log가 호출된다. 이때 console.log의 실행 컨텍스트가 콜 스택에 푸시된다.
서버로부터 응답이 도착하면 xhr 객체에서 load 이벤트가 발생한다. 이때 xhr.onload 이벤트 핸들러 프로퍼티에 바인딩한 이벤트 핸들러가 즉시 실행되는 것이 아니다. xhr.onload 이벤트 핸들러는 load 이벤트가 발생하면 일단 태스트 큐에 저장되어 대기하다가, 콜 스택이 비면 이벤트 루프에 의해 콜 스택으로 푸시되어 실행한다.
따라서 xhr.onload 이벤트 핸들러가 실행되는 시점에는 콜 스택이 빈 상태여야 하므로 console.log는 이미 종료된 이후다. 즉, xhr.onload 이벤트 핸들러에서 상위 스코프의 변수에 서버의 응답 결과를 할당하기 이전에 console.log가 먼저 호출되어 undefined가 출력된다.
따라서 비동기 함수의 처리 결과에 대한 후속 처리는 비동기 함수 내부에서 수행해야 한다. 이때 후속 처리로 콜백 함수를 전달하는 것이 일반적이다.
const get = (url, successCallback, failureCallback) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.send(); xhr.onload = () => { if (xhr.status === 200) { successCallback(JSON.parse(xhr.response)); } else { failureCallback(xhr.status); } }; }; get('https://jsonplaceholder.typicode.com/posts/1', console.log, console.error);
const get = (url, successCallback, failureCallback) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.send(); xhr.onload = () => { if (xhr.status === 200) { successCallback(JSON.parse(xhr.response)); } else { failureCallback(xhr.status); } }; }; get('https://jsonplaceholder.typicode.com/posts/1', console.log, console.error);
이처럼 콜백 함수를 통해 비동기 처리 결과에 대한 후속 처리를 수행하는 비동기 함수가 비동기 처리 결과를 가지고 또 다시 비동기 함수를 호출해야 한다면 콜백 함수 호출이 중첩되어 복잡도가 높아지는 현상이 발생하는데, 이를 콜백 헬이라고 한다.
try { setTimeout(() => { throw new Error('Error'); }, 1000); } catch(e) { console.error('캐치한 에러', e); }
try { setTimeout(() => { throw new Error('Error'); }, 1000); } catch(e) { console.error('캐치한 에러', e); }
try 코드 블럭 내에서 호출한 setTimeout 함수는 1초 후에 콜백 함수가 실행되도록 타이머를 설정하고, 이후 콜백 함수는 에러를 발생시킨다. 하지만 이 에러는 catch 코드 블럭에서 캐치되지 않는다. setTimeout 함수의 콜백 함수가 실행될 때 setTimeout 함수는 이미 콜 스택에서 제거된 상태다. 이것은 setTimeout 함수의 콜백 함수를 호출한 것이 setTimeout 함수가 아니라는 것이다.
에러는 호출자 방향으로 전파된다. 즉, 콜 스택의 아래 방향으로 전파된다. 하지만 앞에서 살펴본 바와 같이 setTimeout 함수의 콜백 함수를 호출한 것은 setTimeout 함수가 아니다. 따라서 setTimeout 함수의 콜백 함수가 발생시킨 에러는 catch 블록에서 캐치되지 않는다.
Promise 생성자 함수를 new 연산자와 함께 호출하면 프로미스를 생성한다. ES6에서 도입된 Promise는 ECMAScript 사양에 정의된 표준 빌트인 객체다. Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수를 인수로 전달받는데, 이 콜백 함수는 resolve와 reject 함수를 인수로 전달 받는다.
const promise = new Promise((resolve, reject) => { if (/* 비동기 처리 성공 */) { resolve('result'); } else { /* 비동기 처리 실패 */ reject('failure reason'); } });
const promise = new Promise((resolve, reject) => { if (/* 비동기 처리 성공 */) { resolve('result'); } else { /* 비동기 처리 실패 */ reject('failure reason'); } });
Promise 생성자 함수가 인수로 전달받은 콜백 함수 내부에서 비동기 처리를 수행한다. 이때 비동기 처리가 성공하면 콜백 함수의 인수로 전달받은 resolve 함수를 호출하고, 비동기 처리가 실패하면 reject 함수를 호출한다.
앞에서 살펴본 비동기 함수 get을 프로미스를 사용해 다시 구현해보자.
const promiseGet = url => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.send(); xhr.onload = () => { if (xhr.status === 200) { resolve(JSON.parse(xhr.response)); } else { reject(xhr.status); } }; }); }; premiseGet('https://jsonplaceholder.typicode.com/posts/1');
const promiseGet = url => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.send(); xhr.onload = () => { if (xhr.status === 200) { resolve(JSON.parse(xhr.response)); } else { reject(xhr.status); } }; }); }; premiseGet('https://jsonplaceholder.typicode.com/posts/1');
비동기 함수인 promiseGet은 함수 내부에서 프로미스를 생성하고 반환한다. 비동기 처리는 Promise 생성자 함수가 인수로 전달받은 콜백 함수 내부에서 수행한다. 만약 비동기 처리가 성공하면 비동기 처리 결과를 resolve 함수에 인수로 전달하면서 호출하고 비동기 처리가 실패하면 비동기 처리 결과를 reject 함수에 인수로 전달하면서 호출한다.
즉, 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체이다.
프로미스의 비동기 처리 상태가 변화하면 이에 따른 후속 처리를 해야 한다. 예를 들어, 프로미스가 fulfilled 상태가 되면 프로미스의 처리 결과를 가지고 무언가를 해야 하고 프로미스가 rejected 상태가 되면 프로미스의 처리 결과(에러)를 가지고 에러 처리를 해야한다. 이를 위해 프로미스는 후속 메서드 then, catch, finally를 제공한다.
즉, 첫 번째 콜백 함수는 비동기 처리가 성공했을 때 호출되는 성공 처리 콜백 함수이며, 두 번째 콜백함수는 비동기 처리가 실패했을 때 호출되는 실패 처리 콜백 함수다.
// fulfilled new Promise(resolve => resolve('fulfilled')) .then(v => console.log(v), e => console.error(e)); // fulfilled // rejected new Promise((_, reject) => reject(new Error('rejected'))) .then(v => console.log(v), e => console.error(e)); // Error: rejected
// fulfilled new Promise(resolve => resolve('fulfilled')) .then(v => console.log(v), e => console.error(e)); // fulfilled // rejected new Promise((_, reject) => reject(new Error('rejected'))) .then(v => console.log(v), e => console.error(e)); // Error: rejected
then 메서드는 언제나 프로미스를 반환한다. 만약 then 메서드의 콜백 함수가 프로미스를 반환하면 그 프로미스를 그대로 반환하고, 콜백 함수가 프로미스가 아닌 값을 반환하면 그 값은 암묵적으로 resolve 또는 reject하여 프로미스를 생성해 반환한다.
catch 메서드는 한 개의 콜백 함수를 인수로 전달받는다. catch 메서드의 콜백 함수는 프로미스가 rejected 상태인 경우만 호출한다.
// rejected new Promise((reject) => reject(new Error('rejected'))) .catch(e => console.error(e)); // Error: rejected
// rejected new Promise((reject) => reject(new Error('rejected'))) .catch(e => console.error(e)); // Error: rejected
finally 메서드는 한 개의 콜백 함수를 인수로 전달받는다. finally 메서드의 콜백 함수는 프로미스의 성공(fulfilled) 또는 실패(rejected)와 상관없이 무조건 한 번 호출된다. finally 메서드는 프로미스의 상태와 상관없이 공통적으로 수행해야 할 처리 내용이 있을 때 유용하다. finally 메서드도 then/catch 메서드와 마찬가지로 언제나 프로미스를 반환한다.
new Promise(() => {}) .finally(() => console.log('finally')); // finally
new Promise(() => {}) .finally(() => console.log('finally')); // finally
마지막으로 프로미스로 구현한 비동기 get을 사용해 후속 처리를 구현해보자.
const promiseGet = url => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.send(); xhr.onload = () => { if (xhr.status === 200) { resolve(JSON.parse(xhr.response)); } else { reject(xhr.status); } }; }); }; premiseGet('https://jsonplaceholder.typicode.com/posts/1') .then(res => console.log(res)) .catch(err => console.error(err)) .finally(() => console.log('Bye!'));
const promiseGet = url => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.send(); xhr.onload = () => { if (xhr.status === 200) { resolve(JSON.parse(xhr.response)); } else { reject(xhr.status); } }; }); }; premiseGet('https://jsonplaceholder.typicode.com/posts/1') .then(res => console.log(res)) .catch(err => console.error(err)) .finally(() => console.log('Bye!'));
# Contact : jyw966@naver.com
Copyright © doromo. All Rights Reserved.