무브라더

[JavaScript] Promise 에 대해서 알아보자 본문

Programming/JavaScript

[JavaScript] Promise 에 대해서 알아보자

동스다
반응형
SMALL

Promise

  • 자바스크립트 비동기 처리에 사용되는 객체
  • 서버에서 받아온 데이터를 화면에 표시할 때 사용
  • 데이터를 받아오기도 전에 화면에서 데이터를 표시하려고 할 때 발생하는 오류를 방지하기 위해 사용

 

프로미스를 사용하기 전에 프로미스를 사용하지 않고 데이터를 받아오는 코드를 살펴보면

function getData(callbackFunc) {
  $.get('url 주소/products/1', function(response) {
    callbackFunc(response); // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
  });
}

getData(function(tableData) {
  console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});

비동기 처리를 위해 프로미스 대신에 콜백 함수를 사용했다. 

 

위 코드에 프로미스를 적용시킨다면

function getData(callback) {
  // new Promise() 추가
  return new Promise(function(resolve, reject) {
    $.get('url 주소/products/1', function(response) {
      // 데이터를 받으면 resolve() 호출
      resolve(response);
    });
  });
}

// getData()의 실행이 끝나면 호출되는 then()
getData().then(function(tableData) {
  // resolve()의 결과 값이 여기로 전달됨
  console.log(tableData); // $.get()의 reponse 값이 tableData에 전달됨
});

new Promise, resolve, then 과 같은 프로미스 API를 사용한 구조로 바뀌었다.

 

프로미스를 처음 본다면 resolve나 then 을 사용하거나 보는데에 어색함이 있을 수 있는데

프로미스에서 중요한 역할을 하니 제대로 이해를 하고 넘어가야한다.

 

프로미스의 3가지 상태(states)

프로미스를 이해하기 위해 먼저 알아야할 개념 중 하나는 프로미스의 상태이다.

위에서 데이터를 주고받을 때 생기는 에러를 방지하기 위해 프로미스를 사용해준다고 말했는데

그 프로미스를 생성하고 종료될때 까지 생기는 상태에는 3가지가 있다.

 

1. Pending (대기) : 비동기 처리 로직이 아직 완료되지 않은 상태

2. Fulfilled (이행)  : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태

