본문 바로가기

JavaScript

비동기처리 - Promise

# 콜백함수를 통한 비동기처리의 문제점

ES6에 Promise가 도입되어 지금처럼 널리 사용되기 이전에는 주로 콜백함수를 다른 함수의 인자로 넘겨서 비동기처리를 했었다.

2023.12.23 - [JavaScript] - 비동기함수의 순서를 제어하는 방법 1. Callback함수

 

비동기함수의 순서를 제어하는 방법 1. Callback함수

# 콜백함수란? 2023.12.23 - [JavaScript] - callback함수란? callback함수란? # 함수란? - 어떤 특정한 일을 하는 코드의 묶음 - 다양하고 의미있는 일을 하기 위해 매개변수(인자)를 전달받을 수 있다. - 여러

lion284.tistory.com

 

단순 코드를 작성할 때는 위와 같이 전통적인 방식으로 콜백함수를 통해 비동기처리를 해도 큰 문제가 발생하지 않았다.

하지만, 콜백함수를 중첩해서 연쇄적으로 호출해야하는 복잡한 코드의 경우, 계속되는 들여쓰기 때문에 코드 가독성이 현저하게 떨어진다. (콜백지옥..)

 

자바스크립트 개발자들 사이에서 소위 '콜백지옥'이라고 불리는 이 문제를 해결하기 위해 여러가지 방법들이 논의되었고, 그 중 하나가 "Promise".

 

 

# Promise의 개념

Promise는 현재에는 당장 얻을 수는 없지만(데이터를 얻는데까지 지연시간이 발생하는 경우), 가까운 미래에는 얻을 수 있는 데이터에 접근하기 위한 방법을 제공.

I/O나 network를 통해서 데이터를 얻는 경우가 대표적.

// callback함수 이용
findUserAndCallBack(1, function (user) {
  console.log("user:", user); //비동기함수 실행 후, 실행될 코드
});

function findUserAndCallBack(id, cb) {
  setTimeout(function () {
    console.log("waited 0.1 sec.");
    const user = {
      id: id,
      name: "User" + id,
      email: id + "@test.com",
    };
    cb(user);
  }, 100);
}

비동기함수 다음에 실행될 로직을 callback함수에 넣는 방법이다.

 

 

callback함수를 이용한 위 코드를 Promise를 이용하여 아래와 같이 재작성할 수 있다.

// promise 이용
findUserAndCallBack(1).then(function (user) {
  console.log("user:", user);
});

function findUserAndCallBack(id) {
  return new Promise(function (resolve) {
     setTimeout(function () {
        console.log("waited 0.1 sec.");
        const user = {
         id: id,
         name: "User" + id,
         email: id + "@test.com",
       };
       resolve(user);
    }, 100);
  })
}
waited 0.1 sec.
user: {id: 1, name: "User1", email: "1@test.com"}

콜백함수를 인자로 넘기는 대신에 Promise객체를 생성하여 리턴.

호출부에서는 리턴받은 Promise 객체(findUserAndCallBack(1))에 then()메서드를 호출하여 결과값을 가지고 실행할 로직을 넘겨주고 있다.

 

 

## 콜백함수와의 가장 큰 차이점

함수를 호출하면 'Promise 타입의 결과값'이 return되고, 이 결과값을 가지고 다음에 수행할 작업을 진행한다는 것.

따라서 기존 스타일보다 비동기처리임에도 불구하고, 마치 동기 처리코드처럼 코드가 읽히기 때문에 좀 더 '직관적'으로 느껴지게 된다.

(비동기방식으로 처리된 Promise 객체 → 비동기 처리로직이 종료된 후, then메서드의 콜백함수 실행)

 

 

 

# Promise 생성방법

Promise 객체는 new 키워드생성자를 통해서 생성할 수 있는데, 이 생성자는 함수를 인자로 받는다.

이 함수인자는 resolvereject라는 2개의 함수형 파라미터를 가진다.

아래와 같은 모습으로 Promise 객체를 생성하여 변수에 할당할 수 있다.

const promise = new Promise(function(resolve, reject) {...});

 

 

실제로는 위 코드처럼 Promise객체를 변수에 할당하기보다는, 어떤 함수의 return값으로 바로 사용되는 경우가 많고, ES6의 화살표함수 키워드를 많이 사용한다.

