본문 바로가기

ECMAScript 6

프로미스

프로미스

자바스크립트는 비동기 처리를 위해 하나의 패턴으로 콜백 함수를 사용한다.

ES6에서 비동기 처리를 위한 패턴으로 프로미스를 도입했다.


#1 콜백 헬

콜백 헬은 비동기 처리를 위해 콜백 패턴을 사용하면 처리 순서를 보장하기 위해 여러 개의 콜백 함수가 네스팅(중첩)되어 복잡도가 높아지는 현상을 말한다.

콜백 헬은 코드의 가독성을 나쁘게 하며 실수를 유발하는 원인이 된다.

아래는 콜백 헬이 발생하는 전형적인 사례이다.

step1(function(value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
step5(value4, function(value5) {
// value5를 사용하는 처리
});
});
});
});
});

콜백 헬이 발생하는 이유는 비동기 처리 모델은 실행 완료를 기다리지 않고 즉시 다음 태스크를 실행한다.

따라서 비동기 함수(비동기를 처리하는 함수) 내에서 처리 결과를 반환하면 기대한 대로 동작하지 않는다.

아래 코드를 보자

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Promise example</title>
</head>
<body>
<h1>Promise example</h1>
<script>
// 비동기 함수
function get(url) {
// XMLHttpRequest 객체 생성
const xhr = new XMLHttpRequest();

// 서버 응답 시 호출될 이벤트 핸들러
xhr.onreadystatechange = function() {
// 서버 응답 완료
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) { // 정상 응답
/*
비동기식 처리 모델은 처리 완료를 기다리지 않고 즉시 다음 태스크를 실행한다.
따라서 비동기 함수 내애서 처리 결과를 반환(또는 전역 변수에 할당)하면
기대한 대로 동작하지 않는다.
비동기 함수의 결과에 대한 처리는 함수 내에서 처리해야 한다.
*/
console.log(xhr.response);
return xhr.response;
} else { // 비정상 응답
console.log('Error: ' + xhr.status);
}
}
};

// 비동기 방식으로 Request 오픈
xhr.open('GET', url);
// Request 전송
xhr.send();
}

const url = 'http://jsonplaceholder.typicode.com/posts/1';
/*
get 함수는 비동기 함수이므로 처리 완료를 기다리지 않고 즉시 다음 태스크를 수행한다.
즉, 함수의 실행이 완료되어 함수의 반환값을 받기 이전에 다음 태스크로 진행한다.
따라서 res는 undefined이다.
*/

const res = get(url);
console.log(res); // undefined
</script>
</body>
</html>


코드를 살펴보면 비동기 함수의 처리 결과를 반환하는 경우 순서가 보장되지 않기 때문에 그 반환 결과를 가지고 후속 처리를 할 수 없다.

즉, 비동기 함수의 처리 결과에 대한 처리는 비동기 함수의 콜백 함수 내에서 처리해야 한다. 이로 인해서 콜백 헬이 발생하는 것이다.


#2 프로미스의 생성

프로미스는 Promise 생성자 함수를 통해 인스턴스화한다.

Promise 생성자 함수는 비동기 작업을 수행할 콜백 함술를 인자로 전달받는데 이 콜백 함수는 resolve와 reject 함수를 인자로 전달받는다.

// Promise 객체의 생성
const promise = new Promise((resolve, rejec) => {
// 비동기 작업 수행
if (/* 비동기 작업 수행 성공 */) {
resolve('result');
}
else { /* 비동기 작업 수행 실패 */
reject('failure reason');
}
});

프로미스는 비동기 처리가 성공했는지 또는 실패했는지 등의 상태 정보를 갖는다.

   상태      의미                                   구현
pending 비동기 처리가 아직 수행되지 않은 상태 resolve 또는 reject 함수가 아직 호출되지 않은 상태
fulfilled 비동기 처리가 수행된 상태(성공) resolve 함수가 호출된 상태
rejected 비동기 처리가 수행된 상태(실패) reject 함수가 호출된 상태
setted 비동기 처리가 수행된 상태(성공 또는 실패) resolve 또는 reject 함수가 호출된 상태


다음은 Promise를 사용하여 비동기 함수를 정의하는 코드이다.

// 비동기 함수
function get(url) {
// Promise 객체의 생성과 반환
return new Promise((resolve, reject) => {
// XMLHttpRequest 객체 생성
const xhr = new XMLHttpRequest();

// 서버 응답 시 호출될 이벤트 핸들러
xhr.onreadystatechange = function() {
// 서버 응답 완료
if (xhr.readyState === 200) { // 정상 응답
// resolve 메소드에 처리 결과를 전달
resolve(xhr.response);
} else { // 비정상 응답
// reject 메소드에 에러 메시지를 전달
reject('Error: ' + xhr.status);
}
};

// 비동기 방식으로 Request 오픈
xhr.open('GET', url);
// Request 전송
xhr.send();
});
}


위 예제처럼 비동기 함수 내에서 Promise 객체를 생성하고 그 내부에서 비동기 처리를 구현한다.

이때 비동기 처리에 성공하면 resolve, 실패하면 reject 메소드를 호출한다.


#3 프로미스의 사용

Promise로 구현된 비동기 함수는 Promise 객체를 반환해야 한다.

Promise로 구현된 비동기 함수를 호출하는 측에서는 Promise 객체의 후속 처리 메소드 (then, catch)를 통해 비동기 처리 결과 또는 에러 메시지를 전달받아 처리한다.

Promise 객체는 상태를 갖는데 이 상태에 따라 후속 처리 메소드를 체이닝 방식으로 호출한다.

