티스토리 뷰

반응형

 

지난 포스팅에서는 비동기 처리를 위한 콜백 지옥을 의도적으로 체험해보았다. 

[비동기 |  동기 ] 비동기 처리의 시작 - 콜백 지옥 체험

 

이번 포스팅에서는 이런 콜백지옥의 대안으로 ES6에서 나온 프로미스(Promise)에 대해 다뤄보는 시간을 가지겠다. 

 

자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용한다. 하지만 전통적인 콜백 패턴은 콜백 헬로 인해 가독성이 나쁘고 비동기 처리 중 발생한 에러의 처리가 번거로워지며 여러 개의 비동기 처리를  한 번에 처리하는 데도 한계가 있다. 

 

이의 대안으로 ES6에서는 비동기 처리를 위한 또 다른 패턴으로 프로미스(Promise) 를 도입했다. 프로미스는 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다. 

 

 


 

 

프로미스(Promise)

저번 포스팅에서도 살짝 다뤘지만 보통 우리가 네트워크에서 데이터를 받아 올 때 큰 데이터를 읽어오는 경우가 많아 시간이 소요된다. 이런 것을 동기적으로 처리하게 되면 데이터를 받아올 동안 다음 태스크(task)가 수행될 수 없기 때문에 시간이 걸리는 작업들은 프로미스를 만들어 비동기적으로 처리하는 것이 좋다. 

 

프로미스는 자바스크립트에서 제공하는 비동기를 간편하게 처리할 수 있도록 도와주는 오브젝트이다.  이 프로미스는 정해진 장시간에 기능을 수행하고 나서 정상적으로 기능이 수행(resolve) 되었다면 성공메시지와 함께 처리된 결괏값을 전달해 주고 만약 기능을 수행했다가 예상치 못한 문제가 발생(reject) 했다면 에러를 전달해준다. 

 

프로미스에서는 1. State와  2. Producer & Counsumer 차이 두  가지 포인트를 잡고 이해해야한다.

 

 

1. State

프로세스가 무거운 오퍼레이션을 수행하고 있는 중인지 아니면 이 기능 수행이 성공하거나 실패했는지에 대한 상태

다음과 같이 현재 비동기 처리가 어떻게 진행되고 있는지 나타내는 상태 정보를 갖는다.

 

프로미스의 상태 정보 의미 상태 변경 조건
pending 비동기 처리 수행중 프로미스가 생성된 직후 기본 상태
fulfilled 비동기 처리 수행 완료 (성공) resolve 함수 호출
rejected 비동기 처리 수행 완료 (실패) reject 함수 호출

 

2. Producer &  Consumer의 차이

우리가 원하는 데이터를 제공하는 (Producer)과 이 제공된 데이터를 필요로 하는 (Consumers)의 차이

 

 

 

Producer - 프로미스의 생성

 

우리가 원하는 기능을 비동기적으로 실행하는 프로미스라는 프로듀서를 생성해보자. 프로미스는 비동기 처리를 수행할 콜백 함수(ES6에서는 executor라고 함)를 인수로 전달받는데 이 콜백 함수는 resolve, reject 함수를 인수로 전달받는다.  

 

아래의 생성된 프로미스에서 콘솔을 찍어보고 확인하면 바로 실행되는 것을 확인할 수 있다. 이는 프로미스 안에 네트워크 통신을 하는 코드를 작성했다면 프로미스가 만들어지는 그 순간 바로 네트워크 통신을 수행하게 된다는 것이 되겠다. 

 

가령 버튼을 클릭했을 때만 네트워크를 요청하고 싶은데,  아래처럼 콘솔을 바로 실행할 경우 불필요한 네트워크 통신이 일어날 수 있다. 따라서 프로미스를 만들 때는 이 점을 유의하여 작성해야 한다. 

 

const promise = new Promise((resolve, reject) => {
  // doing some heavy work (network, read files)
  console.log('doing something...');
  setTimeout(() => {
    // 성공하면 resolve 콜백함수 호출
    resolve('hello!');
    // reject(new Error('no network'));
  }, 2000);
});

 

 

 

Consumers - 프로미스 사용하기,  프로미스의 후속처리 메서드 (then, catch, finally)

프로미스의 비동기 처리 상태가 변화하면 이에 따른 후속 처리를 해야 한다. 이를 위해 프로미스는 후속 메서드 then, catch, finally를 제공한다. 

 

 

 

1_  Promise.prototype.then

then 메서드는 두 개의 콜백 함수를 인수로 전 달 받는다. 

 

1. 비동기 처리가 성공했을 때