function returnPromise(){
  return new Promise((resolve, reject) => { ... });
}

 

생성자의 인자로 넘어가는 함수 인자의 바디에는 resolve()나 reject()함수를 정상처리, 예외 발생 여부에 따라 적절히 호출해줘야한다.

일반적으로

- resolve()함수의 인자로는 미래 시점에 얻게될 결과를 넘겨주고,

- reject() 함수의 인자로는 미래 시점에 발생할 예외를 넘겨준다.

 

 

예를 들어, 나눗셈 함수를 Promise를 리턴하도록 구현해보자.

(나눗셈을 비동기처리할 이유는 없지만 간단한 예시를 위해..)

function divide(numA, numB) {
  return new Promise((resolve, reject) => {
   if(numB === 0) reject(new Error('Unable to divide by 0.'));
   else resolve(numA / numB);
  });
}

 

 

정상적인 인자를 넘겨 divide()함수를 호출하여 Promise객체를 얻은 후, 결과값을 출력해보자.

divide(8, 2)
  .then((result) => console.log("성공:", result))
  .catch((error) => console.log("실패:", error));
성공: 4

정상적인 인자를 넘긴 경우, then() 메서드가 호출되었다.

 

 

이번에는 비정상적인 인자를 넘겨보자.

divide(8, 0)
  .then((result) => console.log("성공:", result))
  .catch((error) => console.log("실패:", error));
실패: Error: Unable to divide by 0.
    at <anonymous>:3:26
    at new Promise (<anonymous>)
    at divide (<anonymous>:2:10)
    at <anonymous>:1:1

비정상적인 인자를 넘긴 경우, catch() 메서드가 호출되었다.

 

 

 

# Promise 사용 방법

실제 코딩을 할 때는 위와 같이 Promise를 직접 생성해서 return해주는 코드보다는, 어떤 라이브러리의 함수를 호출해서 return받은 Promise 객체를 사용하는 경우가 더 많다.

REST API를 호출할 때 사용되는 브라우저 내장함수인 fetch()가 대표적.

(NodeJS 런타임에서는 node-fetch 모듈을 설치해야 사용 가능하다.)

 

fetch() 함수는 API의 URL을 인자로 받고, 미래 시점에 얻게될 API 호출 결과를 "Promise 객체"로 리턴.

network latency (네트워크 시간지연)때문에 바로 결과값을 얻을 수 없는 상황이므로, 위에서 설명한 Promise 사용 목적에 정확히 부합한다.

 

- Promise 객체의 then() 메서드는 결과값을 가지고 수행할 로직을 담은 콜백함수를 인자로 받는다.

- catch() 메서드는 예외처리 로직을 담은 콜백함수를 인자로 받는다.

 

fetch() 함수를 이용하여 어떤 서비스의 API를 호출 후, 정상 응답 결과를 출력해보자.

fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then((response) => console.log("response:", response))
  .catch((error) => console.log("error:", error));
response: Response {type: "cors", url: "https://jsonplaceholder.typicode.com/posts/1", redirected: false, status: 200, ok: true, …}

 

인터넷 상에 유효한 URL을 fetch() 함수의 인자로 넘겼기 때문에, 예외가 발생하지 않고 then()인자로 넘긴 콜백함수가 호출되어 상태 코드 200의 응답이 출력되었다.

 

이번에는 fetch()함수의 인자를 넘기지 말아보자.

fetch()
  .then((response) => console.log("response:", response))
  .catch((error) => console.log("error:", error));
error: TypeError: Failed to execute 'fetch' on 'Window': 1 argument required, but only 0 present.
    at main-sha512-G7qgGx8Wefk5JskAfRw2DfBPNPQTxDC23DcZ+KQTmNoSr2S6pZ3IJgYs1ThvLvvH7uI_KhycDx_FIDNlu5KhOw==.bundle.js:9070
    at <anonymous>:1:1

 

이번에는 catch() 메서드의 인자로 넘긴 콜백함수가 호출되어 에러 정보가 출력되었다.

 

 

이와 같이 Promise는 then()과 catch() 메서드를 통해 동기처리 코드에서 사용하던 try-catch 블록과 유사한 방법으로 비동기 처리 코드를 작성할 수 있다.

 

 

 

