Promise

p-q·2021년 9월 30일
0

JavaScript

목록 보기
6/11

Promis ??

프로미스는 비동기적 작업 처리에 사용되는 객체로
콜백 지옥의해결과 에러의 처리 용이성이 있다

콜백 지옥 (Pyramid of Doom)

비동기적으로 처리해야할 작업이 둘 이상이라고 가정했을때

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() {
                	if(error) {
                    	handleError(error);
                    } else {
                    	//계속 로드를 위해 반복되는 스크립트 ....
                    }
                });
            }
        });
    }
});

코드의 흐름

  1. 1.js 파일 로드 에러시 처리
  2. 1.js 파일 로드성공 2.js 파일 로드 에러시 처리
  3. 2.js 파일 로드성공 3.sj 파일 로드 에러시 처리

이처럼 처리해야 할 작업이 많아질수록 코드가 뾰족탑처럼 오른쪽으로 치우치는 형태를 보이는데
이런 형태를 피라미드와 비슷하다고 하여 Pyramid of Doom 이라고 부르게 되었다
위처럼 콜백 지옥은 가독성을 저해하게 되고
프로미스를 활용해 이러한 문제점을 해결할 수 있다.

에러의 처리

콜백 지옥을 프로미스를 사용하지 않고 해결하는 방법
익명 함수의 사용을 포기하고 콜백 함수들을 분리하는 방법을 사용한다

loadScript('1.js', step1);

function step1(error, script) {
	if (error) {
    	handleError(error);
    } else {
    	//...
        loadScript('2.js', step2);
    }
}

function step2(error, script) {
	if (error) {
    	handleError(error);
    } else {
    	//..
        loadScript('3.js', step4);
    }
}

// function step3, step4 ....

위의 예시처럼 콜백 함수의 분리를 통해 코드 가독성을 높일 수 있음에도 프로미스가 더 바람직한 이유는 에러 처리가 쉽다는 측면이 있다.

기본 문법

생성자 함수를 사용한 프로미스 객체 생성 법

const promise = new Promise(( resolve, reject ) => {
	// 실행함수(executor)
});

Promise() 생성자에 전달되는 함수는 실행함수(executor)로, 객체 생성 후 자동적으로 실행된다.

프로미스 객체 프로퍼티

프로미스 객체는 두 가지 프로퍼티(properties)를 가진다.

  1. 상태(state): [pending] 으로 초기화되며 [resolve]가 호출될 시 [fulfilled]로,
    [reject]가 호출될시 [rejected]로 바뀐다.
    [reject],[resolved] 두 상태를 통칭하여 [settled] 라고 한다.
  2. 결과(resule): [undefined]로 초기화되며 [resolve(value)]메서드가 호출될 시 [value]로, [reject(error)] 메서드가 호출될 시 [error]로 바뀐다.

기본 예제

const promise = new Promise(function(resolve, reject) {
	setTimeout(() => {
    	resolve('success');
    }, 1000);
});
console.log(promise);

프로미스 객체의 실행 함수를 1초가 지난 뒤에 실행하게끔 설정해두었으므로,
위의 예제를 실행시 콘솔상 다음과 같은 결과가 나타난다.

Promise {<pending>}
[[Prototype]]: Promise
[[PromiseState]]: "pending"
[[PromiseResult]]: undefined

이후 1초가 지난 시점에서 재확인한 결과

Promise {<fulfilled>: "success"}
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: "success"

주의사항

프로미스 객체의 실행 함수는 단 하나의 resolve 또는 reject만 처리 가능하다.

const promise = new Promise((resolve, reject) => {
    resolve('done!');

    reject(new Error('error')); // ignored
    setTimeout(() => {
        resolve('..');
    }, 1000);
});

프로미스의 reject는resolve와 마찬가지로 인자에 어떤 타입이 와도 상관이 없지만, Error객체와 함께 처리하는 것이 권장된다.

경고: .then()은 함수만 허용합니다.
경고: 오류가 아닌 약속이 거부되었습니다.
경고: 처리기에서 약속이 생성되었지만 반환되지 않았습니다.
오류확인
위와같은 오류가 발생할 수 있다.

resovle와reject는 꼭 비동기적으로 호출되어야 하는 것은 아니다.

const promise = new Promise((resolve, reject) => {
    resolve(123);
});

프로미스 객체의 state와result는 외부에서 접근할 수 없다. .then, .catch, .finally 메소드를 통해 다뤄진다.

then, catch, finally

then

.then 메서드는 첫 번째 인자로 resolved 상태를 처리하는 함수를 받고, 두 번째 인자로는 rejected 상태를 처리하는 함수를 받는다.

