ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 프라미스(Promise)
    Javascript 2020. 6. 9. 00:37

     

     

    0px

     

     

     

    Promise는 무엇인가?

    단어 그대로 약속(promise)입니다.

    어떤 약속이냐면 어떠한 작업이 모두 끝나면 알려주겠다는 약속입니다.

    데이터를 가져오려면 적절한 시간이 필요합니다. 만약에 거대한 데이터를 한 번에 받아오라는 요청을 한 뒤에

    곧바로 데이터를 이용하는 코드로 넘어간다면 완전한 데이터를 알 수 없을 것입니다. 이를 위해서 모든 데이터를

    받았다면 데이터를 넘겨주어 그 다음 작업을 할 수 있게 돕는 객체입니다.

     

    Promise 객체의 생성자 구문

    let promise = new Promise(function(resolve, reject) {
        // 실행문
    });

     

    Promise가 생성될때 실행문이 자동으로 실행되어 작업이 성공적으로 완료되었다면

    resolve 콜백을 호출하고, 오류가 발생한 경우에는 reject 콜백을 호출합니다.

     

    Promise 객체의 내부 속성

    state - pending(대기), fulfilled(이행/완료), rejected(실패)

    result - undefined, value(resolve), error(reject)

    new Promise() 메서드로 생성되면 대기(pending) 상태입니다.

    new Promise(function(resolve, reject) {
        // ...
    });

     

    resolve 콜백으로 value 결과 값을 보내면 이행(fulfilled) 상태가 됩니다.

    new Promise(function(resolve, reject) {
        let value = 'Hello'
        resolve(value);
    });

     

    reject를 호출하면 실패(rejected) 상태가 됩니다.

    new Promise(function(resolve, reject) {
        reject(new Error("failed"));
    });

     

    실행 프로그램은 작업을 수행한 다음에 promise 객체의 상태에 따라서 resolve를 호출하거나 reject 해야 합니다.

    상태 변경이 최종이며 단일 결과 또는 오류만 있습니다. 즉, 추가적인 resolve나 reject는 무시됩니다.

     

     


     

    promise의 state와 result에 어떻게 접근할 수 있을까요?

    state와 result는 promise 내부에 있기 때문에 직접 접근할 수 없고

    .then / .catch / .finally 메서드를 사용해야 합니다.

     

    .then

    promise.then(
      function(result) { /* promise의 resolve 값을 다룹니다 */ },
      function(error) { /* promise의 reject 값을 다룹니다 */ }
    );

    첫 번째 인수는 promise의 상태가 fulfilled가 되었을 때의 값을 받는 함수입니다.

    (만약에 성공된 결과 값만을 원한다면 첫 번째 인수만 제공해도 됩니다.)

    let promise = new Promise(function(resolve, reject) {
      setTimeout(() => resolve("done!"), 1000);
    });
    
    // resolve 메소드가 .then의 첫번째 함수를 실행합니다.
    promise.then(
      result => alert(result), // 1초뒤에 "done!"가 보여집니다.
      error => alert(error) // 실행되지 않습니다.
    );

    두 번째 인수는 promise의 상태가 rejected가 되었을 때 실행되는 함수입니다.

    let promise = new Promise(function(resolve, reject) {
      setTimeout(() => reject(new Error("Whoops!")), 1000);
    });
    
    // reject 메소드가 .then의 두번째 함수를 실행합니다.
    promise.then(
      result => alert(result), // 실행되지 않습니다.
      error => alert(error) // "Error: Whoops!"가 1초 뒤에 보여집니다.
    );

     

    .catch

    .then(null, F)과 같으며, 오류를 받습니다.

    let promise = new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error("Whoops!")), 1000);
    });
    
    // .catch(f)는 promise.then(null, f)과 같습니다.
    promise.catch(alert); // "Error: Whoops!"가 1초후에 보여집니다.

     

    .finally

    .then(F, F)과 같으며, 결과가 무엇이든 항상 실행됩니다.

    promise의 결과에 상관없기 때문에 일반적인 마무리 절차를 진행합니다.

    예를 들어 로딩 UI를 중지하는 등 클린업을 실행하는데 활용됩니다.

    new Promise((resolve, reject) => {
      /* 무언가 작업이 진행되고, resolve나 reject가 호출됩니다. */
    })
      // promise의 성공여부에 상관없이 finally가 실행됩니다.
      .finally(() => stop loading indicator)
      .then(result => show result, err => show error)

     

     


     

    Promise chaining

    promise는 비동기 함수가 완료된 후에 resolve 또는 reject가 실행되며, 함수의 결과를 받습니다.

    즉, promise는 비동기 함수가 완료된 다음의 작업을 설정하여 차례대로 실행할 수 있습니다.

     

    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;
    
    });

     

    promise.then이 promise를 반환하며 다음 .then으로 넘어가고,

    promise를 받은 핸들러 함수가 값을 반환하면 결과는 promise로 반환되고,

    다음 .then으로 넘어가는 과정을 반복할 수 있습니다.

     

     

    다음과 같은 코드는 chaining이 아닙니다.

    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;
    });

    하나의 promise에 대한 결과를 순차적으로 전달하지 않고 각각 독립적으로 처리합니다. 


     

    return Promise

    chaining 중간에 promise를 반환하여 비동기 함수를 실행하는 비동기 작업 체인이 구축됩니다.

    new Promise(function(resolve, reject) {
    
      setTimeout(() => resolve(1), 1000);
    
    }).then(function(result) {
    
      alert(result); // 1
    
      // promise가 반환되며 다음 .then으로 넘어가기전에 비동기 함수가 실행됩니다.
      return new Promise((resolve, reject) => { 
        setTimeout(() => resolve(result * 2), 1000); 
      });
    
    }).then(function(result) { 
    
      alert(result); // 2
    
      // promise 반환
      return new Promise((resolve, reject) => {
        setTimeout(() => resolve(result * 2), 1000);
      });
    
    }).then(function(result) {
    
      alert(result); // 4
    
    });

    만약에 .then이 promise를 반환한다면, 나머지 체인들은 생성된 promise가 처리될 때까지 대기합니다.

    처리가 완료되면 생성된 promise의 결과가 다음 체인으로 전달됩니다.

     


     

     

     

    Error handling

    promise가 거부되면 흐름상 가장 가까운 rejection 핸들러로 넘어갑니다.

    가장 가깝다는 말은 .catch가 바로 나오지 않아도 된다는 의미입니다.

    그렇기 때문에 체인의 마지막에 .catch를 붙여서 간단히 에러 처리를 할 수 있습니다.

    fetch('https://존재하지 않는 주소')
      .then(response => response.json())
      .then(user => fetch('https://존재하는 주소')
      .then(response => response.json())
      .catch(err => alert(err)) // TypeError: failed to fetch

    네트워크 문제, 잘못된 형식의 JSON 등으로 인해 promise가 거부될때 .catch에서 에러를 잡고,

    정상적인 경우라면 .catch로 넘어가지 않습니다.

     

     

    암시적 try...catch

    promise 실행자와 핸들러 주위에는 '보이지 않는 try...catch'가 있어서 예외를 잡고 reject처럼 다룹니다.

    new Promise((resolve, reject) => {
      throw new Error("에러 발생!"); // throw뿐 아니라 모든 종류의 에러가 처리됩니다.
    }).catch(alert); // Error: 에러 발생!
    new Promise((resolve, reject) => {
      reject(new Error("에러 발생!"));
    }).catch(alert); // Error: 에러 발생!

     

     

    다시 던지기

    .catch 뒤에 .then을 붙여서 에러를 처리한 후에 다른 실행을 이어갈 수 있습니다. 

    new Promise((resolve, reject) => {
      throw new Error("에러 발생!");
    }).catch(function(error) {
      alert("에러가 잘 처리되었습니다. 정상적으로 실행이 이어집니다.");
    }).then(() => alert("다음 핸들러가 실행됩니다."));

    .catch를 활용하여 처리할 수 없는 에러를 다시 throw 하여 다른 .catch로 잡습니다.

    // 실행 순서: catch -> catch -> then
    new Promise((resolve, reject) => {
    
      throw new Error("에러 발생!");
    
    }).catch(function(error) { // (*)
    
      if (error instanceof URIError) {
        // 에러 처리
      } else {
        alert("처리할 수 없는 에러");
    
        throw error; // 에러 다시 던지기
      }
    
    }).then(function() {
      /* 여기는 실행되지 않습니다. */
    }).catch(error => { // (**)
    
      alert(`알 수 없는 에러가 발생함: ${error}`);
      // 반환값이 없음 => 실행이 계속됨
    
    });

     

     

    처리되지 못한 reject

    에러를 처리하지 못하면 어떤 일이 생길까요?

    에러가 발생하면 프라미스가 rejected 상태가 되고, rejection 핸들러로 넘어가는데 없다면 에러가 갇힙니다.

    처리하지 못한 에러가 남는다면 전역 에러가 발생하여 실무에서 일어난다면 끔찍할 것입니다.

    이를 unhandlerrejection 이벤트로 해결할 수 있습니다.

    window.addEventListener('unhandlerrejection', function(event) {
      // 이벤트엔 두 개의 특별 프로퍼티가 있습니다.
      alert(event.promise); // [object Promise] - 에러를 생성하는 프라미스
      alert(event.promise); // Error: 에러 발생! - 처리하지 못한 에러 객체
    });
    
    new Promise(function() {
      throw new Error("에러 발생!");
    }); // 에러 처리 핸들러, catch가 없음

     


     

     

    Promise API

     

    Promise.all

    여러 개의 promise를 동시에 실행시키고 모든 promise가 준비되었을 때 새로운 promise를 반환합니다.

    반환되는 result는 promise를 요소로 가진 배열이 됩니다. 배열의 순서는 전달되는 promise의 순서입니다.

    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(alert); // 프라미스 전체가 처리되면 1, 2, 3이 반환됩니다. 각 프라미스는 배열을 구성하는 요소가 됩니다.

    Promise.all에 전달되는 promise 중 하나라도 rejected가 되면 Promise.all은 에러와 함께 rejected 됩니다.

    Promise.all([
      new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
      new Promise((resolve, reject) => setTimeout(() => reject(new Error("에러 발생!")), 2000)),
      new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
    ]).catch(alert); // Error: 에러 발생!

    2초 뒤에 두 번째 promise가 rejected 되며 Promise.all 전체가 rejected되고, .catch가 실행됩니다.

    rejected 에러는 Promise.all 전체의 결과가 됩니다.

     

     

     

    Promise.allSettled

    하나라도 rejected 되면 전체가 rejected 되는 Promise.all과 달리 Promise.allSettled는

    모든 promise가 처리될 때까지 기다리고 각각의 결과를 요소로 갖는 배열을 result로 가집니다.

    let urls = [
      'https://api.github.com/users/iliakan',
      'https://api.github.com/users/remy',
      'https://no-such-url'
    ];
    
    Promise.allSettled(urls.map(url => fetch(url)))
      .then(results => { // (*)
        results.forEach((result, num) => {
          if (result.status == "fulfilled") {
            alert(`${urls[num]}: ${result.value.status}`);
          }
          if (result.status == "rejected") {
            alert(`${urls[num]}: ${result.reason}`);
          }
        });
      });

     

    위의 코드에서 반환되는 results는 다음과 같이 각각의 promise 상태와 값 또는 에러를 받습니다.

    [
      {status: 'fulfilled', value: Response},
      {status: 'fulfilled', value: Response},
      {status: 'rejected', reason: TypeError: Failed to fetch}
    ]

     

     

    Promise.race

    가장 먼저 처리되는 promise의 결과를 반환합니다.

    레이스의 승자가 나타나는 순간 다른 promise의 결과나 에러를 무시합니다.

    Promise.race([
      new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
      new Promise((resolve, reject) => setTimeout(() => reject(new Error("에러 발생!")), 2000)),
      new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
    ]).then(alert); // 1

     


     

     

     

    async, await

    promise를 좀 더 쉽게 이해하고 사용할 수 있도록 돕는 문법입니다.

     

     

    async function

    함수 앞에 async를 붙이면 함수가 항상 promise를 반환합니다.

    async function asyncFunc() {
      return 'async';
    }
    
    asyncFunc().then(alert); // 'async'

    async function에서만 동작하는 await라는 특별한 문법이 있습니다.

     

    await

    자바 스크립트는 await 키워드를 만나면 promise가 처리될 때까지 기다리고 결과를 반환합니다.

    promise.then보다 가독성이 좋고, 간편하게 promise의 result 값을 얻을 수 있습니다.

    주의할 사항은 오직 async function 안에서만 사용할 수 있다는 점입니다.

    async function asyncFunc() {
    
      let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve("완료!"), 1000)
      });
    
      let result = await promise; // 프라미스가 이행될 때까지 기다립니다.
    
      alert(result); // "완료!"
    }
    
    asyncFunc();

     

     

    Error handling

    await promise가 정상적으로 fulfilled 된다면 result에 값을 반환하고,

    await pomise가 rejected 된다면 마치 throw new Error를 작성하듯이 에러가 던져집니다.

    그러므로 try...catch로 에러를 잡을 수 있습니다.

    async function asyncFunc() {
    
      try {
        let response = await fetch('http://존재하지 않는 주소');
        let user = await response.json();
      } catch(err) {
        // fetch와 response.json의 에러를 모두 잡습니다.
        alert(err);
      }
    }
    
    asyncFunc();

    만약에 try...catch가 없다면 async function이 반환하는 promise는 rejected 상태가 되어

    .catch로 rejected된 promise를 처리합니다.

    async function asyncFunc() {
      let response = await fetch('http://존재하지 않는 주소');
    }
    
    // asyncFunc()는 rejected 상태의 promise입니다.
    asyncFunc().catch(alert); // TypeError: failed to fetch

     

     

    async/await를 사용하면

    await가 pending을 처리하기 때문에 .then이 거의 필요하지 않고,

    에러를 .catch 대신 try...catch를 사용하여 잡을 수 있는 점이 편합니다.

     

    그러나 async function 밖에서 await를 사용하지 못하기 때문에

    오류를 잡기 위해서는 try...catch를 감싸서 오류를 잡아야 합니다.

     


     

     

     

     

     

     

     

     

     

    'Javascript' 카테고리의 다른 글

    GoF Pattern  (0) 2020.09.26
    값과 참조 (Value & Reference)  (0) 2020.05.16
    (loading)class  (0) 2020.05.10
    (loading)Prototype Chain  (0) 2020.05.08
    실행 컨텍스트 (execution context)  (0) 2020.04.30

    댓글

Designed by CHANUL