# Promise의 메서드 체이닝(Method chaining)

then()과 catch()메서드는 또 다른 Promise 객체를 리턴한다.

그리고 이 Promise 객체는 인자로 넘긴 콜백 함수의 리턴값을 다시 then()과 catch() 메서드를 통해 접근할 수 있도록 해준다.

다시 말하면 then()과 catch() 메서드는 마치 사슬처럼 계속 연결하여 연쇄적으로 호출할 수 있다.

 

예를 들어, fetch() 메서드 사용 예제에서 단순히 응답 결과가 아닌, json형태로 출력하고 싶은 경우에는 then() 메서드를 추가로 연결해주면 된다.

fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then((response) => response.json())
  .then((post) => console.log("post:", post))//response.json()값이 post로 넘어옴
  .catch((error) => console.log("error:", error));
post: {userId: 1, id: 1, title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', body: 'quia et suscipit\nsuscipit recusandae consequuntur …strum rerum est autem sunt rem eveniet architecto'}

 

 

 

또 다른 예로 userId 1을 가진 유저의 데이터가 필요한 경우, 다음과 같이 추가 메서드 체이닝을 할 수 있다.

fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then((response) => response.json())
  .then((post) => post.userId) //post객체에서 userId만 추출하여 return
  .then((userId) => "https://jsonplaceholder.typicode.com/users/" + userId) //위 post.userId가 post로 넘어옴
  .then((url) => fetch(url))
  .then((post) => console.log("user:", user))
  .catch((error) => console.log("error:", error));
user: {id: 1, name: "Leanne Graham", username: "Bret", email: "Sincere@april.biz", address: {…}, …}

then()과 catch()의 인자로 넘긴 콜백함수는 3,4번째 줄처럼 Promise객체가 아닌 값이 return하든, 5번째 줄처럼 Promise객체를 리턴하든 크게 상관이 없다.

 

자바스크립트에서 then()함수는 인자로 넘어온 콜백함수가 무엇을 반환하는지에 따라 다르게 작동한다.

- then()함수의 인자로 넘어온 콜백함수가 어떤 값을 'return'할 경우 : 그 값이 그대로 그 다음 then()함수로 넘긴 콜백함수의 인자로 전달된다.

- then()함수의 인자로 넘어온 콜백함수가 어떤 값을 'resolve'할 경우 (즉, Promise객체를 반환하면) : Promise 객체가 비동기로 제공하는 값이 그 다음 then()함수로 넘긴 콜백함수의 인자로 전달된다.

function returnArg(arg) {
  return `${arg} in returnArg 👉`;
}

function resolveArg(arg) {
  return new Promise((resolve) => setTimeout(resolve(`${arg} in resolveArg 👉`), 100));
}

resolveArg("시작 👉")
  .then((arg) => returnArg(arg))    // arg: "시작 👉 in resolveArg 👉"
  .then((arg) => returnArg(arg))    // arg: "시작 👉 in resolveArg 👉 in returnArg 👉"
  .then((arg) => console.log(arg)); // arg: "시작 👉 in resolveArg 👉 in returnArg 👉 in returnArg 👉"


resolveArg("시작 👉")
  .then((arg) => resolveArg(arg))   // arg: "시작 👉 in resolveArg 👉"
  .then((arg) => resolveArg(arg))   // arg: "시작 👉 in resolveArg 👉 in resolveArg 👉"
  .then((arg) => console.log(arg)); // arg: "시작 👉 in resolveArg 👉 in resolveArg 👉 in resolveArg 👉"

 

 

# 마치면서

Promise를 사용하면서 자연스럽게 발생하는 이러한 코딩 스타일은 매우 흔하게 볼 수 있으며, 자바스크립트 개발자들 사이에서도 호불호가 갈리기도 한다.

 

최근 이러한 Promise를 이용해서 계속해서 메서드 체이닝하는 코딩 스타일은 자바스크립트의 async/await 키워드를 사용하는 방식으로 대체되고 있는 추세이다.

 

 

* 참고글 : https://www.daleseo.com/js-async-promise/

 

[자바스크립트] 비동기 처리 2부 - Promise

Engineering Blog by Dale Seo

www.daleseo.com