JS 시리즈 - Promise 편

MOON·2021년 6월 30일
0

JS 시리즈

목록 보기
1/2
post-thumbnail

🔆 Promise

보통 대부분의 사람들이 공부하다가 막히는 부분이 C에서는 포인터였을 것이고 JS에서는 Promise 부분이 아닐까 생각해봅니다.
(개인적인 생각 ... ㅎㅎ 제가 그랬어서)
그래서 이번에 한번 제대로 Promise에 대해서 공부하고 정리해보고자 합니다.

비동기

자바스크립트는 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 실행하는 특성을 가집니다. 만약 이러한 특성이 없다면 서버에 데이터를 요청한 경우 그 서버가 요청에 대한 응답을 언제 줄지도 모르는데 계속 기다리게 되는 상황이 연출될 것입니다.

setTimeout()

setTimeout()은 Web API중 하나입니다. 지정한 시간만큼 기다렸다가 로직을 실행해주는 API입니다.

console.log("첫번째 실행");

setTimeout(()=>{
  console.log("두번째 실행");
},5000)

console.log("세번째 실행");

위 코드를 작성하고 일반적으로는 다음과 같은 순서의 결과값을 예상합니다.

  • "첫번째 실행"
  • 5초 후 "두번째 실행"
  • "세번째 실행"

그러나 실제로는 다음과 같은 결과값을 보여줍니다.

  • "첫번째 실행"
  • "세번째 실행"
  • 5초 후 "두번째 실행"

이렇게 결과값이 나오는 이유도 바로 JS의 특성인 비동기 방식때문입니다.
코드는 위에서 아래로 진행되고 setTimeout()이라는 함수를 만나고 이 코드가 진행되는 5초를 기다렸다가 다음 코드가 진행되는 것이 아니고 바로 다음 코드로 넘어가게 됩니다.

프로미스가 없었을때의 비동기 처리 방식 해결 방식

콜백 함수

프로미스가 없었을때는 이러한 비동기 처리 방식의 문제점을 해결하기 위해 바로 콜백 함수라는 것을 사용했습니다.
그러나 이 콜백 함수는 비동기 처리 로직을 위해 연속해서 사용하다 보면 콜백 지옥이라는 문제를 맡게 됩니다. 아래의 코드를 봅시다.


function c(callback){
   setTimeout(()=>{
      callback();
   },1000)
}

c(()=>{
   // console.log("1000ms 후에 callback 함수가 실행됩니다.");
  c(()=>{
     // console.log("2000ms 후에 callback 함수가 실행됩니다.")
    c(()=>{
       console.log("3000ms 후에 callback 함수가 실행됩니다.")
    });
  });
});

3가지 과정에 대해서 모든 과정을 비동기로 처리해야 한다면 위 코드와 같이 콜백 안에 콜백을 계속 무는 형식으로 코딩이 됩니다.
일반적으로 코드는 위에서 아래로 단계적으로 진행됩니다. 그러나 이코드는 코드가 점점 안으로 들어가게 되고 이러한 코드 구조는 가독성을 떨어뜨리고 로직을 변경하기도 어렵게 됩니다.

프로미스의 등장

그래서 이러한 콜백지옥을 보고 싶지 않기 때문에 프로미스를 사용합니다.
프로미스는 비동기 처리에 사용되는 하나의 객체이며 ES6 부터 JS의 표준 내장 객체로 추가되었습니다.

프로미스를 사용하는 근본적 이유

주로 웹 서비스를 개발 할때 프로미스는 서버에서 받아온 데이터를 화면에 표시 할때 사용합니다. 일반적으로 클라이언트에서 서버에 데이터를 요청하면 서버가 데이터를 찾고 데이터를 보내주는데 시간이 걸립니다. 그러면 클라이언트는 데이터를 받아올때 까지 기다렸다가 다음 로직을 진행해야 하는데 그 전에 로직이 진행되면 오류가 발생하거나 빈화면이 뜨게 되는 겁니다. 이 문제를 해결하기 위한 방법 중 하나가 바로 프로미스 입니다.

예제를 통해 프로미스에 대해 알아보자

프로미스의 형태

생성자를 통해 프로미스 객체를 만들 수 있습니다.
생성자는 인자로 executor 라는 함수를 이용하고 executor 함수는 resolve,reject를 인자로 가집니다.

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

프로미스 처리 흐름

출처 : MDN

프로미스의 상태들

프로미스 객체의 상태에는 총 3가지의 상태가 존재합니다.
1. pending (대기) 상태
2. fulfilled (이행) 상태
3. rejected (거부) 상태

