프라미스와 체이닝

345·2023년 7월 9일

모던 JavaScript

목록 보기
21/23

자바스크립트에서 콜백 함수 라 불리는 함수는 일정 동작이 끝난 후 호출되는 함수입니다.
브라우저 환경에서 다른 사이트로 네트워크 요청을 하는 등의 경우가 많으므로,
요청이 끝난 후 처리할 동작을 콜백에 담아 이후에 실행하곤 했습니다.

즉, 콜백 함수는 이전에 요청한 동작이 완료된 것을 탐지하고 그에 맞춰 비동기적으로 실행되어야 하는 함수입니다.
그러나 비동기처리를 콜백만으로 하게 되면 코드의 복잡도가 올라가며, 예외 처리도 쉽지 않습니다.

따라서, 더 간편한 방법으로 프라미스(promise) 가 고안되었습니다.


✅ 프라미스

프라미스는 특정한 상태와 결과값을 지니는 객체로, 비동기 작업 처리에 사용합니다.

let promise = new Promise(function(resolve, reject) {
  // executor 
  // 시간이 걸리는 일...
  
  // ...
  if(success){
	resolve(value);
  }else{
	reject(new Error("message"));
  }
});

위처럼 프라미스 객체를 만들 수 있습니다.
new Promise 에 전달된 함수는 executor (실행자) 라고 하며, 실행자는 새로운 프라미스 객체 생성(new Promise)과 함께 자동으로 실행됩니다.

이 때 실행자는 인수 resolvereject 를 받는데, 이는 자바스크립트가 자체 제공하는 콜백으로 실행자에서 둘 중 하나를 반드시 호출해야 합니다.

실행자가 성공적으로 동작을 완료하면 resolve 를, 에러가 발생하면 reject 를 호출합니다.

  • resolve(value): 일 성공 시 결과를 value 에 담아 호출
  • reject(error): 에러 발생 시 에러 객체 error 와 함께 호출

🔔 프라미스 객체의 내부 프로퍼티

new Promise 로 생성한 promise 객체는 stateresult 등의 내부 프로퍼티를 갖습니다.
이 내부 프로퍼티에 직접 접근할 수는 없으며, 실행자가 resolve 를 호출했는지 아니면 reject 를 호출했는지에 따라 상태가 변화합니다.

  • state: 실행자의 동작에 따라 상태가 바뀜
    • 처음: pending(보류)
    • resolve(value) 호출(동작 성공)이면? ➡️ fulfilled (이행)
    • reject(error) 호출(동작 실패)이면? ➡️ rejected (거부)
  • result: 실행자의 결과에 따른 값을 담음
    • 처음: undefined
    • resolve(value) 호출(동작 성공): value
    • reject(error) 호출(동작 실패): error

즉, 실행자에서 시간이 좀 걸리는 코드의 수행이 완료되면, 동작이 성공했는지 아닌지에 따라
생성된 프라미스 객체의 stateresult 값이 정해집니다.

실행자가 호출한 resolvereject 는 프라미스의 상태를 단 한 번만 변경합니다.
한 번 resolve 를 호출한 뒤 reject 를 호출한다고 해도 프라미스의 상태가 rejected 가 되지는 않습니다.


⚡ then, catch, finally

프라미스 객체는 메서드를 활용하여 실행자 이후의 동작을 수행합니다.

실행자에서 시간이 좀 걸리는 동작을 수행 후, resolvereject 를 호출하여 프라미스 객체의 상태를 변화시켰습니다.
이후, 프라미스 객체는 then, catch, finally 등의 메서드로 실행자의 결과에 따른 추후 동작을 수행합니다.

각 메서드는 동작을 수행 후 결과를 value 에 담아 resolve(value) 를 호출하는 프라미스를 반환합니다.
에러가 나면 거부 상태의 프라미스를 반환하겠죠?


  • then
promise.then(
  function(result) { /* 결과(result)를 다룹니다 */ },
  function(error) { /* 에러(error)를 다룹니다 */ }
);

then 은 두 가지 함수를 인수로 받습니다.

첫 번째 함수는 실행자의 동작이 성공했을 때를 가정하며, 프라미스 객체가 fulfilled 상태일 때 실행됩니다.
두 번째 함수는 실행자의 동작에서 에러가 난 경우를 가정하며, 프라미스 객체가 rejected 상태일 때 실행됩니다.

두 함수는 프라미스에서 인수를 넘겨받는데, 이는 프라미스 객체 내부 프로퍼티 result 의 값입니다.

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("완료!"), 1000); // state: fulfilled, result: "완료!"
});

// resolve 함수는 .then의 첫 번째 함수(인수)를 실행
promise.then(
  result => alert(result), // 1초 후 "완료!"를 출력
  error => alert(error) // 실행되지 않음
);

사용 예시는 위와 같습니다.


  • catch
let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

// .catch(f)는 promise.then(null, f)과 동일하게 작동
promise.catch(alert); // 1초 뒤 "Error: 에러 발생!" 출력
// promise.catch(err => alert(err)); 와 같음

catchthen 과 비슷하나, 실행자가 reject 를 호출한 경우만 다룹니다.
.then(null,f) 와 동일하다고 볼 수 있습니다.


  • finally
// 성공
new Promise((resolve, reject) => {
  setTimeout(() => resolve("결과"), 2000)
})
  .finally(() => alert("프라미스가 준비되었습니다."))
  .then(result => alert(result)); // <-- .then에서 result를 다룰 수 있음

