[Javascript] 프로미스(Promise) 사용법


Study/Javascript, Jquery, CSS  2020. 3. 26. 23:22

안녕하세요. 명월입니다.


이 글은 Javascript의 프로미스(Promise) 사용법에 대한 글입니다.


프로미스(Promise)란 Javscript의 단점인 무한 콜백의 단점을 해소하기 위해 만들어진 일종의 패턴이라고 할 수 있습니다.

제가 이전에 callback과 타이머 함수(setTimeout, setInterval)에 대해 설명한 적이 있습니다.

링크 - [Javascript] 익명함수, 람다식, 클로저 그리고 callback (DOMContentLoadedd와 load이벤트 예제)

링크 - [Javascript] 타이머 함수(setTimeout, setInterval)


이게 제 기준에 설명을 하면 Javascript은 스크립트 언어이기 때문에 Java와 C#처럼 메모리 구조(stack, heap)를 생각하며 포인터의 개념이 없습니다.

즉, 모든 것은 값 참조에 의한 흐름이기 때문에 이벤트가 발생했을 때, 이 callback 구조로 가져갈 수 밖에 없습니다.

<!doctype html>
<html>
 <head>
  <title>Document</title>
 </head>
 <body>
  <!-- textarea 콘솔 출력을 위한 오브젝트 -->
  <textarea id="console" style="width:500px;height:300px;"></textarea>
  <script>
    // 메시지에 날짜 추가 함수
    function addDate(msg, cb) {
      // 콜백 함수가 있으면
      if(cb !== undefined && typeof cb === "function") {
        // 날짜 생성
        let date = new Date();
        // 오늘 날짜
        msg = date.getFullYear() + "/" +(date.getMonth() + 1) + "/" + date.getDate() + "    " + msg;
        // 콜백 호출
        cb.call(window, msg);
      }
    }
    // 개행 추가 함수
    function newLine(msg, cb) {
      // 콜백 함수가 있으면
      if(cb !== undefined && typeof cb === "function") {
        // 개행 추가
        msg += "\r\n";
        // 콜백 호출
        cb.call(window, msg);
      }
    }
    // 콘솔에 메시지 작성 함수
    function write(msg, cb) {
      // 콜백 함수가 있으면
      if(cb !== undefined && typeof cb === "function") {
        // 메시지 출력
        document.getElementById("console").value += msg;
        // 콜백 호출
        cb.call(window, msg);
      }
    }
    // 0부터 99까지
    for(let i=0;i<100;i++) {
      // setTimeout을 통해 이벤트 큐에 추가
      setTimeout(()=>{
        // 날짜 추가
        addDate(i, (msg)=> {
          // 개행 추가
          newLine(msg, (msg)=>{
            // textarea 작성
            write(msg, (msg)=> {
              // 콘솔에 출력
              console.log(msg);
            });
          });
        });
      });  
    }
  </script>
 </body>
</html>

위 예제를 보시면 setTimeout 함수 안에 콜백의 콜백의 콜백의 콜백을 넣었습니다. 사실 위 예제는 이렇게 복잡하게 작성할 필요는 없습니다. 엄청 간단하게 작성이 가능하나 콜백의 콜백의 예제를 표현하기 위해 일부러 복잡하게 만들었습니다.

저기에서 메시지에 무언가를 추가하고 싶으면 콜백 함수 하나 더 만들고 setTimeout에 콜백을 또 추가하면 됩니다.

왜 이게 이렇게 개발이 되냐면 setTimeout 함수 때문입니다. main의 싱글 스레드의 실행하는 타이밍과 setTimeout안의 소스가 실행하는 타이밍이 시간의 차이가 있습니다.

또 Javascript는 스크립트이기 때문에 객체의 포인터가 없어서 주소 참조 방식이 되지 않기 때문에 위 같은 현상이 발생하는 것입니다.

즉, 자바나 C#이면 return this를 이용해서 obj.addDate().newLine().write()... 식의 작성이 가능합니다만, javascript에서는 그게 안되네요.

이게 Javascript 개발자들이 이야기하는 콜백 지옥(callback hell)입니다.


