[CN] 비동기 함수 (2): Promise와 .then()

곽재훈·2024년 5월 11일
2
post-thumbnail

1. Promise

1) Promise의 등장.

callback을 활용한 비동기 함수를 제어하는 것은 윗 글과 같은 문제가 있었다.

이런 상황에서 callback hell의 문제를 해결하기 위해 Promise는 등장했다. 즉, Promise 문법은 callback 함수와 마찬가지로 비동기 함수의 실행 순서를 제어하기 위해 나왔다고 볼 수 있는 것이다.

그렇다면 Promise는 callback과 어떻게 다른 방식으로 비동기의 순서를 제어하는가?

기존의 callback 함수를 연속적으로 활용해서 비동기 작업을 제어하는 것이 어려웠던 이유중 하나는 callback 함수가 여러 번 연결될수록 함수의 실행 순서가 직관적으로 눈에 들어오지 않게 되기 때문이었다.

아래와 같은 콜백 함수를 파악하려면 위와 아래를 번갈아가며 확인하면서 코드를 확인해야만 한다.

  • 인터넷에 돌아다니는 흔한 callback hell 짤방

그런데 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;

});

출처: https://ko.javascript.info/promise-chaining

위의 코드가 프로미스 체이닝을 활용한 코드인데, 이전의 callback을 활용한 방식과 달리 코드의 실행 순서가 .then()이라는 단위를 바탕으로 해서 사람이 읽기 편한 방식과 순서대로 코드가 정리되는 것을 볼 수 있다.

아마도 저 맨 처음의 new Promise라는 인스턴스 생성자가 익숙하지 않을 수도 있을 것이다. 충분히 그럴 수 있는 것이, 우리가 실제로 Promise 객체를 사용할 때는 우리가 직접 new Promise라는 생성자를 생성해서 사용하기보다는 fetch()와 같은 API의 결괏값으로 Promise 객체를 받아서 사용하는 경우가 훨씬 많기 때문일거다.

어쨋든 Promisecallback hell의 문제를 해결하기 위해 도입되었다는 것을 기억하면 충분하다.

2) Promise 객체와 Promise의 난해함.

주소창에 https://jsonplaceholder.typicode.com/todos/1를 쳐보면 JSON 형식으로 데이터를 반환해주는 것을 볼 수 있다.

이제 이걸 코드 상에서 데이터로 받고 싶으면 fetch()같은 방법을 통해서 데이터를 요청할 수 있는데, 보통은

fetch("요청하고 싶은 주소")

의 형태로 구성된다.

그런데 fetch() 함수는 함수의 결괏값을 우리가 원하는 데이터의 형태가 아니라 Promise 객체의 형태로 반환한다.

나는 처음에 Promise 객체를 만났을 때 너무 당황했었다. 물론 내가 처음에 다른 사전지식 없이 Promise를 만났기 때문이기도 하지만 아마 대부분의 우리가 처음에 그런 순간을 겪지 않을까 싶다. 앞서 언급했던 것처럼 우리가 Promise 객체를 만나게 되는 첫 순간은 대부분 API 요청의 결과로 Promise 객체를 받는 경우였을 거고, 앞으로도 그럴 것 같으니까 말이다.

갑자기 수업에서 fetch() 함수를 써보라고 해서 썼는데, 함수의 반환값으로 내가 원하는 데이터가 들어온 게 아니라 웬 못생긴 Promise {<pending>}이라는 결괏값을 받았던 순간 말이다.

  • 나는 data를 출력했는데, 내 데이터는 어디가고 이런 못생긴 게 들어있는 거지??

웹 상에서 Promise 객체를 보면 이렇게 생겼는데,

  • 이건 뭘까...?

저 안에 있는 것들이 뭔지 궁금하다면 검색하면서 찾아보시길!