후속 처리 메소드는 아래와 같다.

* then : 두 개의 콜백 함수를 인자로 전달받는다.
첫 번째 콜백 함수는 성공(fulfilled, resolve 함수가 호출된 상태) 시 호출되고,
두 번째 콜백 함수는 실패(rejected, reject 함수가 호출된 상태) 시 호출된다.

* catch : 예외(비동기 처리에서 발생한 에러와 then 메소드에서 발생한 에러) 가 발생하면 호출된다.


앞에서 프로미스로 정의한 비동기 함수 get을 이용해보자

get 함수는 XMLHttpRequest 객체를 통해 Ajax 요청을 수행하므로 브라우저에서 실행해야 한다.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Promise example</title>
</head>
<body>
<h1>Promise example</h1>
<pre id="result"></pre>
<script>
// 비동기 함수
function get(url) {
// Promise 객체의 생성과 반환
return new Promise((resolve, reject) => {
// XMLHttpRequest 객체 생성
const xhr = new XMLHttpRequest();

// 서버 응답 시 호출될 이벤트 핸들러
xhr.onreadystatechange = function() {
// 서버 응답 완료
if (xhr.readyState === 200) { // 정상 응답
// resolve 메소드에 처리 결과를 전달
resolve(xhr.response);
} else { // 비정상 응답
// reject 메소드에 에러 메시지를 전달
reject('Error: ' + xhr.status);
}
};

// 비동기 방식으로 Request 오픈
xhr.open('GET', url);
// Request 전송
xhr.send();
});
}


const url = 'http://jsonplaceholder.typicode.com/posts/1';
/*
비동기 함수 get은 Promise 객체를 반환한다.
Promise 객체의 후속 메소드를 사용하여 비동기 처리 결과에 대한 후속 처리를 수행한다.
*/
get(url).then(
// 첫 번째 콜백 함수는 성공 시 호출된다.
result => document.getElementById('result').innerHTML = result,
// 두 번째 함수는 실패 시 호출된다.
error => alert(error)
);
</script>
</body>
</html>


위 코드의 비동기 함수 get은 Promise 객체를 반환한다.

Promise 객체의 후속 처리 메소드를 사용해 비동기 처리 결과에 대한 후속 처리를 수행한다.


#4 프로미스의 에러 처리

비동기 처리 시에 발생한 에러 메시지는 then 메소드의 두 번째 콜백 함수로 전달된다.

Promise 객체의 후속 처리 메소드 catch을 사용해도 에러 처리가 가능하다.
get(url)
.then(result => document.getElementById('result').innerHTML = result)
.catch(error => console.log(error));


catch 메소드는 에러를 처리한다는 점에서 then 메소드의 두 번째 콜백 함수와 유사하지만 미묘한 차이가 있다.

then 메소드의 두 번째 콜백 함수는 비동기 처리에서 발생한 에러만을 캐치한다.

하지만 catch 메소드는 비동기 처리에서 발생한 에러뿐만 아니라 then 내부에서 발생한 에러도 캐치한다.

따라서 에러 처리는 catch 메소드를 사용하는 편이 보다 효율적이다.


#5 프로미스 체이닝

Promise 객체를 반환한 비동기 함수는 프로미스 후속 처리 메소드인 then이나 catfch 메소드를 사용할 수 있다.

따라서 then 메소드가 Promise 객체를 반환하도록 하면 여러 개의 프로미스를 사용할 수 있다.


아래는 서버로부터 특정 포스트를 취득한 후 , 그 포스트를 작성한 사용자의 아이디로 작성된 다른 포스트를 검색하는 예제이다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Promise example</title>
</head>
<body>
<!-- 서버로부터 특정 포스트를 취득한 후, 그 포스트를 작성한 사용자의 아이디로 작성된 다른 포스트를 검색하는 예제 -->
<h1>Promise example</h1>
<pre id="result"></pre>
<script>
// 비동기 함수
function get(url) {
// Promise 객체의 생성과 반환
return new Promise((resolve, reject) => {
// XMLHttpRequest 객체 생성
const xhr = new XMLHttpRequest();

// 서버 응답 시 호출될 이벤트 핸들러
xhr.onreadystatechange = function() {
// 서버 응답 완료
if (xhr.readyState === 200) { // 정상 응답
// resolve 메소드에 처리 결과를 전달
resolve(xhr.response);
} else { // 비정상 응답
// reject 메소드에 에러 메시지를 전달
reject('Error: ' + xhr.status);
}
};

// 비동기 방식으로 Request 오픈
xhr.open('GET', url);
// Request 전송
xhr.send();
});
}

const url = 'http://jsonplaceholder.typicode.com/posts';

// 포스트 id가 1인 포스트를 검색하고 프로미스 반환
get(`${url}/1`)
// 포스트 id가 1인 포스트 작성자의 아이디로 작성된 모든 포스트를 검색하고 프로미스 반환
.then(result1 => get(`${url}?userId=${JSON.parse(result1).userId}`))
// 포스트 검색 결과를 DOM에 반영
.then(result2 => document.getElementById('result').innerHTML = result2)
.catch(error => console.log(error));
</script>
</body>
</html>


다음 글에선 이터레이션 프로토콜과 for-of 루프에 대해 알아보도록 하겠습니다.


'ECMAScript 6' 카테고리의 다른 글

Symbol  (0) 2019.08.20
이터레이션 프로토콜 & for-of 루프  (0) 2019.08.19
클래스 -2  (0) 2019.08.17
클래스 -1  (0) 2019.08.16
디스트럭처링  (0) 2019.08.14