3. Rejected (실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

 

역시 개발자는 글보단 코드로 이해하는게 더 쉽다. 코드로 살펴보면

 

Pending (대기)

new Promise() 메소드를 호출하면 Pending 상태가 된다.

new Promise();

new Promise() 메소드를 호출할 때 콜백 함수를 선언할 수 있고, 콜백 함수의 인자는 resolve, reject 다.

new Promise(function(resolve, reject) {
	// ..
});

 

Fulfilled (이행)

콜백 함수의 인자 resolve를 실행하면 Fulfilled 상태가 된다.

new Promise(function(resolve, reject) {
  resolve();
});

Fulfilled 상태가 되면 then 을 이용해 처리한 결과 값을 받을 수 있다.

function getData() {
  return new Promise(function(resolve, reject) {
    const data = 100;
    resolve(data);
  });
}

// resolve()의 결과 값 data를 resolvedData로 받음
getData().then(function(resolvedData) {
  console.log(resolvedData); // 100
});

 

Rejected (실패)

콜백 함수의 인자 rejected를 실행하면 Rejected 상태가 된다.

new Promise(function(resolve, reject) {
  reject();
});

Rejected 상태가 되면 catch 를 이용해 처리한 결과 값을 받을 수 있다.

function getData() {
  return new Promise(function(resolve, reject) {
    reject(new Error("Request is failed"));
  });
}

// reject()의 결과 값 Error를 err에 받음
getData().then().catch(function(err) {
  console.log(err); // Error: Request is failed
});

 

 

 

Promise Chaining

프로미스의 특징 중 하나는 여러 개의 프로미스를 연결해서 사용 할 수 있다는 점인데 이걸 프로미스 체이닝이라고 한다.

function getData() {
  return new Promise({
    // ...
  });
}

// then() 으로 여러 개의 프로미스를 연결한 형식
getData()
  .then(function(data) {
    // ...
  })
  .then(function() {
    // ...
  })
  .then(function() {
    // ...
  });

 

비동기 처리예제에서 흔하게 사용되는 setTimeout()을 사용해서 프로미스 체이닝이 어떻게 동작하는지 살펴보면

new Promise(function(resolve, reject){
  setTimeout(function() {
    resolve(1);
  }, 2000);
})
.then(function(result) {
  console.log(result); // 1
  return result + 10;
})
.then(function(result) {
  console.log(result); // 11
  return result + 20;
})
.then(function(result) {
  console.log(result); // 31
});

* setTimeout 의 2000은 2s 후에 실행된다는 의미

 

resolve(1)이 2초 후에 실행이 되면 프로미스가 Pending 상태에서 Fulfilled 상태로 넘어간다.

결과적으로 첫번째 .then() 로직으로 넘어가고 다시 두번째 .then() 로직 세번째 .then() 로직으로 넘어가고 최종적으로

세번째 then() 에서 결과 값을 출력한다.

 

프로미스의 에러 처리 방법

맨 처음에 말한것처럼 프로미스는 발생하는 에러를 처리해주기 위해 사용한다고 했다.

그렇다면 그 에러를 어떻게 처리하는지도 알아야하는게 당연하다.

 

에러 처리 방법에는 2가지 방법이 있다.

 

1. then()의 두번 째 인자로 에러를 처리하는 방법

getData().then(
  success,
  error
);

2. catch()를 이용하는 방법

getData().then().catch();

 

function getData() {
  return new Promise(function(resolve, reject) {
    reject('error!!');
  });
}

// 1. then()의 두 번째 인자로 에러를 처리하는 코드
getData().then(function() {
  // ...
}, function(err) {
  console.log(err);
});

// 2. catch()로 에러를 처리하는 코드
getData().then().catch(function(err) {
  console.log(err);
});

 

그럼 어떤 방법으로 에러 처리를 하는게 좋을까? 코드를 살펴보자

// then()의 두 번째 인자로는 감지하지 못하는 오류
function getData() {
  return new Promise(function(resolve, reject) {
    resolve('hi');
  });
}

getData().then(function(result) {
  console.log(result);
  throw new Error("Error in then()"); // Uncaught (in promise) Error: Error in then()
}, function(err) {
  console.log('then error : ', err);
});

then의 두번째 인자로 에러를 처리해준 코드인데 실행 결과 값을 보면 다음과 같이 나온다.

에러를 못잡았다는 로그

이유는 getData() 함수의 프로미스에서 resolve() 메소드를 호출하여 정상적으로 로직을 처리했지만, then() 의 첫번째 콜백 함수 내부에서 오류가 나는 경우에는 오류를 제대로 잡아내지 못하기 때문이다.

 

하지만 catch() 를 사용한다면

// catch()로 오류를 감지하는 코드
function getData() {
  return new Promise(function(resolve, reject) {
    resolve('hi');
  });
}

getData().then(function(result) {
  console.log(result); // hi
  throw new Error("Error in then()");
}).catch(function(err) {
  console.log('then error : ', err); // then error :  Error: Error in then()
});

에러를 잡은 로그

친절하게 에러를 잡은 로그가 나온다.

 

따라서 가급적이면 catch()로 예외처리하는게 좋다.

 

* finally

finally는 프로미스가 처리된(settled) 상태일 때 호출되는 메서드다. 프로미스 체인의 가장 마지막에 사용된다. then 메서드와 유사하지만, 이전에 사용한 프로미스를 그대로 반환한다는 점이 다르다.

처리된 프로미스의 데이터를 건들이지 않고 추가작업(서버에 로그 보내기 등)을 할 때 유용하다

 

Promise 정적메소드

1. Promise.all

Promise.all은 여러 개의 비동기 처리를 한꺼번에 병렬처리할 때 사용한다.

 

- 여러 개의 비동기 처리를 순차적으로 처리할 때

const requestData1 = () => new Promise(resolve => setTimeout(() => resolve(1), 3000))
const requestData2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000))
const requestData3 = () => new Promise(resolve => setTimeout(() => resolve(3), 1000))

const res = []
requestData1()
  .then(data => {
    res.push(data)
    return requestData2()
})
  .then(data => {
    res.push(data)
    return requestData3()
})
  .then(data => {
    res.push(data)
    console.log(res) // [ 1, 2, 3 ] => 6초
})
  .catch((err) => console.log(err))

 

  • 첫 번째 비동기 처리에 3초, 두 번째에 2초, 세 번째에 1초 걸려서 총 6초 이상 소요
  • 서로 의존하지 않고 개별적으로 수행, 후속처리 아님, 순차적으로 처리할 필요x

 

