ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JS/Asynchronous] Promise / Promise.all(), Promise.race() 정리
    Frontend 2019. 8. 10. 17:07

     

     

    저번에 event loop 포스팅에서 언급했듯이 싱글 스레드 언어인 자바스크립트는 한 번에 한 가지 일 밖에 못한다.

    그래서 모든 작업을 동기로 처리하면 데이터를 받아와서 화면에 렌더링할 때

    데이터 받아오는 작업을 완료하기 전까지는 렌더링을 할 수 없기 때문에 화면이 블록(block)되어 버린다.

     

     

    이미지 출처: https://www.deadcoderising.com/being-asynchronous-in-javascript-using-promises/

     

    그래서 비동기란 것이 생겨났다.

    비동기란 일종의 예약 시스템이다.

     

    작업을 동기(Synchronous)로 처리하게 되면 

    어떤 작업이 끝날 때까지 다음 작업을 진행할 수 없지만

     

    비동기(Asynchronous)로 처리하게 되면 이야기가 달라진다.

    어떤 요청을 보내서 응답이 올 때까지 기다려야되는 작업이나

    사전에 지정한 시간을 기다려야하는 작업들을 예약을 걸어놓고

    지금 당장 처리할 수 있는 작업들을 진행하는 것이다.

     

    그러다가 당장 처리할 수 있는 작업들을 모두 처리하고

    예약 걸어둔 작업들의 예약 시간이 되었을 때 그 작업들을 순차적으로 처리하는 것이다.

     

     

    예를 들어서 마트 계산대를 생각해보자.

     

    어떤 고객 A가 계산을 하던 도중에 잘못 가져온 물건이 있다고 다시 쇼핑을 하러 갔다고 생각해보자.

    만약에 동기로 처리한다면 계산대에 줄 선 고객들은 고객 A가 다시 물건을 가져올 때까지 하염없이 기다려야한다.

     

    그러나 비동기로 처리하면 고객 A가 다시 물건을 가져올 때까지 순차적으로 다른 고객들의 계산을 처리하고 있다가

    고객 A가 물건을 가져오면 그 시점에 계산하고 있던 손님의 물건을 모두 계산해준 후 다시 A의 물건을 계산한다.

     

     

     

    Callback Function (콜백 함수)

    function sayHello (callback) {
      setTimeout(() => {
        console.log('Hello');
        callback();
      }, 1000);
    }
    
    sayHello(function () {
      setTimeout(() => {
        console.log('World');
      }, 1000);
    });

     

    Promise가 나오기 전에는 비동기는 콜백 함수를 이용하여 처리할 수 밖에 없었다.

     

    위 코드를 보면 sayHello()라는 함수는 1초 후 콘솔 창에 'Hello'를 출력하고 인자로 받은 callback 함수를 실행한다.

    callback 함수는 또 1초 후에 'World'라는 단어를 콘솔 창에 출력하는 함수이므로

     

    위 코드를 실행하면 1초 후 'Hello', 그 다음 1초 후 'World'가 순차적으로 출력된다.

     

     

    function printWord (word, callback) {
      setTimeout(() => {
        console.log(word);
    
        if (callback) {
          callback();
        }
      }, 1000);
    }
    
    printWord('Hello', function () {
      printWord ('World', function () {
        printWord ('I am', function () {
          printWord ('Julia');
        });
      });
    });

     

    그런데 이 방식의 문제점은 함수를 인자로 넘겨받는 과정을 계속 반복해야하기 때문에

    콜백안에 또 콜백을 넘겨주는 형태로 적을 수 밖에 없다.

     

    이 과정이 조금이라도 길어지면 코드의 가독성이 매우 떨어지고 수정하기 어려워진다.

     

     

     

    Promise (프로미스)

    이것을 해결하기 위해 등장한 것이 바로 Promise이다.

    Promise는 3가지의 상태를 가진다.

     

    • 대기(pending): 이행하거나 거부되지 않은 초기 상태.
    • 이행(fulfilled): 연산이 성공적으로 완료됨.
    • 거부(rejected): 연산이 실패함.

     

     

    위에서 작성한 코드를 Promise로 다시 작성해보자.

     

    function printWord (word) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log(word);
    
          resolve();
        }, 1000);
      });
    }
    
    printWord('Hello')
      .then(() => printWord('World'))
      .then(() => printWord('I am'))
      .then(() => printWord('Julia!'));

     

    위 코드에서 printWord() 함수는 프로미스 객체를 리턴해준다.

    프로미스는 printWord() 함수가 실행되고 리턴되어 프로미스 객체가 정의되는 순간에 발동된다.

    만약에 성공적으로 비동기 작업이 처리되면 resolve 메소드가 호출되고 then 구문으로 넘어가게 된다.

     

     

    function multiply5 (number) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          let result = number * 5;
    
          if (result > 100) {
            reject(result);
          }
    
          console.log(result);
          resolve(result);
        }, 1000);
      });
    }
    
    multiply5(1)
      .then(n => multiply5(n))
      .then(n => multiply5(n))
      .then(n => multiply5(n))
      .catch(n => {
        throw new Error(`${n} is greater than 100`);
      });

     

    프로미스를 실행하고 얻은 결과값은 resolve나 reject 메소드를 통해

    다음 then 구문의 인자로 넘겨줄 수 있다.

     

    만약에 프로미스를 실행하는 과정에서 에러가 발생한다면

    rejected 상태가 되어 reject 메소드가 실행되고,

    catch 구문으로 넘어가게 된다.

     

     

     

    Promise.all()

    function multiply5 (number) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          let result = number * 5;
    
          console.log(result);
          resolve(result);
        }, 1000);
      });
    }
    
    Promise.all([
      multiply5(5),
      multiply5(10),
      multiply5(20)
    ]).then(result => {
      console.log('result', result);
    });

     

    만약에 여러가지 작업을 동시에 병렬로 처리하고 싶다면 Promise.all을 사용할 수 있다.

    내가 처리하고자 하는 프로미스들을 배열로 담아 Promise.all에 인자로 전달하면

    배열에 있는 모든 프로미스들이 거의 동시에 트리거된다.

     

    즉 위 코드를 실행하면 3초 후에 결과값을 받는 것이 아니고,

    1초 후에 결과값을 받을 수 있다.

    그리고 그 결과값은 배열에 넣은 프로미스 순서로 담긴다.

     

     

     

     

    Promise.race()

    function multiply2 (number) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          let result = number * 2;
    
          resolve(result);
          console.log('result', result);
        }, 1000 * number);
      });
    }
    
    Promise.race([
      multiply2(3),
      multiply2(2),
      multiply2(1)
    ]).then(result => {
      console.log('final result', result);
    });

     

    Promise.race()는 Promise.all()과 마찬가지로 배열과 같은 iterable한 객체를 매개변수로 받는다.

    그리고 배열에 담긴 프로미스를 병렬로 실행하는데,

    Promise.all()이 실행한 모든 프로미스들의 결과값을 배열로 받는 것과 달리

    Promise.race()는 가장 빨리 응답을 받은 결과값만 resolve한다.

     

    즉, 위 코드를 실행하면 모든 프로미스가 실행되어 콘솔 창에 2, 4, 6이 출력되지만

    가장 빠른 2만 then 구문으로 넘어간다.

     

     

    function multiply2 (number) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          let result = number * 2;
    
          reject(result);
          console.log('result', result);
        }, 1000 * number);
      });
    }
    
    Promise.race([
      multiply2(3),
      multiply2(2),
      multiply2(1)
    ]).then(result => {
      console.log('final result', result);
    }).catch(result => {
      console.log('final error', result);
      throw result;
    });

     

    에러가 발생할 때도 마찬가지이다.

    모든 프로미스가 실행되긴 하지만 가장 빠르게 응답한 에러만 catch 구문으로 넘어간다.

     

     

     


     

    Promise를 활용한 Ajax 비동기 처리 구현

    이제 Promise를 활용하여 좀 더 간편하게 Ajax 비동기 처리를 구현할 수 있다.

    아래 코드는 React와 axios 라이브러리를 활용하여 Twitch API로

    게임, 비디오 정보 등을 불러오는 연습 코드 중 일부이다.

     

     

    const TWITCH_URL = 'https://api.twitch.tv/helix';
    const CLIENT_ID = 'client-id';
    
    axios.defaults.headers.common['Client-ID'] = CLIENT_ID;
    
    function getTopGamePromise () {
      return axios({
        method: 'get',
        url: `${TWITCH_URL}/games/top?first=10`
      });
    }
    
    function getVidoePromise (gameId) {
      return axios({
        method: 'get',
        url: `${TWITCH_URL}/videos?game_id=${gameId}&first=20`
      });
    }
    
    function getClipPromise (gameId) {
      return axios({
        method: 'get',
        url: `${TWITCH_URL}/clips?game_id=${gameId}&first=5`
      });
    }
    
    function getVideoClipPromises (gameId) {
      return Promise.all([
        getVidoePromise(gameId),
        getClipPromise(gameId)
      ])
        .then(([ videos, clips ]) => ({
          videos, clips
        }));
    }

     

    먼저 Top Games 정보를 불러오고, Games 정보를 바탕으로 

    관련 비디오와 클립 정보를 불러오려고 한다.

     

     

    class App extends Component {
      constructor(props) {
        super(props);
    
        this.state = {
          gameData: [],
          videoData: [],
          userData: [],
          clipData: [],
          clickedGameIndex: 0,
          currentPageIndex: 0,
          pagination: null
        };
    
        this.handlePagination = this.handlePagination.bind(this);
      }
    
      componentDidMount() {
        getTopGamePromise()
          .then((response) => {
            this.setState({
              gameData: response.data.data,
              pagination: response.data.pagination.cursor
            });
          })
          .then(() => {
            return getVideoClipPromises(this.state.gameData[0].id);
          })
          .then(({ videos, clips }) => {
            this.setState({
              videoData: videos.data.data,
              clipData: clips.data.data,
            });
          });
      }
      
      render() {
      
      }
    }

     

    getTopGamePromise()를 실행하여 게임 데이터를 요청하고,

    요청에 대한 응답이 오면 state 정보를 바꿔준다.

     

    그리고 바로 video와 clip 정보를 가져오는 프로미스를

    Promise.all()로 병렬로 실행하고 두 정보에 대한 결과값을 모두 받으면 다시 state를 업데이트해준다.

     

     


    참고자료:

     

    Twitch API docs:

    https://dev.twitch.tv/docs/

     

    Twitch Developer Documentation

    Twitch Developer Documentation

    dev.twitch.tv

     

    반응형

    COMMENT