이 콜백 지옥을 해결하는 방법으로 프로미스(Promise)가 있습니다.

<!doctype html>
<html>
 <head>
  <title>Document</title>
 </head>
 <body>
  <textarea id="console" style="width:500px;height:300px;"></textarea>
  <script>
    // 메시지에 날짜 추가 함수
    function addDate(msg) {
      // 날짜 생성
      let date = new Date();
      // 오늘 날짜
      return date.getFullYear() + "/" +(date.getMonth() + 1) + "/" + date.getDate() + "    " + msg;
    }
    // 개행 추가 함수
    function newLine(msg) {
      // 개행 추가
      return msg += "\r\n";
    }
    // 콘솔에 메시지 작성 함수
    function write(msg) {
      // 메시지 출력
      document.getElementById("console").value += msg;
      return msg;
    }
    // 0부터 99까지
    for(let i=0;i<100;i++) {
      // Promise 선언
      var p = new Promise((resolve, reject)=>{
        // setTimeout을 통해 이벤트 큐에 추가
        setTimeout(()=>{
          resolve(i);
        });
      });
      // 그리고 콜백을 포인터 참조 형태의 메소드 체인 패턴으로 프로그래밍이 가능합니다.
      p.then(addDate).then(newLine).then(write).then((msg)=>{ console.log(msg);});
    }
  </script>
 </body>
</html>

Promise를 사용하면 콜백 지옥에서 벗어나서 메소드 체인 패턴으로 프로그래밍이 가능합니다.

<!doctype html>
<html>
 <head>
  <title>Document</title>
 </head>
 <body>
  <script>
    // 함수 생성
    function call(is) {
      // Promise 생성
      return new Promise((resolve, reject)=>{
        setTimeout(()=>{
          if(is) {
            // resolve 호출
            resolve();
          } else {
            // reject 호출
            reject();
          }
        });
      });
    }
    // Promise의 then은 파라미터를 두개 가질 수 있다. 첫번째 파라미터는 resovle를 두번째 파라미터는 reject이다.
    // true이기 때문에 then의 첫번째 파라미터가 실행된다.
    call(true).then(()=>{ console.log("then"); }, 
                    ()=>{ console.log("catch"); })
    // 혹은 Promise의 catch를 생성해서 resolve와 reject를 구분한다.
    // 하지만 catch를 나누어서 하는게 더 좋다. 이유는 then의 내부에서 에러가 나면 catch에서 잡아주지만, 파라미터가 두개일 때는 잡아주지 않는다.
    // false이기 때문에 reject가 호출되어 catch가 실행된다.
    call(false).then(()=>{ console.log("then"); })
               .catch(()=>{ console.log("catch"); });
  </script>
 </body>
</html>

resolve는 then으로 처리되고 reject는 catch로 처리됩니다. 혹은 then의 파라미터를 두개를 사용해서 구분해서 사용할 수도 있습니다.

위의 catch를 사용하면 메소드 체인 패턴이 조금 헷갈릴 수도 있습니다. 그러나 then 내부에서 에러가 발생하게 되면 catch 함수에서 잡아주기 때문에 catch를 사용하는 게 유리합니다.

<!doctype html>
<html>
 <head>
  <title>Document</title>
 </head>
 <body>
  <script>
    // Promise 선언
    new Promise((resolve)=>{
        resolve(); 
      })
      .then(()=> {
        // 에러 발생
        throw new Error("error");
      }, ()=> {
        // 콘솔 출력
        console.log("catch");
      });
    // Promise 선언
    new Promise((resolve)=>{ 
        resolve(); 
      })
      .then(()=>{
        // 에러 발생
        throw new Error("error");
      })
      .catch(()=>{
        // 콘솔 출력
        console.log("catch");
      });
  </script>
 </body>
</html>

첫번째에 있는 throw는 콘솔에 에러로 표시가 됩니다. 이렇게 되면, 자바스크립트가 아마 멈추게 될 것입니다.

두번째에 있는 throw는 catch함수로 넘어가서 console.log가 작성되었습니다.


여기까지 Javascript의 프로미스(Promise) 사용법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.