프로미스가 fulfilled 상태(resolve 함수 호출된 상태)가 되면 호출된다. 이때 콜백 함수는 프로미스와 비동기 처리 결과를 인수로 받는다. 

 

2. 비동기 처리가 실패했을 때

프로미스가 rejected 상태(reject 함수 호출된 상태)가 되면 호출된다. 이때 콜백 함수는 프로미스의 에러를 인수로 전달받는다. 

하지만. then에서 실패 처리까지 하면 가독성이 좋지 않아 뒤에서 배울 cath메서드로 실패 처리를 해주는 게 좋다. 

 

then 메서드는 언제나 프로미스를 반환한다. 만약 then 메서드의 콜백 함수가 프로미스를 반환하면 그 프로미스를 그대로 반환하고, 콜백함수가 프로미스가 아닌 값을 반환하면 그 값을 암묵적으로 resolve 또는 reject 하여 프로미스를 생성해 반환한다. 

 

Producer에서 만들었던 promise객체를 참고해 아래 코드를 보면 만약 promise가 잘 수행된다면 위 코드에서 적었던 "hello!"가 3초 후에 출력될 것이다. 

promise
  //promise가 잘 수행되면 .then 원하는 value를 받아올 것 > 3초 후 hello! 출력
  .then((value) => {
    console.log(value);
  })

 

 

2_ Promise.prototype.catch

catch메서드는 한 개의 콜백 함수를 인수로 전달받는다. catch메서드의 콜백 함수는 프로미스가 rejected인 상태인 경우만 호출된다. 

. then을 통해 return 된 promise에서 catch를 등록한다. 

 

만약 catch를 사용하지 않고 프로미스에서 reject와 new Error객체를 사용하면 에러 메시지가 나오게 된다. 

const promise = new Promise((resolve, reject) => {
  // doing some heavy work (network, read files)
  console.log("doing something...");
  setTimeout(() => {
    // 성공하면 resolve 콜백함수 호출
    //resolve("hello!");
    // reject , new Error 오브젝트 통해 에러 출력
    reject(new Error('no network'));
  }, 2000);
});

 

promise
  //promise가 잘 수행되면 .then 원하는 value를 받아올 것 > 3초 후 hello! 출력
  .then((value) => {
    console.log(value);
  })

여기서 발생한 Uncaught 에러가 나온 이유는 then에서 받아온 value는 성공했을 시에 받아오는 value였기 때문에 잡히지 않는 에러가 발생한 것이다. 

new Error 객체 통해 에러문구 출력

 

 

이때 catch라는 함수를 이용해 에러가 발생했을 때 어떻게 처리할 것인지에 대한 콜백 함수를 등록해준다. 

promise
  //promise가 잘 수행되면 .then 원하는 value를 받아올 것 > 3초 후 hello! 출력
  .then((value) => {
    console.log(value);
  })
  // .then을 통해 return 된 promise 에서 catch를 등록
  .catch((error) => {
    console.log(error);
  })

이렇게 해주면 더 이상 에러 메시지가 나오지 않게 된다. 여기서 체이닝(chaining이 발생하는데). then을 통해 리턴된 프로미스에서. catch를 수행하게 된다. 

 

 

 

3_ Promise.prototype.finally

finally 메서드는 한 개의 콜백 함수를 인수로 전달받는다. finally 메서드의 콜백 함수는 프로미스의 성공 실패 유무에 상관없이 무조건 한 번 호출된다. 이 메서드는 프로미스의 상태와 상관없이 공통적으로 수행해야 할 처리 내용이 있을 때 유용하다. finally 메서드도 then/catch메서드와 마찬가지로 언제나 프로미스를 반환한다. 

promise
  //promise가 잘 수행되면 .then 원하는 value를 받아올 것 > 3초 후 hello! 출력
  .then((value) => {
    console.log(value);
  })
  // .then을 통해 return 된 promise 에서 catch를 등록
  .catch((error) => {
    console.log(error);
  })
  // 성공 실패유무에 상관없이 무조건 마지막에 호출된다. 
  .finally(() => {
    console.log("finally!!!");
  }); // output : finally!!!

 

 

 

 

프로미스 체이닝 ( Promis chaining ) 

여러 가지 비동기적인 것들을 묶어서 처리할 수 있다. 

 

const fetchNumber = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 1000);
});

fetchNumber
  .then((num) => num * 2)
  .then((num) => num * 3)
  .then((num) => {
    // then 은 값 뿐만아니라 프로미스도 전달할 수 있다.
    // 2배 후 3배까지 한 그 num을 프로미스를 통해 다른 서버에 보내서 다른 숫자로 변환된 값을 받아온다. 
    return new Promise((resolve, reject) => {
      // 또 다른 프로미스를 통해 다른 서버와 통신 num - 1 값 반환 
      setTimeout(() => resolve(num - 1), 1000);
    });
  })
  //  5 반환 
  .then((num) => console.log(num));

 

 

 

