Promise Chaining

100pearlcent·2021년 11월 13일
0

JavaScript

목록 보기
21/22
post-thumbnail

예시

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  alert(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});
  1. 1초 후 최초 프라미스가 이행 = (*)
  2. 첫번째 .then 핸들러가 호출 = (**)
  3. 2에서 반환한 값은 다음 .then 핸들러에 전달 = (****)
  4. 반복

프라미스 체이닝이 가능한 이유는 promise.then을 호출하면 프라미스가 반환되기 때문이다
반환된 프라미스에는 당연히 .then을 호출할 수 있고 핸들러가 값을 반환할 때엔 이 값이 프라미스의 result가 된다
👉 다음 .then은 이 값을 이용하여 호출된다


하나의 프라미스에 .then을 여러 개 추가하는 것은 체이닝이 아니다

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

예시의 프라미스는 하나이지만 여기에 등록된 핸들러는 여러 개이다
이 핸들러들은 result를 순차적으로 전달하지 않고 독립적으로 처리한다




프라미스 반환하기

then(handler)에 사용된 핸들러가 프라미스를 생성하거나 반환하는 경우가 있다
이 경우 이어지는 핸들러는 프라미스가 처리될 때 까지 기다리다가 처리가 완료되면 그 결과를 받는다

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000);

}).then(function(result) {

  alert(result); // 1

  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) { // (**)

  alert(result); // 2

  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) {

  alert(result); // 4

});

첫 번째 .then1을 출력하고 new Promise(..)를 반환한다 = (*)
1초 후 이 프라미스가 이행되고 그 결과((**))는 2를 출력하고 동일한 과정이 반복된다
👉 핸들러 안에서 프라미스를 반환하는 것도 비동기 작업 체이닝을 가능하게 한다



프라미스로 코드 개선하기

loadScript("/article/promise-chaining/one.js")
  .then(function(script) {
    return loadScript("/article/promise-chaining/two.js");
  })
  .then(function(script) {
    return loadScript("/article/promise-chaining/three.js");
  })
  .then(function(script) {
    // 불러온 스크립트 안에 정의된 함수를 호출해
    // 실제로 스크립트들이 정상적으로 로드되었는지 확인
    one();
    two();
    three();
  });

순차적으로 스크립트를 불러오는 위 코드를 화살표 함수를 사용하면 아래와 같이 줄일 수 있다

loadScript("/article/promise-chaining/one.js")
  .then(script => loadScript("/article/promise-chaining/two.js"))
  .then(script => loadScript("/article/promise-chaining/three.js"))
  .then(script => {
    // 스크립트를 정상적으로 불러왔기 때문에
  	// 스크립트 내의 함수를 호출가능
    one();
    two();
    three();
  });

loadScript를 호출할 때마다 프라미스가 반환되고 다음 .then은 프라미스가 이행되었을 때 실행된다
이후 다음 스크립트를 로딩하기 위한 초기화가 진행되고 스크립트는 이런 과정을 거쳐 순차적으로 로드된다

체인에 더 많은 비동기 동작을 추가할 수도 있는데, 추가 작업이 많아져도 코드가 오른쪽으로 길어지지 않고 아래로만 증가해서 활 모양의 코드가 만들어지지 않는다


loadScript("/article/promise-chaining/one.js").then(script1 => {
  loadScript("/article/promise-chaining/two.js").then(script2 => {
    loadScript("/article/promise-chaining/three.js").then(script3 => {
      // 여기서 script1, script2, script3에 정의된 함수를 사용가능
      one();
      two();
      three();
    });
  });
});

혹은 이와 같이 각 loadScript.then을 바로 붙일 수도 있지만 코드가 오른쪽으로 길어지는 문제가 발생한다



thenable

핸들러는 프라미스가 아닌 thenable이라는 객체를 반환하기도 한다
.then이라는 메서드를 가진 객체는 모두 thenable객체라고 부르는데, 이 객체는 프라미스와 같은 방식으로 처리된다