- 여러 개의 비동기 처리를 병렬적으로 처리할 때

const requestData1 = () => new Promise(resolve => setTimeout(() => resolve(1), 3000))
const requestData2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000))
const requestData3 = () => new Promise(resolve => setTimeout(() => resolve(3), 1000))

Promise.all([requestData1(), requestData2(), requestData3()])
  .then(data => console.log(data)) // [ 1, 2, 3 ] => 3초
  .catch(err => console.log(err))
  • 병렬적으로 처리하기 때문에 가장 늦게 실행되는 requsetData1 처리 시간에 맞춰 실행
  • 모든 프로미스가 fulfilled 상태가 되면 모든 처리 결과를 배열에 저장해 새로운 프로미스 변환
  • 인수로 전달받은 배열의 프로미스가 하나라도 rejected 되면 즉시 에러 띄우고 종료

 

2. Promise.race

프로미스를 요소로 갖는 배열 등의 이터러블(반복가능한)을 인수로 전달받는다. 가장 먼저 이행된 프로미스의 처리결과만 resolve 한다. 말그대로 race(경주) 와 같다.

const requestData1 = () => new Promise(resolve => setTimeout(() => resolve(1), 3000))
const requestData2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000))
const requestData3 = () => new Promise(resolve => setTimeout(() => resolve(3), 1000))

Promise.race([requestData1(), requestData2(), requestData3()])
  .then(res => console.log(res)) // 3
  .catch(err => console.log(err))
  • 다 fulfilled 상태가 되기를 기다리는게 아닌 제일 먼저 fulfilled 된 하나가 나오면 종료
  • 하나라도 reject 되면 에러 띄우고 종료 

 

3. Promise.allSettled

프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 받는다.

각각의 비동기 처리가 settled 된 상태를 배열로 반환한다.

*settled : fulfilled 또는 rejected 하나라도 된 상태

Promise.allSettled([
  new Promise(resolve => setTimeout(() => resolve(1), 2000)),
  new Promise((_, reject) => setTimeout(() => reject(new Error('Error!!!')), 1000))
]).then(data => console.log(data))

// [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: Error: 'Error!!!' }
// ]
  • 각각의 프로미스의 처리 결과가 객체로 나타남
  • 이행된(fulfilled) 상태인 경우 비동기 처리 상태를 나타내는 status 프로퍼티와 처리 결과를 나타내는 value 프로퍼티 가짐
  • 실패한(rejected) 상태인 경우 비동기 처리 상태를 나타내는 status 프로퍼티와 에러를 나타내는 reason 프로퍼티 가짐

4. Promise.any

여러 프로미스 중 단 하나의 프로미스가 이행되면 프로미스 객체를 반환한다.

Promise.race는 fulfilled와 rejected를 가리지 않지만 Promise.any 는 fulfilled만을 반환한다.

const firstPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Data for Promise One")
    }, 3500)
});

const secondPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Data for Promise Two")
    }, 800)
});

const thirdPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("Data for Promise Three")
    }, 300)
});

const forthPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Data for Promise Four")
    }, 1000)
});

Promise.any([firstPromise, secondPromise, thirdPromise, forthPromise])
.then((firstResolvedData) => {
  console.log(`Data Received: ${firstResolvedData}`); //Data Received: Data for Promise Two
});

위 코드를 보면 가장 빨리 실행되는건 thirdPromise 지만 reject로 이행되지 않은걸 알 수 있다.

fulfilled 만 반환하기때문에 다음으로 빨리 실행되는 secondPromise 가 출력되는걸 볼 수 있다.

 

const firstPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Data for Promise One")
    }, 3500)
});

const secondPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Data for Promise Two")
    }, 800)
});

const thirdPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("Data for Promise Three")
    }, 300)
});

const forthPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Data for Promise Four")
    }, 1000)
});

Promise.race([firstPromise, secondPromise, thirdPromise, forthPromise])
.then((resolvedData) => {
  console.log(`Data Received: ${resolvedData}`)
}, (rejectedData) => {
    console.log(`Data Received: ${rejectedData}`) //Data Received: Data for Promise Three
});

Promise.race 의 경우에는 any와 달리 thirdPromise가 reject 되어도 출력이 되는걸 알 수 있다.

반응형
LIST
Comments