new Promise() 새로운 프로미스 객체가 생성되는 이 순간 프로미스는 pending (대기) 상태에 돌입합니다.

프로미스 객체에서 비동기 작업이 진행됬을때 성공적으로 완료된 경우에는 resolve 함수가 실행되고 실패한 경우에는 reject 함수가 실행됩니다.

new Promise((resolve,reject)=>{
   // pending 상태 
   setTimeout(()=>{
      resolve(); // 1초 후에 fulfilled 상태로 전환
      // reject(); // 1초 후에 rejected 상태로 전환 
   },1000)
});

then과 catch

프로미스 객체에는 then과 catch라는 함수가 존재합니다.

function p(){
   return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        resolve();
      },1000)
   });
}

p().then(()=>{
   console.log("1000ms 후에 성공적으로 fulfilled 상태가 되었습니다.");
}).catch(()=>{
   console.log("rejected 상태")
});

위 코드를 보면 p라는 함수는 실행시에 새로운 프로미스 객체를 생성합니다. 여기서 then과 catch가 있는데 then은 프로미스 객체가 resolve된 경우에 실행되고 catch는 프로미스 객체가 reject된 경우에 실행됩니다.

프로미스 객체의 인자 전달과 finally

executor의 resolve와 reject에 인자를 넣어 실행하게 되면
then과 catch의 callback 함수의 인자로 받을 수 있습니다.
일반적으로 catch의 인자로는 에러 객체를 전달합니다.

function p(ms){
   return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        resolve(ms);
        reject(new Error("reason"));
      },ms)
   });
}

p(1000).then((data)=>{
   console.log(data); // 1000 
}).catch((error)=>{
   console.log(error); // Error 객체 reason 
}).finally(()=>{ 
   console.log("end");
});

finally는 프로미스가 resolve되든 reject되든 그 후에 최종적으로 실행할 코드가 있다면 finally()를 설정하고 함수를 인자로 넣습니다.

Promise.resolve(value)

resolve가 실행될때 인자로 들어오는 value는 프로미스 객체일 수도있고 하나의 값일 수도 있습니다.

  1. value가 프로미스 객체인 경우에는 resolve된 then 메소드가 실행
Promise.resolve(new Promise((resolve,reject)=>{
   setTimeout(()=>{
      resolve("foo");
   },1000)
})).then((data)=>{
   console.log("프로미스 객체인 경우, resolve 된 결과를 받아서 then이 실행됩니다.",data);
})

1초 후에 결과값 출력
프로미스 객체인 경우 resolve된 결과를 받아서 then이 실행됩니다.foo

  1. value가 프로미스 객체가 아닌 경우에는 값을 인자로 보내면서 then 메서드 실행
Promise.resolve("bar").then((data)=>{
   console.log(data); // bar 출력 
});

프로미스 객체가 여러개 일때

Promise.all()
이는 Promise 객체들을 가지는 배열을 인자로 받아 모든 프로미스 객체들이 성공적으로 resolve되었을 때 then이 실행되고 data에는 각 프로미스 객체들이 resolve시 인자로 전달하는 데이터가 배열로 전달됩니다.

Promise.all([p(1000),p(2000),p(3000)])
  .then((data)=>{
     console.log(data); // [1000,2000,3000]
  });

Promise.race()
사용하는 방법은 동일하지만 race는 모든 프로미스 객체들 중 가장 빨리 resolve되었을때 then이 실행되며 data는 가장 빨리 resolve된 프로미스의 data가 전달됩니다.
위 코드를 race로 변경했다고 하면 then의 data에는 1000이 출력 될 것입니다.

콜백 지옥 해결책들

콜백 지옥이라는 것은 여러과정을 비동기로 처리 할때 나타나는데 코드가 점점 안으로 들어가 depth가 깊어져 가독성을 떨어 뜨립니다.

첫번째 해결책 then의 체이닝


function p(){
   return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        resolve();
      },1000)
   });
};

p()
  .then(()=>{
   return p();
})
  .then(()=>p())
  .then(p)
  .then(()=>{
     console.log("4000ms 후");
  });

then함수를 체이닝 하여 해결 해주는 방법입니다.
일단 코드의 흐름인 위에서 아래로 코드가 진행되기 때문에 기존의 콜백 지옥보다는 훨씬 가독성이 뛰어난 것을 볼 수 있습니다.
이 코드가 이해가 되지 않는다면 위에 프로미스 처리 흐름을 보고 오세요.

0개의 댓글