요약하자면 fetch()같은 어떤 비동기 함수들은 비동기 함수를 처리하는 방법으로 callbackPromise 중에서 Promise를 활용한다.
그래서 우리에게 함수 실행에 대한 결괏값으로 Promise 객체라는 것을 반환하는데, 이 Promise 객체를 우리가 원하는 방법으로 사용하기 위해서는 특별한 방법이 필요하다.

3) 프로미스 체이닝과 then()

const fetchData = () => {
  fetch("https://jsonplaceholder.typicode.com/todos/1")
    .then((response) => response.json())
    .then((data) => {
      console.log(data);
    });
};

fetchData();

일단 결론부터 말하자면, 우리가 원하는 결과를 얻기 위해서는 then()이라는 메서드를 사용해서 프로미스 체이닝이라는 것을 하면 된다. 아마도 이미 대부분이 써봤을지도 모른다.

근데 저 코드에서는 무슨 일이 일어나고 있는가?
then()을 쓰면 promise 객체data로 변하는가?
그리고 then()이 계속 이어지는 것은 무엇을 의미하는가?

여기서 하나의 예시를 들어볼까 한다.
혹시 중고 거래를 해 본 적이 있는가? 나는 애플 전자기기나 소니의 카메라를 좋아해서 전자기기를 자주 사고 파는 편인데, 중고 거래로 물건을 팔기 전날이면 택배를 보내기 전에 소중하게 물건을 닦고 포장해서 뽁뽁이로 감싸던 기억이 난다.
|
여기서 내가 팔고자 하는 카메라나 맥북은 data가 되고, 카메라나 맥북을 감싼 뽁뽁이나 택배 박스는 Promise 객체가 된다. 그리고 물건을 사고 파는 사람이 then() 메서드가 된다.
|
물건을 사고 파는 사람은 물건을 산 동안에는 자유롭게 사용할 수 있지만, 그걸 다시 팔고자 해서 포장한 다음 다른 사람에게 배송중인 중간에는 물건을 사용할 수가 없다.
|
그게 바로 then()이 다음 then()에게 Promise 객체를 전달하는 과정이다.

위의 코드를 조금 뜯어보자.

const fetchData = () => {
  fetch("https://jsonplaceholder.typicode.com/todos/1")
    .then((response) => response.json())
    .then((data) => {
      console.log(data);
    });
};

fetchData();

위의 함수는 fetch() 함수의 결괏값을 then()response로 넘겨준다는 뜻이므로 이렇게 쓸 수도 있다.

const fetchData = () => {
  const result = fetch("https://jsonplaceholder.typicode.com/todos/1");
  
    result.then((response) => response.json())
    .then((data) => {
      console.log(data);
    });
};

fetchData();

하나씩 뜯어보자. 어렵게 느껴진다면 당연하다. 천천히 하나씩 보자.
위에서 말했던 카메라를 사고 파는 과정을 예시로 들어보자.

const buyDSLRCamera = () => {
  const package = fetch("https://jsonplaceholder.typicode.com/todos/1");
  console.log(package);
};

buyDSLRCamera();

먼저 fetch()의 결괏값은 result라는 변수에 담겨있다. 무엇이 담겨있을까? 요청에 대한 데이터가 담겨있을까?

궁금하다면 직접 코드를 적고 실행해봐도 되는데, 아마도 Promise {<pending>}이라는 Promise 객체가 담겨있을 것이다.

Promise 객체는 우리가 그냥 쓸 수는 없는, 쉽게 말하면 포장이 뜯기지 않은 상태이다. 즉, 아까의 예시로 이어서 설명하자면 내가 새로 소니 카메라를 사서 물건을 받기는 했는데 아직 포장이 뜯기지 않은, 포장 박스에 담긴 상태인 것이다.

그리고 그 포장을 열어주는게 then() 이다.

const buyDSLRCamera = () => {
  const package = fetch("https://jsonplaceholder.typicode.com/todos/1");
  package.then((myCamera) => {
    console.log(myCamera);
  });
};

buyDSLRCamera();

