ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Promise부터 정리하고 가자!
    JavaScript 2022. 2. 16. 18:07

    Promise에 대해 제대로 알기 이전에 async와 await부터 따라 쓰기 시작해서

    도대체 어떤 상황에 async와 await을 쓰는지 전혀 감을 잡지 못했습니다.

     

    추천드리는 학습 방법은 우선 가볍게 어떤 순서로 왜 이런 것들이 나온 건지 훑어보시고

    그 후에 Promise 핵심을 어느 정도 외우신 후에 드림 코딩을 한번 보시고

    예제들을 직접 치고 실행해서 이해 안 되는 부분에서는 스탑 포인트 지정해서 직접 어떤 식으로 동작하는 건지 확인하시는 것을 추천드립니다. (VS Code 사용 중이시면 기본적인 html 만드셔서 js 파일 script에 연동하신 후에 "Open with Live Server"를 통해서 브라우저로 여신 후에 F12 누르셔서 console이나 source에서 보시는 방법도 있으니 참고 부탁드려요)

     

    그래서 asyncawait이 왜 등장했는지부터 알고 싶어서 정리를 하려다 보니

    먼저 Promise를 꼭 알고 가야겠다 싶어서 오늘은 Promise가 왜 등장했고 어떤 것인지 알아보고자 합니다.

     

    Callback hell  =>  Promise  =>  Promise Chaning  =>  Async & Await

    제가 알기로는 기존 함수들의 단점을 보완하고자 생긴 것으로 알고 있습니다.

     

    callback 지옥에 대해서만 짧게 보고 가겠습니다.

     

    Callback 지옥이란..?

    콜백 함수를 익명 함수로 전달하는 과정이 반복되면서 들여 쓰기가 많이 사용되어 가독성이 떨어지는 것을 말하고

    이로 인해 코드 수정도 어렵습니다.

     

    처음에는 콜백 함수란 게 따로 있는 건가 싶었습니다.

    그런데 그게 아니고 말 그대로 나중에 호출되는 함수를 의미하는 것이었습니다.

    그 대표적인 예가 setTimeOut, addEventListner 등입니다.

     

    이런 콜백 지옥을 해결하기 위해 나온 것이 Promise!

     

    # Promise 핵심

    • Javascript에 내장되어 있는 object(객체)입니다.
    • 비동기적인 것 수행할 때 콜백 함수 대신에 사용 가능합니다.
    • 시간이 소요될 것으로 예상되는 작업에 사용 (ex - 네트워크, 파일 읽기 등)
    • State (상태) : pending(실행 전 대기 상태) / fulfilled (실행 완료) / rejected (실행 불가)
    • Producer - resolve(정상적으로 처리된 경우) / reject(에러 발생할 경우)로 처리
      Consumer - then(resolve로 처리된 경우) / catch(reject 된 정보 처리) / finally(resolve나 reject 결과와 상관없이 실행)

     

    1. Promise를 쓴다는 것은 Promise 객체를 리턴하는 것과 같다.
    2. resolve에 넣은 value가 then에 전달되는 함수의 parameter가 된다.
    3. Promise를 리턴하는 함수를 실행하게 되면 리턴 값은 당연히 Promise 객체가 즉시 리턴된다.
    4. new Promise()를 생성하게 되면 즉시 Promise에 전달된 executor 함수가 자동적으로 실행된다.
    5. 마찬가지로, Promise를 리턴하는 함수를 실행하게 되면 Promise에 전달된 executor 함수가 즉시 실행된다.
    6. Promise chaining을 하게 되면 then()에서 리턴한 값을 그다음 then()에 전달되는 함수의 parameter가 된다.
    7. Promise chaining을 할 때 then은 값을 바로 리턴해도 되고 promise를 리턴해도 된다.
        값을 리턴할 때는 즉시 then에 전달한 함수가 즉시 실행된다.
        promise를 리턴할 때는 resolve 되면 그다음 then이 실행된다.
        promise의 then을 호출하게 되면 똑같은 Promise를 리턴하기 때문에
        그 리턴된 promise의 catch를 다시 호출할 수 있다.
    8. 동시에 여러 Promise를 실행시킬 때는 Promise.all()을 자주 사용한다.

     

    # 위의 1번과 4번 예제

    1. Promise를 쓴다는 것은 Promise 객체를 리턴하는 것과 같다.

    4. new Promise()를 생성하게 되면 즉시 Promise에 전달된 executor 함수가 자동적으로 실행된다.
     - 이런 간단한 예시를 통해서 1번과 4번 모두 확인해 볼 수 있습니다.

    const promise = new Promise((resolve, reject) => {
      console.log('ing');
    });
    
    console.log(promise);

     

    Promise를 쓴다는 것이 Promise 객체를 리턴하는 것과 같다의 예제

     

    Promise를 통해 executor 함수를 확인할 수 있습니다.

     

    # pending 된 상태의 Promise 객체를 확인하실 수 있습니다.

    따로 실행시키지 않았는데 ing가 찍힌 걸 확인하실 수 있습니다.

    이를 통해 4번을 직접 확인해 볼 수 있었습니다.

     

    원래 Promise는 시간이 소요될 것으로 예상되는 작업에 사용하기에 네트워크 작업에도 많이 쓰이는데 네트워크 작업에서 요청이 있을 경우 실행되어야 하는 곳에서 위에서처럼 작동하게 되면 굉장히 비효율적이니 참고 부탁드립니다~!

     

    2. resolve에 넣은 value가 then에 전달되는 함수의 parameter가 된다.

    const promise = new Promise((resolve, reject) => {
      console.log('ing');
      setTimeout(() => {
        resolve('completed');
      }, 2000);
    });
    
    console.log(promise);
    
    // 아래의 코드는 promise.then(console.log); 와 같다.
    promise.then((res) => {
      console.log(res);
    });

     

    # 에러 처리 예제

    7. Promise chaining을 할 때 then은 값을 바로 리턴해도 되고 promise를 리턴해도 된다.

    const promise = new Promise((resolve, reject) => {
      console.log('ing');
      setTimeout(() => {
        // resolve('completed');
        reject(new Error('no network'));
      }, 2000);
    });
    
    console.log(promise);
    
    promise
      .then((res) => {
        console.log(res);
      })
      .catch((rej) => {
        console.log(rej);
      });

    여기서는 then에서 promise를 리턴해서 catch가 잘 동작하는 것입니다.

     

    # Promise Chaining 예시

    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('Doctor Strange');
        // reject(new Error('Batman'));
      }, 1000);
    });
    console.log(promise);
    
    promise
      .then((res) => {
        const word = res + ' says ';
        return word;
      })
      .catch((rej) => {
        const newPromise = new Promise((resolve, reject) => {
          setTimeout(() => {
            reject(new Error(rej + ' says '));
          }, 2000);
        });
        return newPromise;
      })
      .then((res) => {
        const word = res + '"We are in the endgame now."';
        console.log(word);
        return word;
      })
      .catch((rej) => {
        const word = rej + '"We lost..."';
        console.log(word);
        return word;
      });

    무언가와 닮았다거나 비슷한 느낌을 받으시지 않았나요??

    맞습니다!

    콜백 지옥과 비슷한 모습입니다.

    그래서 이런 점을 보완하고자 async와 await이 나왔습니다.

     

    그전에 8번의 promise.all()만 보고 끝내겠습니다.

    const products = [
      {
        id: 0,
        title: '가방',
      },
      {
        id: 1,
        title: '옷',
      },
      {
        id: 2,
        title: '바지',
      },
      {
        id: 3,
        title: '신발',
      },
    ];
    
    //4. new Promise()를 하게 되면 즉시 Promise에 전달 된 executor 함수가 실행된다.
    const getProductTitle = (id) =>
      new Promise((resolve, reject) => {
        setTimeout(() => resolve(products[id].title), 1000);
      });
    
    (async () => {
      // 순차실행 예시
      let product_titles = [];
      const start = Date.now();
      for (let product of products) {
        const p_title = await getProductTitle(product.id);
        console.log(p_title);
        product_titles.push(p_title);
      }
      const end = Date.now();
      console.log(`순차실행: ${end - start}ms`, product_titles);
    
      // 동시실행 예시
      // map 함수는 기존의 배열을 callbackFunction에 의해 새 배열을 만드는 함수입니다.
      const start1 = Date.now();
      const title_promises = products.map(
        async (product) => await getProductTitle(product.id),
      );
      console.log(title_promises);
      product_titles = await Promise.all(title_promises);
      const end1 = Date.now();
      console.log(`동시실행: ${end1 - start1}ms`, product_titles);
    })();

    <실행 결과>

    저처럼 (async () => {}); 이런 형태의 코드를 처음 보셨다면 많이 당황하셨을 것 같아요.

    "익명 함수 또는 즉시 실행 함수"라는 건데 아래가 기본 형태이고 한 번만 사용하거나 즉시 실행되어야 할 경우에 쓰는 함수이니 참고하시면 좋을 것 같습니다. 

    (function() {
    
    }());

    # 위 코드의 순차실행

    다시 코드로 돌아가 보면 순차실행이라고 적어둔 것은 우리에게 보다 더 익숙한 코드의 형태를 하고 있습니다.

    순차실행 예시의 경우에는 for 문을 다 돌면서 각각의 title을 넣는데 await getProductTitle(product.id) 때문에 각 1초씩 시간이 걸려서 총 4초의 시간이 걸리고 있습니다.

    await을 쓰는 것은 "이 작업 마무리할 때까지 다음 작업들 기다려!"의 의미이고 그 작업이 1초씩 걸리고 총 4개여서 4초의 시간이 소요됨을 알 수 있습니다.

     

    # 위 코드의 동시실행 Promise.all()

    반면에 동시실행 예시의 경우에는 console.log(title_promises); 를 통해서 알 수 있는 것은 Promise 객체가 리턴된다는 것입니다. 위의 순차실행에서는 하나씩 실행하다 보니 총 4초의 시간이 걸렸지만 여기서는 Promise.all을 통해서 동시실행 했기에 약 1초 만에 끝나는 것을 알 수 있습니다.

     

    동시실행인 Promise.all은 어떤 경우에 써야 하는 걸까요?

    Promise.all 내부의 작업들은 순서를 제어할 수 없기에 그 순서가 중요하지 않은 경우에 사용해야 함을 명심해야 합니다.

     

     

    위의 예시들에서도 async와 await을 사용했었는데 다음 글에서 자세히 다뤄보겠습니다.

    개인적으로 async와 await이 더 재밌었고 활용하기에도 더 쉬웠던 것 같습니다.

    바로 다음에 다뤄보겠습니다!

     

     

    <참고한 자료>

    https://www.youtube.com/watch?v=JB_yU6Oe2eE&t=652s 

    https://velog.io/@yunkuk/%EB%8F%99%EA%B8%B0Sync-vs-%EB%B9%84%EB%8F%99%EA%B8%B0-Async

     

    동기(Sync) vs 비동기 (Async)

    Javascript 는 싱글쓰레드 이지만, 그래도 쓰레드의 개념을 알고 오면 좋다.보통 setTimeout 을 사용하여 비동기처리 예시를 드는데 처음 배웠을 때 실제 사용방법과 약간의 차이점이 있어서 잘 이해

    velog.io

     

    'JavaScript' 카테고리의 다른 글

    배열 - slice & splice  (0) 2022.03.22
    호이스팅이란  (0) 2022.03.05
    var, let, const는 어떤 것이며 무엇이 다르고 왜 나왔을까?  (0) 2022.02.13

    댓글

Designed by Tistory.