// 실패
new Promise((resolve, reject) => {
  throw new Error("에러 발생!");
})
  .finally(() => alert("프라미스가 준비되었습니다."))
  .catch(err => alert(err)); // <-- .catch에서 에러 객체를 다룰 수 있음

finallyresolve 를 호출하든, reject 를 호출하든 상관없이 모든 경우에 실행되며,
then 이나 catch 와 달리 프라미스의 성공 여부를 따지지 않아 인수가 없습니다.
대신 다음 핸들러에 resulterror 를 전달합니다.

finally 는 프라미스 결과 처리가 아닌 마무리 동작에 사용합니다.
프라미스 결과 처리는 then 이나 catch 로 수행합니다.


프라미스가 pending 상태(대기 상태)일 때, then, catch, finally 등의 핸들러는
프라미스의 처리(fulfilledrejected 상태가 되기)를 기다립니다.
프라미스가 이미 처리된 상태라면 핸들러가 즉각 실행됩니다.

보통은 결과가 성공이면 then 을, 오류가 발생하면 catch 를, 마지막에 finally 를 수행하도록 나눠서 코드를 작성합니다.

new Promise((resolve, reject) => {
  // ...
  
  if(success){
	resolve(value);
  }else {
	reject(new Error("message"));
  }
})
  .then(result => alert(result)) // 성공이면 실행
  .catch(error => alert(error)) // 에러나면 실행
  .finally(() => alert("끝!")); // 성공여부 관련없이 마지막에 실행

🧩 프라미스 체이닝

비동기 작업 여러 개를 처리할 때 프라미스 체이닝을 사용합니다.
then 핸들러 체인을 통해 구현하는데, 다음 then 핸들러는 인수로 이전 then 핸들러의 반환값을 받아옵니다.

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 에서 반환한 값을 인수로 받아오는 것을 확인할 수 있습니다.
프라미스 체이닝은 promise.then/catch/finally 가 프라미스를 반환하기 때문에 가능합니다.
반환된 프라미스로 다시 then/catch/finally 메서드를 사용하는 것이죠.


then 핸들러 안에서 새로운 프라미스를 반환하는 방식으로 체이닝을 전개할 수도 있습니다.
반환한 프라미스의 결과가 다음 then 의 인수가 됩니다.

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000);

}).then(function(result) {

  alert(result); // 1

  return new Promise((resolve, reject) => { // 프라미스 반환
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) { // 프라미스의 결과를 받아옴

  alert(result); // 2
  
  throw new Error("에러 발생!");

}).catch(alert); // 에러 발생!

then 핸들러 내에서 발생한 에러는 catch 로 처리할 수 있습니다.


fetch 와 체이닝

fetch 메서드를 사용하면 원격 서버에서 정보를 가져올 수 있습니다.

let promise = fetch(url);

위처럼 fetch(url) 을 실행하면 url 에 네트워크 요청을 보내고 프라미스를 반환합니다.
원격 서버가 응답을 보내면 프라미스가 이행되는데, 응답을 전부 읽어오기 전에 프라미스가 먼저 fulfilled 상태가 되어버립니다.

따라서, 응답을 완전히 읽어온 후 결과를 처리하기 위해 프라미스를 반환하는 메서드를 사용합니다.
then 체인에서 새로운 프라미스를 반환하면, 그 프라미스가 이행될 때까지 다음 체인의 수행을 대기하므로 응답을 완전히 읽어오면 이행되도록 반환값이 프라미스가 되는 것입니다.

fetch('some_url')
  .then(response => response.json()) // response.json() 이 프라미스 반환, 결과를 파싱
  .then(user => alert(user.name)); // Violet-Bora-Lee, 이름만 성공적으로 가져옴

이렇게 json() 메서드는 응답을 완전히 읽어온 후 프라미스를 반환합니다.
이때 resolve 에 응답의 바디 본문을 JSON 으로 파싱한 결과를 담아 호출합니다.


에러 핸들링

프라미스가 거부되면 가장 가까운 rejection 핸들러 (then(null, f)catch) 가 실행됩니다.
따라서 체인 중 발생하는 에러를 쉽게 처리할 수 있습니다.

fetch('some_url')
  .then(response => response.json())
  .then(user => fetch(`some_url/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise((resolve, reject) => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  }))
  .catch(error => alert(error.message));

프라미스 체인 중간에 에러가 발생한다면 제어 흐름이 catch 로 넘어가게 됩니다.


암시적 try..catch

실행자와 핸들러는 보이지 않는 try..catch 에 둘러싸여 있다고 볼 수 있습니다.
코드 실행 중 에러가 발생하면, 이 try..catch 는 에러를 잡아 rejected 상태의 프라미스를 생성합니다.

즉, throw 로 에러를 만들어 던지면 거부된 프라미스에 의해 에러 핸들러가 실행됩니다.
throw 로 만든 것 뿐만 아니라 그 외 모든 종류의 에러를 try..catch 가 잡아냅니다.

new Promise((resolve, reject) => {
  resolve("OK");
}).then((result) => {
  blabla(); // 존재하지 않는 함수
}).catch(alert); // ReferenceError: blabla is not defined

그런데, 에러를 처리할 catch 등의 핸들러가 없다면 전역 에러를 생성합니다.
브라우저 환경에서는 이런 에러를 unhandledrejection 이벤트로 받아 처리합니다.

profile
기록용 블로그 + 오류가 있을 수 있습니다🔥

0개의 댓글