'thenable' 객체에 대한 아이디어는 서드파티 라이브러리가 '프라미스와 호환 가능한' 자체 객체를 구현할 수 있다는 점에서 나왔다
이 객체들엔 자체 확장 메서드가 구현되어 있겠지만 .then이 있으므로 네이티브 프라미스와도 호환 가능하다

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { 네이티브 코드 }
    // 1초 후 this.num*2와 함께 이행됨
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise(resolve => resolve(1))
  .then(result => {
    return new Thenable(result); // (*)
  })
  .then(alert); // 1000밀리 초 후 2를 보여줌


fetch와 체이닝 함께 응용하기

프론트에서는 네트워크 요청 시 프라미스를 자주 사용한다

let promise = fetch(url);

위 코드를 실행하면 url에 네트워크 요청을 보내고 프라미스를 반환한다
원격 서버가 헤더와 함께 응답을 보내면, 프라미스는 response 객체와 함께 이행된다
그런데 이때 response 전체가 완전히 다운로드 되기 전에 프라미스는 이행 상태가 되어버린다


fetch('/article/promise-chaining/user.json')
  // 원격 서버가 응답하면 .then 아래 코드가 실행
  .then(function(response) {
    // response.text()는 응답 텍스트 전체가 다운로드되면
    // 응답 텍스트를 새로운 이행 프라미스를 만들고, 이를 반환
    return response.text();
  })
  .then(function(text) {
    // 원격에서 받아온 파일의 내용
    alert(text); // {"name": "jinju", "isAdmin": true}
  });

응답이 완전히 종료되고, 응답 전체를 읽으려면 메서드 response.text()를 호출해야 한다
response.text()는 원격 서버에서 전송한 텍스트 전체가 다운로드되면, 이 텍스트를 result값으로 갖는 이행된 프라미스를 반환한다


fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => alert(user.name));

메서드 response.json()을 쓰면 원격에서 받아온 데이터를 읽고 JSON으로 파싱할 수 있다


// user.json에 요청을 보냄
fetch('/article/promise-chaining/user.json')
  // 응답받은 내용을 json으로 불러옴
  .then(response => response.json())
  // GitHub에 요청을 보냄
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  // 응답받은 내용을 json 형태로 불러옴
  .then(response => response.json())
  // 3초간 아바타 이미지(githubUser.avatar_url)를 보여줌
  .then(githubUser => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => img.remove(), 3000); // (*)
  });

만약 아바타가 잠깐 보였다가 사라진 이후에 무언가를 하고 싶다면 아래 코드와 같이
체인을 확장할 수 있도록 아바타가 사라질 때 이행 프라미스를 반환해주어야 한다

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise(function(resolve, reject) { // (*)
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser); // (**)
    }, 3000);
  }))
  // 3초 후 동작함
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));

(*).then핸들러는 이제 setTimeout안의 resolve(githubUser)를 호출했을 때 ((**))만 처리상태가 되는 new Promise를 반환하고 체인의 다음 .then은 이를 기다린다

비동기 동작은 항상 프라미스를 반환하도록 하는 것이 좋다
당장은 체인을 확장할 계획이 없더라도 이렇게 구현해 놓으면 나중에 체인 확장이 필요한 경우 손쉽게 체인을 확장할 수 있다

위 함수를 재사용 가능한 함수 단위로 분리할 수 있다

function loadJson(url) {
  return fetch(url)
    .then(response => response.json());
}

function loadGithubUser(name) {
  return fetch(`https://api.github.com/users/${name}`)
    .then(response => response.json());
}

function showAvatar(githubUser) {
  return new Promise(function(resolve, reject) {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  });
}

// 함수를 이용하여 다시 동일 작업 수행
loadJson('/article/promise-chaining/user.json')
  .then(user => loadGithubUser(user.name))
  .then(showAvatar)
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));
  // ...

요약

.then이나 .catch, .finally의 핸들러가 프라미스를 반환하면, 나머지 체인은 프라미스가 처리될 때 까지 대기한다
처리가 완료되면 프라미스의 result(값 또는 에러)가 다음 체인으로 전달된다



0개의 댓글