Frontend/Javascript

Javascript - Promise

취업하고싶다! 2024. 12. 21. 17:03

콜백 지옥을 탈출하는 새로운 문법

자바스크립트에서 '비동기 처리' 란 현재 실행중인 작업과는 별도로 다른 작업을 수행하는 것을 말한다.

예를 들어 서버에서 데이터를 받아오는 작업은 시간이 걸리기 때문에 자바스크립트의 서버 호출 함수는 비동기 함수(링크)로 이루어져 있다.

비동기특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 방식이기 때문에, 만일 비동기 작업의 결과에 따라 다른 작업을 수행해야 할 때는 전통적으로 콜백 함수를 사용했다.

 

콜백 함수란 비동기 작업이 완료되면 호출되는 함수의 의미로서, 비동기 함수의 매개변수로 함수 객체를 넘기는 기법을 말한다. 그래서 함수 내부에서 함수 호출을 통해 비동기 작업의 결과를 받아서 인자로 주면 이를 통해 후속 처리 작업을 수행할 수 있다. 하지만 콜백 함수를 사용하면 코드가 복잡하고 가독성이 떨어지는 문제가 있다. 특히, 여러 개의 비동기 작업을 순차적으로 수행해야 할 때는 콜백 함수가 중첩되어 코드의 깊이가 깊어지는 현상이 발생한다. 이러한 현상을 콜백 지옥(callback hell) 이라고 부른다.

 

콜백 함수의 비동기 처리 문법

아래는 숫자 n 을 파라미터로 받아와서 다섯번에 걸쳐 1초마다 1씩 더해서 출력하는 작업을 setTimeout 비동기 함수로 구현한 코드이다. 한눈에 봐도 콜백함수를 연달아 써서 코드의 깊이가 깊어졌다. 보기에 매우 안좋은 걸 볼 수 있다.

function increaseAndPrint(n, callback) {
  setTimeout(() => {
    const increased = n + 1;
    console.log(increased);
    if (callback) {
      callback(increased); // 콜백함수 호출
    }
  }, 1000);
}

increaseAndPrint(0, n => {
  increaseAndPrint(n, n => {
    increaseAndPrint(n, n => {
      increaseAndPrint(n, n => {
        increaseAndPrint(n, n => {
          console.log('끝!');
        });
      });
    });
  });
});

이러한 콜백 함수의 코드 형태는 콜백 함수가 중첩되면서 들여쓰기 수준이 깊어져 코드의 가독성을 떨어뜨리며 코드의 흐름을 파악하기 어려워진다. 또한 콜백 함수마다 에러 처리를 따로 해줘야 하고, 에러가 발생한 위치를 추적하기 힘들게 된다.

 

프로미스로 개선된 비동기 처리 문법

이를 자바스크립트의 Promise 객체를 이용해 리팩토링하면, 비동기 작업의 개수가 많아져도 들여쓰기 코드의 깊이가 깊어지지 않게 된다.

function increaseAndPrint(n) {
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      const increased = n + 1;
      console.log(increased);
      resolve(increased);
    }, 1000)
  })
}

increaseAndPrint(0)
  .then((n) => increaseAndPrint(n))
  .then((n) => increaseAndPrint(n))
  .then((n) => increaseAndPrint(n))
  .then((n) => increaseAndPrint(n)); // 체이닝 기법

 

 

자바스크립트 프로미스 객체

비동기 작업의 최종 완료 또는 실패를 나타내는 Array나 Object 처럼 독자적인 객체

비동기 작업이 끝날 때까지 결과를 기다리는 것이 아니라, 결과를 제공하겠다는 '약속'을 반환한다는 의미에서 Promise라 명명 지어졌다.

 

프로미스 객체 기본 사용법

 

- 프로미스 객체 생성

Promise 객체를 생성하려면 new 키워드Promise 생성자 함수를 사용하면 된다. 이때 Promise 생성자 안에 두개의 매개변수를 가진 콜백 함수를 넣게 되는데, 첫 번째 인수는 작업이 성공했을 때 성공(resolve)임을 알려주는 객체이며, 두 번째 인수는 작업이 실패했을 때 실패(reject)임을 알려주는 오류 객체이다.

Promise 생성자안에 들어가는 콜백 함수를 executor 라고 부른다.

const myPromise = new Promise((resolve, reject) => {
	// 비동기 작업 수행
    const data = fetch('서버로부터 요청할 URL');
    
    if(data)
    	resolve(data); // 만일 요청이 성공하여 데이터가 있다면
    else
    	reject("Error"); // 만일 요청이 실패하여 데이터가 없다면
})

 