프로미스의 에러 처리

다음 코드는 콘솔 창에

🐓 => 🥚 => 🍳 를 프로미스를 통해 출력하는 과정이다. 
 
const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve("🐓"), 1000);
  });
const getEgg = (hen) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(new Error(`${hen} => `🥚`)), 1000);
  });
const cook = (egg) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} => 🍳`), 1000);
  });

getHen() //
  // .then(hen => getEgg(hen)) 다른함수로 하나만 호출하는 경우 생략가능 암묵적으로 전달해서 호출해줌
  // .then(egg => cook(egg))
  // .then(meal => console.log(meal))
  .then(getEgg)
  .then(cook)
  .then(console.log)

 

 

 

만약 getEgg에서 네트워크에 문제가 생겨 실패가 된다면 아래와 같은 실패 시에 정해준 메시지가 발생된다. 

 

 

 

 

const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve("🐓"), 1000);
  });
const getEgg = (hen) =>
  new Promise((resolve, reject) => {
  	// 네트워크 통신 문제로 에러 발생 
    setTimeout(() => reject(new Error(`error! ${hen} => 🥚`)), 1000);
  });
const cook = (egg) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} => 🍳`), 1000);
  });

getHen() //
  .then(getEgg)
  .then(cook)
  .then(console.log)
  // catch()를 통해 에러가 발생 했을 시 에러를 전달해줌
  .catch(console.log);
  
// 요리를 완성시키려면 대체품을 사용할 수 있다
// getHen() 
//   .then(getEgg)
//   .catch((error) => {
//     // 달갈 받아오는 데서 에러가 발생했을 시 빵으로 대체하며 바로 문제가 해결됨 : 대체제로 요리 완성
//     return `🥯`;
//   })
//   .then(cook)
//   .then(console.log)
//   .catch(console.log);

 

 

 

 

 

콜백 지옥 to 프로미스 

 

마지막으로 지난 포스팅에서 에서 다룬 콜백 지옥 코드를 프로미스로 바꾸어보자. 길고 복잡했던 코드가 프로미스의 메서드로 통해 깔끔하게 변한 것을 확인할 수 있다. 

// Callback Hell example
class UserStorage {
  // onSuccess , onError 필요 없음 
  loginUser(id, password) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (
          (id === 'ellie' && password === 'dream') ||
          (id === 'coder' && password === 'academy')
        ) {
          // 성공
          resolve(id);
        } else {
          // 실패 
          reject(new Error('not found'));
        }
      }, 2000);
    });
  }

  getRoles(user) {
     // onSuccess , onError 필요 없음 
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (user === 'ellie') {
          // 성공
          resolve({ name: 'ellie', role: 'admin' });
        } else {
          // 실패
          reject(new Error('no access'));
        }
      }, 1000);
    });
  }


// Original code from Youtube course
const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your passrod');

// 프로미스 후속 처리
userStorage
  // 아이디 비번 받아와서
  .loginUser(id, password)
  // getRoles호출 인자가 같아 생략됨 
  .then(userStorage.getRoles)
  // role 잘 받아 온다면 alert실행 
  .then(user => alert(`Hello ${user.name}, you have a ${user.role} role`))
  // 실패시 정해둔 에러 콘솔 출력 
  .catch(console.log);

 

 

 

마무리

 

프로미스는 프로미스 체이닝을 통해 비동기 처리 결과를 전달 받아 후속 처리를 하므로 비동기 처리를 위한 콜백 패턴에서 발생하던 콜백지옥이 발생하지 않는다. 다만 프로미스도 콜백 패턴을 사용하므로 콜백 함수를 사용하지 않는 것은 아니다. 

 

콜백 패턴은 가독성이 좋지 않다. 이 문제는 ES8에서 도입된 async/await을 통해 해결할 수 있다. async/await을 사용하면 프로미스의 후속 처리 메서드 없이 마치 동기 처리처럼 프로미스가 처리 결과를 반환하도록 구현할 수 있다. 다만 async/await도 프로미스를 기반으로 동작하며, 프로미스를 써야하는 경우도 있으므로  프로미스를 먼저 잘 이해하는 것이 중요하겠다. 

 

 

 

 

Reaference:

[Youtube] 드림 코딩 엘리  

[위키북스] 자바스크립트 deep dive

 

반응형