[JavaScript] 콜백함수와 프로미스

KIM DA MI·2023년 3월 19일
0

JavaScript

목록 보기
16/16
post-thumbnail

개발자는 코드 작성시 예측 가능한 코드를 작성하도록 노력해야 한다.
그러기 위해서는 비동기로 작동하는 코드를 제어할 수 있는 방법에 대해 잘 알고 있어야 한다.
여러 방법 중 하나는 Callback 함수를 활용하는 방법이다.

1. Callback (콜백함수)


  • Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있다.
    즉, 비동기를 동기화할 수 있다는 의미이다.

    const printString = (string, callback) => {
      setTimeout(function () {
        console.log(string);
        callback();
      }, Math.floor(Math.random() * 100) + 1);
    };
    
    const printAll = () => {
      printString('A', () => {
        printString('B', () => {
          printString('C', () => {});
        });
      });
    };
    
    printAll();
    
    console.log(
      `아래와 같이 Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있습니다!`
    );
    • 실행 결과



Callback Hell

  • Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있지만 코드가 길어질 수록 복잡해지고 가독성이 낮아지는 Callback Hell이 발생하는 단점이 있다.

    const printString = (string, callback) => {
      setTimeout(function () {
        console.log(string);
        callback();
      }, Math.floor(Math.random() * 100) + 1);
    };
    
    const printAll = () => {
      printString('A', () => {
        printString('B', () => {
          printString('C', () => {
            printString('D', () => {
              printString('E', () => {
                printString('F', () => {
                  printString('G', () => {
                    printString('H', () => {
                      printString('I', () => {
                        printString('J', () => {
                          printString('K', () => {
                            printString('L', () => {
                              printString('M', () => {
                                printString('N', () => {
                                  printString('O', () => {
                                    printString('P', () => {});
                                  });
                                });
                              });
                            });
                          });
                        });
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    };
    
    printAll();
    • 실행 결과



2. Promise (프로미스)


  • 비동기로 작동하는 코드를 제어할 수 있는 다른 방법은 Promise를 활용하는 방법이다.
    또한 Callback Hell을 방지하는 역할도 수행합니다.

new Promise

  • Promise는 class이기 때문에 new 키워드를 통해 Promise 객체를 생성한다.
    또한 Promise는 비동기 처리를 수행할 콜백 함수(executor)를 인수로 전달받는데 이 콜백 함수는 resolve, reject 함수를 인수로 전달받는다.

  • Promise 객체가 생성되면 콜백함수는 자동으로 실행되고 작성했던 코드들이 작동된다.
    코드가 정상적으로 처리가 되었다면 resolve 함수를 호출하고 에러가 발생했을 경우에는 reject 함수를 호출하면 된다.

    let promise = new Promise((resolve, reject) => {
        // 1. 정상적으로 처리되는 경우
        // resolve의 인자에 값을 전달할 수도 있다.
        resolve(value);
    
        // 2. 에러가 발생하는 경우
        // reject의 인자에 에러메세지를 전달할 수도 있다.
        reject(error);
    });

Promise 객체의 내부 프로퍼티

  • new Promise가 반환하는 Promise 객체는 state, result 내부 프로퍼티를 갖는다.
    하지만 직접 접근할 수 없고 .then, .catch, .finally의 메서드를 사용해야 접근이 가능하다.

State

  • 기본 상태는 pending(대기)이다.
  • 비동기 처리를 수행할 콜백 함수(executor)가
    성공적으로 작동했다면 fulfilled(이행)로 변경이 되고,
    에러가 발생했다면 rejected(거부)가 된다.

Result

  • 처음은 undefined이다.
  • 비동기 처리를 수행할 콜백 함수(executor)가
    성공적으로 작동하여 resolve(value)가 호출되면 value로,
    에러가 발생하여 reject(error)가 호출되면 error로 변합니다.

.Then

  • executor에 작성했던 코드들이 정상적으로 처리가 되었다면 resolve 함수를 호출하고 .then 메서드로 접근할 수 있다.

  • 또한 .then 안에서 리턴한 값이
    PromisePromise의 내부 프로퍼티 result를 다음 .then 의 콜백 함수의 인자로 받아오고,
    Promise가 아니라면 리턴한 값을 .then 의 콜백 함수의 인자로 받아올 수 있다.

    let promise = new Promise((resolve, reject) => {
        resolve("성공");
    });
    
    promise.then(value => {
        console.log(value);
        // "성공"
    })
    • 실행 결과

.Catch

  • executor에 작성했던 코드들이 에러가 발생했을 경우에는 reject 함수를 호출하고 .catch 메서드로 접근할 수 있다.

    let promise = new Promise(function(resolve, reject) {
        reject(new Error("에러"))
    });
    
    promise.catch(error => {
        console.log(error);
        // Error: 에러
    })
    • 실행 결과

.Finally

  • executor에 작성했던 코드들의 정상 처리 여부와 상관없이 .finally 메서드로 접근할 수 있다.

    let promise = new Promise(function(resolve, reject) {
        resolve("성공");
    });
    
    promise
    .then(value => {
        console.log(value);
        // "성공"
    })
    .catch(error => {
        console.log(error);
    })
    .finally(() => {
        console.log("성공이든 실패든 작동!");
        // "성공이든 실패든 작동!"
    })
    • 실행 결과



Promise chaining

  • Promise chaining가 필요하는 경우는 비동기 작업을 순차적으로 진행해야 하는 경우이다.

  • Promise chaining이 가능한 이유는 .then, .catch, .finally 의 메서드들은 Promise를 리턴하기 때문이다.

  • 따라서 .then을 통해 연결할 수 있고, 에러가 발생할 경우 .catch 로 처리하면 된다.

    let promise = new Promise(function(resolve, reject) {
        resolve('성공');
        ...
    });
    
    promise
      .then((value) => {
        console.log(value);
        return '성공';
      })
      .then((value) => {
        console.log(value);
        return '성공';
      })
      .then((value) => {
        console.log(value);
        return '성공';
      })
      .catch((error) => {
        console.log(error);
        return '실패';
      })
      .finally(() => {
        console.log('성공이든 실패든 작동!');
    • 실행 결과

예시 코드

  • Promise로 작성된 아래의 코드

    const printString = (string) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve();
          console.log(string);
        }, Math.floor(Math.random() * 100) + 1);
      });
    };
    
    const printAll = () => {
      printString('A')
        .then(() => {
          return printString('B');
        })
        .then(() => {
          return printString('C');
        });
    };
    
    printAll();
    
    console.log(
      `아래와 같이 Promise를 통해 비동기 코드의 순서를 제어할 수 있습니다!`
    );
    
    • 실행 결과



Promise.all() : 여러 개의 비동기 작업을 동시에 처리하고 싶을때 사용

const promiseOne = () => new Promise((resolve, reject) => setTimeout(() => resolve('1초'), 1000));
const promiseTwo = () => new Promise((resolve, reject) => setTimeout(() => resolve('2초'), 2000));
const promiseThree = () => new Promise((resolve, reject) => setTimeout(() => resolve('3초'), 3000));
  • Promise.all()은 여러 개의 비동기 작업을 동시에 처리하고 싶을때 사용한다.
    인자로는 배열을 받는다. 해당 배열에 있는 모든 Promise에서 executor 내 작성했던 코드들이 정상적으로 처리가 되었다면 결과를 배열에 저장해 새로운 Promise를 반환 해준다.

  • 앞서 배운 Promise chaining을 사용했을 경우는 코드들이 순차적으로 동작되기 때문에 총 6초의 시간이 걸리게 되고, 같은 코드가 중복되는 현상도 발생하게 된다.

    // 기존
    const result = [];
    promiseOne()
      .then(value => {
        result.push(value);
        return promiseTwo();
      })
      .then(value => {
        result.push(value);
        return promiseThree();
      })
      .then(value => {
        result.push(value);
       console.log(result);  
         // ['1초', '2초', '3초']
      })
    • 실행 결과 (6초 뒤)

  • Promise.all()은 비동기 작업들을 동시에 처리하기 때문에, 3초 안에 모든 작업이 종료된다.
    또한 Promise chaining로 작성한 코드보다 간결해진 것을 확인할 수 있다.
    // promise.all
    Promise.all([promiseOne(), promiseTwo(), promiseThree()])
      .then((value) => console.log(value))
      // ['1초', '2초', '3초']
      .catch((err) => console.log(err));
    • 실행 결과 (3초 뒤)

  • 추가적으로 Promise.all() 인자로 받는 배열에 있는 Promise중 하나라도 에러가 발생하게 되면 나머지 Promise state와 상관없이 즉시 종료된다.
    아래의 예시와 같이 1초 후에 에러가 발생하고 Error: 에러1이 반환된 후로는 더 이상 작동하지 않고 종료된다.
    Promise.all([
        new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러1'))), 1000),
        new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러2'))), 2000),
        new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러3'))), 3000),
    ])
        .then((value) => console.log(value))
      .catch((err) => console.log(err));
        // Error: 에러1
    • 실행 결과



Promise Hell

  • Promise를 통해 비동기 코드의 순서를 제어할 수 있지만 Callback 함수와 같이 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Promise Hell이 발생하는 단점이 있다.

    const printString = (string) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(string);
        }, Math.floor(Math.random() * 100) + 1);
      });
    };
    
    const printAll = () => {
      printString('A').then((value) => {
        console.log(value);
    
        printString('B').then((value) => {
          console.log(value);
    
          printString('C').then((value) => {
            console.log(value);
    
            printString('D').then((value) => {
              console.log(value);
    
              printString('E').then((value) => {
                console.log(value);
    
                printString('F').then((value) => {
                  console.log(value);
    
                  printString('G').then((value) => {
                    console.log(value);
    
                    printString('H').then((value) => {
                      console.log(value);
    
                      printString('I').then((value) => {
                        console.log(value);
    
                        printString('J').then((value) => {
                          console.log(value);
    
                          printString('K').then((value) => {
                            console.log(value);
    
                            printString('L').then((value) => {
                              console.log(value);
    
                              printString('M').then((value) => {
                                console.log(value);
    
                                printString('N').then((value) => {
                                  console.log(value);
    
                                  printString('O').then((value) => {
                                    console.log(value);
    
                                    printString('P').then((value) => {
                                      console.log(value);
                                    });
                                  });
                                });
                              });
                            });
                          });
                        });
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    };
    
    printAll();
    
    console.log(
      `아래와 같이 Promise를 통해 비동기 코드의 순서를 제어할 수 있지만 Callback 함수와 같이 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Promise Hell이 발생하는 단점이 있다.`
    );
    • 실행 결과



Async/Await

  • JavaScript는 ES8에서 async/await키워드를 제공하여 복잡한 Promise 코드를 간결하게 작성할 수 있게 되었다.

  • 함수 앞에 async 키워드를 사용하고 async 함수 내에서만 await 키워드를 사용하면 된다.
    이렇게 작성된 코드는 await 키워드가 작성된 코드가 동작하고 나서야 다음 순서의 코드가 동작하게 된다.

    // 함수 선언식
    async function funcDeclarations() {
        await 작성하고자 하는 코드
        ...
    }
    
    // 함수 표현식
    const funcExpression = async function () {
        await 작성하고자 하는 코드
        ...
    }
    
    // 화살표 함수
    const ArrowFunc = async () => {
        await 작성하고자 하는 코드
        ...
    }

예시 코드

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
      console.log(string);
    }, Math.floor(Math.random() * 100) + 1);
  });
};

const printAll = async () => {
  await printString('A');
  await printString('B');
  await printString('C');
};

printAll();

console.log(
  `Async/Await을 통해 Promise를 간결한 코드로 작성할 수 있게 되었습니다.`
);
  • 실행 결과

0개의 댓글