packagethen()메서드를 써서 프로미스 체이닝을 해주면 Promise 객체 안에 담긴 내용물을 인자로 전달해주고 우리는 그것을 사용할 수 있다.

console.log()의 결과로 뭔가 알 수없는 것들이 쏟아져 나왔지만 당황할 필요 없다. 중요한 것은 우리가 포장을 뜯었다는 것! 우리는 지금 택배 박스를 열고나서 카메라가 담긴 상품 박스를 꺼낸 것이다!

이제 우리가 카메라를 잘 사용한 다음에 다른 사람에게 다시 팔기 위해서는, then()을 사용해서 넘겨줘야 한다.

const buyDSLRCamera = () => {
  const package = fetch("https://jsonplaceholder.typicode.com/todos/1");
  const anotherPackage = package.then((myCamera) => {
    return myCamera.json();
  });
};

buyDSLRCamera();

package.then((myCamera) => {...}) 메서드는 myCamera.json()의 값을 반환하고 있는데, 한 가지 기억해야 할 것은 then() 메서드는 항상 Promise 객체를 return 한다는 것이다.

즉 지금 anotherPackage라는 변수에 담긴 값은 우리가 방금 위에서 본 긴 데이터를 다시 박스로 포장한 Promise 객체가 된다.

const buyDSLRCamera = () => {
  const package = fetch("https://jsonplaceholder.typicode.com/todos/1");
  const anotherPackage = package.then((myCamera) => {
    return myCamera.json();
  });
  anotherPackage.then((camera) => {
    console.log(camera);
  });
};

buyDSLRCamera();

이걸 이제 다시 then()메서드를 통해 꺼내서 인자로 데이터를 받아오면 우리가 원하는대로 사용할 수 있게 되는 것이다.

즉, Promise 객체는 택배 박스이고 우리는 then()메서드를 통해서 택배 박스를 뜯은 동안에만, 다시 말하면 then() 메서드 안에서 작동하는 콜백 함수를 통해서만 Promise 객체를 다룰 수 있다는 것이다.

이를 변수에 저장하는 중간 과정을 거치지 않고, 함수의 반환값을 바로 다음 then()으로 넘겨주도록 작성하면 아래의 코드와 같이 된다.

const buyDSLRCamera = () => {
  const package = fetch("https://jsonplaceholder.typicode.com/todos/1")
    .then((myCamera) => myCamera.json())
    .then((camera) => {
      console.log(camera);
    });
};

buyDSLRCamera();

결국 이런 형태들이 우리가 주로 사용하게 되는

const fetchData = () => {
  fetch("https://jsonplaceholder.typicode.com/todos/1")
    .then((response) => response.json())
    .then((data) => {
      console.log(data);
    });
};

fetchData();

의 형태로 코드가 구성되게 되는 것이다.

그림으로 정리해보자면 이런 느낌이 될 것 같다.
결국 Promise객체then() 안에서만 활용할 수 있다는 것을 기억하면 될 것 같다.

원한다면 어떤 변수 안에 then()메서드의 return값인 Promise 객체를 저장할 순 있지만, 그것을 다시 사용하려면 다시 변수에 then() 메서드를 사용해야 하는 것이다.

Promise 객체 내부의 데이터 그 자체를 변수에 Promise 객체로 감싸지 않고 할당하는 것이 아예 불가능한 것은 아니지만, Promise 객체then()의 이런 제한은 변수에 데이터 그 자체를 할당하는 것을 어렵게 만든다는 단점이 있었다.
변수에 데이터를 할당하려고 하면 언제나 그렇듯이 Promise 객체가 들어가고, 이는 변수를 사용하려고 할 때 또 다른 then()을 붙여야 함을 의미하기 때문이다.
즉, 오직 then()의 내부에서만 데이터를 다룰 수 있었기 때문이다.

그리고 이런 불편한 상황을 타개하기 위해서

async, await 문법

이 등장하였다.


profile
개발하고 싶은 국문과 머시기

0개의 댓글