promise.then(
	function(result){
    	// resolved
    },
    function(error){
    	// rejected
    }
);

catch

.catch 메소드는 .then 메소드의 첫 번째 인자에 null을 전달한 것과 마찬가지로 작동
프로미스 객체의 에러를 처리할 때(rejected된 경우) 사용

const asyncTing = new Promise((resolve, reject) => {
	setTimeout(() => reject(new Error('Error!')), 1000
});

asyncThing.catch(alert); // promise.then(null, alert) 과 같다

프로미스의 에러는 가급적 .catch메소드를 사용해야 한다.

finally

.finally 메소드는 프로미스가 settled상태일 때 호출된다.
프로미스 객체의 정의후, 작업 처리의 성공,실패 여부와 상관없이, .finally 메소드를 사용하기만 하면 무조건 호출된다.
.finally 는 인자를 받지 않는다

const promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('result!'), 1000);
});
promise
    .then(console.log);
    .finally(() => {
        alert('promise ready!');
    })
    

프로미스 체이닝

비동기 함수의 처리 결과를 가지고 다른 비동기 함수를 호출해야 하는 경우 함수의 호출이 중첩이 되어 복잡도가 높아지는 콜백 지옥이 발생한다. 프로미스는 후속 처리 메소드를 체이닝(chainning)하여 여러 개의 프로미스를 연결하여 사용할 수 있다. 이로써 콜백 지옥을 해결한다.

Promise 객체를 반환한 비동기 함수는 프로미스 후속 처리 메소드인 then이나 catch메소드를 사용할 수 있다. 따라서 then 메소드가 Promise 객체를 반환하도록 하면 (then메소드는 기본적으로 promise를반환) 여러 개의 프로미스를 연결하여 사용할 수 있다.

프로미스 체이닝 예제
서버로 부터 특정 포스트를 취득한 후, 그 포스트를 작성한 사용자의 아이디로 작성된 모든 포스트를 검색하는 예제

<!DOCTYPE html>
<html>
<body>
  <pre class="result"></pre>
  <script>
    const $result = document.querySelector('.result');
    const render = content => { $result.textContent = JSON.stringify(content, null, 2); };

    const promiseAjax = (method, url, payload) => {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open(method, url);
        xhr.setRequestHeader('Content-type', 'application/json');
        xhr.send(JSON.stringify(payload));

        xhr.onreadystatechange = function () {
          if (xhr.readyState !== XMLHttpRequest.DONE) return;

          if (xhr.status >= 200 && xhr.status < 400) {
            resolve(xhr.response); // Success!
          } else {
            reject(new Error(xhr.status)); // Failed...
          }
        };
      });
    };

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

    // 포스트 id가 1인 포스트를 검색하고 프로미스를 반환한다.
    promiseAjax('GET', `${url}/1`)
      // 포스트 id가 1인 포스트를 작성한 사용자의 아이디로 작성된 모든 포스트를 검색하고 프로미스를 반환한다.
      .then(res => promiseAjax('GET', `${url}?userId=${JSON.parse(res).userId}`))
      .then(JSON.parse)
      .then(render)
      .catch(console.error);
  </script>
</body>
</html>

프로미스의 정적 메소드

promise는 주로 생성자 함수로 사용되지만 함수도 객체이므로 메소드를 갖을 수 있다.

Promise.resolve/Promise.reject

Promist.resolve와 Promise.reject 메소드는 존재하는 값을 Promise 로 래핑하기 위해 사용
정적 메소드 Promise.resoleve 메소드는 인자로 전달된 값을 resolve하는 Promise를생성한다

const resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [1, 2, 3]

const resolvedPromise = new Promise(resolve => resolve([1, 2, 3]));
resolvedPromise.then(console.log); // [ 1, 2, 3 ]

Promise.reject 메소드는 인자로 전달된 값을 reject하는 프로미스를 생성한다.

const rejectedPromise = Promise.reject(new Error('Error!'));
rejectedPromise.catch(console.log); // Error: Error!

const rejectedPromise = new Promise((resolve, reject) => reject(new Error('Error!')));
rejectedPromise.catch(console.log); // Error: Error!

Promise.all

Promise.all 메소드는 프로미스가 담겨 있는 배열 등의 이터러블을 인자로 전달 받는다.
그리고 전달받은 모든 프로미스를 병렬로 처리하고 그 처리 결과를 resolve하는 새로운 프로미스를 반환한다.

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(console.log) // [ 1, 2, 3 ]
  .catch(console.log);