- 프로미스 객체 처리

이렇게 만들어진 Promise 객체는 비동기 작업이 완료된 이후에 다음 작업을 연결시켜 진행할 수 있다.

작업 결과 따라 .then()  .catch() 메서드 체이닝을 통해 성공과 실패에 대한 후속 처리를 진행할 수 있다.

만일 처리가 성공하여 프로미스 객체 내부에서 resolve(data) 를 호출하게 되면, 바로 .then() 으로 이어져 then 메서드의 콜백 함수에서 성공에 대한 추가 처리를 진행한다. 이때 호출한 resolve() 함수의 매개변수의 값이 then 메서드의 콜백 함수 인자로 들어가 then 메서드 내부에서 프로미스 객체 내부에서 다룬 값을 사용할 수 있게 된다.

반대로 처리가 실패하여 프로미스 객체 내부에서 reject("Error") 를 호출하게 되면, 바로 .catch() 로 이어져 catch 메서드의 콜백 함수에서 성공에 대한 추가 처리를 진행한다. 

myPromise
    .then((value) => { // 성공적으로 수행했을 때 실행될 코드
    	console.log("Data: ", value); // 위에서 return resolve(data)의 data값이 출력된다
    })
    .catch((error) => { // 실패했을 때 실행될 코드
     	console.error(error); // 위에서 return reject("Error")의 "Error"가 출력된다
    })
    .finally(() => { // 성공하든 실패하든 무조건 실행될 코드
    	
    })

 

- 프로미스 함수 등록

위와 같이 프로미스 객체를 변수에 바로 할당하는 방식을 사용할 수도 있지만, 보통은 다음과 같이 별도로 함수로 감싸서 사용하는 것이 일반적이다.

// 프로미스 객체를 반환하는 함수 생성
function myPromise() {
  return new Promise((resolve, reject) => {
    if (/* 성공 조건 */) {
      resolve(/* 결과 값 */);
    } else {
      reject(/* 에러 값 */);
    }
  });
}

// 프로미스 객체를 반환하는 함수 사용
myPromise()
    .then((result) => {
      // 성공 시 실행할 콜백 함수
    })
    .catch((error) => {
      // 실패 시 실행할 콜백 함수
    });

 

 

프로미스 3가지 상태

프로미스는 비동기 작업의 결과를 약속하는 것이다. new Promise() 생성자로 프로미스 객체를 생성하면, 그 비동기 작업은 이미 진행 중이고 언젠가는 성공하거나 실패할 것이다. 이러한 진행중, 성공, 실패 상태를 나타내는 것이 바로 프로미스의 상태(state)라고 불리운다. 쉽게 말하자면 일종의 프로미스 처리 과정이라고 보면 된다.

  1. Pending(대기) : 처리가 완료되지 않은 상태 (처리 진행중)
  2. Fulfilled(이행) : 성공적으로 처리가 완료된 상태
  3. Rejected(거부) : 처리가 실패로 끝난 상태

 

프로미스 핸들러

프로미스가 생성되면, 그 작업은 이미 진행 중이고 언젠가는 성공하거나 실패할 것이다. 그 성공/실패 결과를 .then / .catch / .finally 핸들러를 통해 받아 다음 후속 작업을 수행할 수 있다. 프로미스 핸들러는 프로미스의 상태에 따라 실행되는 콜백 함수라고 보면 된다.

  • .then() : 프로미스가 이행(fulfilled)되었을 때 실행할 콜백 함수를 등록하고, 새로운 프로미스를 반환
  • .catch() : 프로미스가 거부(rejected)되었을 때 실행할 콜백 함수를 등록하고, 새로운 프로미스를 반환
  • .finally() : 프로미스가 이행되거나 거부될 때 상관없이 실행할 콜백 함수를 등록하고, 새로운 프로미스를 반환

프로미스 핸들러 구성을 보건데, 마치 try - catch - finally 구조와 굉장히 유사하다고 느낄텐데 그 느낌이 맞다.

 

 

promise.all()

여러 개의 Promise 들을 비동기적으로(병렬적으로) 실행하여 처리하고 모든 프로미스가 완료되면 배열로 결과를 반환

Promise 들 중 하나라도 reject 를 반환하거나 에러가 날 경우, 모든 Promise 들을 reject 시킴

 

 

콜백 지옥을 이은 프로미스 지옥

then() 체인을 길게 이어 나가면 콜백 체인과 마찬가지로 코드의 가독성이 떨어지고 에러가 어디서 일어났는지 보기 어렵다는 단점이 있다.

이를 해결하고자 나온 것이 async/await