Promise.all 메소드는 3개의 프로미스를 담은 배열을 전달받았다. 각각의 프로미스는 아래와 같이 동작한다.

첫번째 프로미스는 3초 후에 1을 resolve하여 처리 결과를 반환한다.
두번째 프로미스는 2초 후에 2을 resolve하여 처리 결과를 반환한다.
세번째 프로미스는 1초 후에 3을 resolve하여 처리 결과를 반환한다.

Promise.all 메소드는 전달받은 모든 프로미스를 병렬로 처리한다. 이때 모든 프로미스의 처리가 종료될 때까지 기다린 후 아래와 모든 처리 결과를 resolve 또는 reject한다.

모든 프로미스의 처리가 성공하면 각각의 프로미스가 resolve한 처리 결과를 배열에 담아 resolve하는 새로운 프로미스를 반환한다. 이때 첫번째 프로미스가 가장 나중에 처리되어도 Promise.all 메소드가 반환하는 프로미스는 첫번째 프로미스가 resolve한 처리 결과부터 차례대로 배열에 담아 그 배열을 resolve하는 새로운 프로미스를 반환한다. 즉, 처리 순서가 보장된다.

프로미스의 처리가 하나라도 실패하면 가장 먼저 실패한 프로미스가 reject한 에러를 reject하는 새로운 프로미스를 즉시 반환한다.

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 1!')), 3000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 2!')), 2000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 3!')), 1000))
]).then(console.log)
  .catch(console.log); // Error: Error 3!

위 예제의 경우, 세번째 프로미스가 가장 먼저 실패하므로 세번째 프로미스가 reject한 에러가 catch 메소드로 전달된다.

Promise.all 메소드는 전달 받은 이터러블의 요소가 프로미스가 아닌 경우, Promise.resolve 메소드를 통해 프로미스로 래핑된다.

Promise.all([
  1, // => Promise.resolve(1)
  2, // => Promise.resolve(2)
  3  // => Promise.resolve(3)
]).then(console.log) // [1, 2, 3]
  .catch(console.log);

github id로 gitgub 사용자 이름 가져오기

const githubIds = ['jeresig', 'ahejlsberg', 'ungmo2'];

Promise.all(githubIds.map(id => fetch(`https://api.github.com/users/${id}`)))
  // [Response, Response, Response] => Promise
  .then(responses => Promise.all(responses.map(res => res.json())))
  // [user, user, user] => Promise
  .then(users => users.map(user => user.name))
  // [ 'John Resig', 'Anders Hejlsberg', 'Ungmo Lee' ]
  .then(console.log)
  .catch(console.log);

위 예제의 Promise.all 메소드는 fetch 함수가 반환한 3개의 프로미스의 배열을 인수로 전달받고 이 프로미스들을 병렬 처리한다. 모든 프로미스의 처리가 성공하면 Promise.all 메소드는 각각의 프로미스가 resolve한 3개의 Response 객체가 담긴 배열을 resolve하는 새로운 프로미스를 반환하고 후속 처리 메소드 then에는 3개의 Response 객체가 담긴 배열이 전달된다. 이때 json 메소드는 프로미스를 반환하므로 한번 더 Promise.all 메소드를 호출해야 하는 것에 주의하자. 두번째 호출한 Promise.all 메소드는 github로 부터 취득한 3개의 사용자 정보 객체가 담긴 배열을 resolve하는 프로미스를 반환하고 후속 처리 메소드 then에는 3개의 사용자 정보 객체가 담긴 배열이 전달된다.

Promise.race

Promise.race 메소드는 Promise.all 메소드와 동일하게 프로미스가 담겨 있는 배열 등의 이터러블을 인자로 전달 받는다. 그리고 Promise.race 메소드는 Promise.all 메소드처럼 모든 프로미스를 병렬 처리하는 것이 아니라 가장 먼저 처리된 프로미스가 resolve한 처리 결과를 resolve하는 새로운 프로미스를 반환한다.

Promise.race([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(console.log) // 3
  .catch(console.log);

에러가 발생한 경우는 Promise.all 메소드와 동일하게 처리된다. 즉, Promise.race 메소드에 전달된 프로미스 처리가 하나라도 실패하면 가장 먼저 실패한 프로미스가 reject한 에러를 reject하는 새로운 프로미스를 즉시 반환한다.

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 1!')), 3000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 2!')), 2000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 3!')), 1000))
]).then(console.log)
  .catch(console.log); // Error: Error 3!

참조
https://poiemaweb.com/es6-promise
https://joshua1988.github.io/vue-camp/

profile
ppppqqqq

0개의 댓글