TIL 19 | 콜백 지옥 탈출하기 : Callback과 Promise

이사감·2021년 3월 17일
2

Javascript

목록 보기
4/9
post-thumbnail

💌 이 글은 드림코딩의 비동기 3부작 | 콜백 - 프로미스 - async & await 에서 콜백과 프로미스에 대해 다룹니다

Callback 함수

이 글은 콜백 지옥을 탈출하기 위해 프로미스를 쓰는 방법에 대해 알아보는 글이다. 당연히 콜백이 무엇인지부터 알아야 하는데, 그러기위해서는 비동기적 처리와 동기적 처리에 대해 알아야 한다.

비동기적 처리와 동기적 처리

동기적 처리

  • 자바스크립트는 동기적(synchronous)이다

  • 동기적이라는 것은 hoisting 이후 코드가 나타나는 순으로 자동으로 동작한다는 것이고, 작동 순서를 예측할 수 있다는 것을 말한다.

비동기적 처리

  • 비동기적이라는 것은 언제 코드가 실행될지 예측할 수 없음을 말한다.

  • 예시 : setTimeout은 지정된 시간이 지나면 콜백함수를 호출하는 브라우저 API로, 대표적인 비동기 방식이다. 자바스크립트는 동기적으로 코드를 처리하다 setTimeout을 만나면 브라우저에 요청을 보내고, 그 응답을 기다리지 않고 다음 코드로 넘어간다.

  • 시간이 오래 걸리는 요청이 발생하면 비동기적으로 처리하여 처리되는 동안 다른 일을 하고 있을 수 있게 한다. (CPU리소스 낭비 방지)


콜백 함수의 의미

  • setTimeout으로 브라우저에 요청을 보냈던 것이 처리가 되면 이를 실행할 수 있도록 나중에 다시 불러야 하는데, 이 불러달라는 것에 착안하여 Call Back 함수라고 이름이 붙었다.

  • 함수 A 호출에서 함수 B가 인자로 전달될 때, 함수 B를 콜백 함수라 말한다. 즉, 함수 A의 매개변수로 전달되어 특정 이벤트 발생 후 다시 호출되는 함수 B를 말한다.


콜백 함수의 종류

자바스크립트에서 비동기적 프로그래밍을 하기 위해 콜백 함수를 사용하기는 하지만 콜백을 항상 비동기일때만 쓰는 것은 아니다.

  • 즉각적으로 동기적으로 실행
  function init(print){
      print();
  }
  init(()=>console.log('sync'));
  • 나중에 언제 실행될지 예측할 수 없음
  function init(print, timeout){
      setTimeout(print, timeout);
  }
  init(()=>console.log('async'),2000);

콜백 지옥 🔥

콜백 함수 안에서 콜백 함수를 부르고 또 부르고... 비동기적 처리를 위해 콜백 함수를 반복해서 사용하는 것을 콜백 지옥이라고 한다.

콜백 지옥의 문제점 : 가독성이 떨어진다.

  • 어디서 어떤식으로 연결되었는지 한눈에 파악하기 어렵다.

  • 비즈니스 로직을 한눈에 이해하기 어렵다.

  • 에러 해결, 디버깅, 문제분석, 유지보수가 어렵다.

💬 콜백 지옥 예제 코드

class UserStorage {
  loginUser(id, password, onSuccess, onError) {
    setTimeout(() => {
      if (
        (id === 'ellie' && password === 'dream') ||
        (id === 'coder' && password === 'academy')
      ) {
        onSuccess(id);
      } else {
        onError(new Error('not found'));
      }
    }, 2000);
  }

  getRoles(user, onSuccess, onError) {
    setTimeout(() => {
      if (user === 'ellie') {
        onSuccess({ name: 'ellie', role: 'admin' });
      } else {
        onError(new Error('no access'));
      }
    }, 1000);
  }
}

const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your password');
userStorage.loginUser( //로그인 진행
  id,
  password,
  user => { //loginUser 성공시
    userStorage.getRoles( 
      user,
      userWithRole => { //getRoles성공
        alert( //로그인이 잘 됐다는 메세지 출력
          `Hello ${userWithRole.name}, you have a ${userWithRole.role} role`
        );
      },
      error => { //getRoles실패
        console.log(error);
      }
    );
  },
  error => { //loginUser 실패시 
    console.log(error);
  }
);

📝 다음 문단에서 다룰 Promise를 통해 콜백 지옥을 해결할 수 있다.


Promise

Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다. 프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있습니다. 다만 최종 결과를 반환하지는 않고, 대신 프로미스를 반환해서 미래의 어떤 시점에 결과를 제공합니다. MDN

  • 자바스크립트 내장 객체. 콜백함수 대신 비동기를 간편하게 처리할 수 있도록 도와주는 객체

  • 비동기적 작업이 완료되면 프로미스의 Consumer 메소드를 통해 프로미스를 불러온다.

  • 성공 또는 실패만 한다 !

    • 성공 : 정해진 장시간의 기능을 수행후, 정상적으로 수행되었다면 성공의 메세지와 함께 처리된 결과값을 전달함.

    • 실패 : 기능 수행 중 예상치 못한 문제가 발생하면 에러를 전달함.


Promise 문법

const promise = new Promise(function(resolve, reject) {
	executor // Promise의 콜백함수
});
  • 클래스이기 때문에 new를 통해 새 객체를 생성함

  • executor의 인수 resolvereject는 자바스크립트 엔진이 미리 정의한 함수이므로 따로 만들 필요가 없다. 따라서 개발자는 executor 안 코드만 작성하면 됨

  • executor 안에는 파라미터로 넘겨받은 콜백 resolve, reject 중 하나를 반드시 호출해야 함

  • executor는 promise의 상태를 fulfilledrejected 둘 중 하나로 변화시킨다

  • 📢 ❗ 새로운 promise가 만들어지는 순간 그 안의 executor가 자동적으로, 즉각적으로 실행되는 점에 유의해야한다. 사용자가 요구하지도 않았는데 불필요한 네트워크 통신이 일어날 수 있기 때문이다.


상태 (State)

promise는 다음 중 하나의 상태를 가진다.

  • 대기(pending): 이행하거나 거부되지 않은 초기 상태

  • 이행(fulfilled): 연산이 성공적으로 완료됨. resolve

  • 거부(rejected): 연산이 실패함. reject

  • 이행, 거부된 상태의 프로미스를 처리된(settled) 프로미스라고 부른다


Producer

  new Promise((resolve, reject) => {
  	doing some heavy work (network, read files)  
  });
  • resolve : 정상 수행 후 최종 결과 반환

  • reject : 수행중 문제발생시 호출. Error object를 반환한다.
    ex. reject(new Error('no network'));

  • heavy work : promise를 통해 비동기적으로 처리하는 것이 좋다. 시간이 걸리기 때문에 동기적으로 처리하게 되면 이 일을 하는 동안 다른 코드를 실행할 수 없기 때문임.


Consumer

프로미스에서 소비함수 역할을 하는 메소드 then, catch, finally

  • then : 첫 번째 인수는 프로미스 성공시의 결과를, 두 번째 인수는 실패시의 에러를 받는다. 인수를 하나만 전달하면 성공시의 결과만 다룬다.

  • catch : 에러가 발생한 경우만 다룬다.

  • finally : 성공,실패와 상관없이 무조건 마지막에 호출된다.

💬 예제코드

  Promise
    .then((value)=>{
    console.log(value);
    });
    .catch(error=>{ 	// reject 콜백함수 값
      console.log(error);
    });
    .finally(()=>{  
      console.log('finally');
    }); 	
  • then메소드 안의 value는 프로미스가 성공하여 얻은 resolve 콜백함수 값을 받는다.

  • catch메소드 안의 error는 프로미스가 실패하여 얻은 reject 콜백함수 값을 받는다.

  • Consumer 간결하게 쓰기
    파라미터가 1개일 때 함수이름만 쓰면 암묵적으로 그 함수가 매개변수로 전달되어 더 간결하게 작성할 수 있다.

    💬 예제코드

    // 정직한 코드
    getHen()
      .then((hen) => {
          return getEgg(hen);
       })
      .then((egg) => {
          return cook(egg);
       })
      .then((meal) => {
          console.log(meal);
       }) 

    // 간결한 코드
      getHen()
        .then(getEgg) // then이 받아오는 value를 바로 getEgg 함수에 전달
        .then(cook)
        .then(console.log);

Promise chaining (연결하기)

  • 프로미스들을 연결하는 것을 말한다. 단, 코드가 난잡해질 수 있는데 이때는 async·await을 활용할 수 있다.
  • chaining을 할 때 then 메소드는 값을 바로 전달할 수도 있고, return으로 비동기인 프로미스를 전달할 수도 있다.

💬 예제코드

  const fetchNumber = new Promise((resolve,reject)=>{
    setTimeout(()=>resolve(1), 1000);
    //1초 있다가 숫자 1을 전달
  });
  
  fetchNumber
    .then(num => num*2) // 2  num에 숫자 1 전달됨 (성공), 이후 숫자를 2배
    .then(num => num*3) // 6
    .then(num => { // 5
      return new Promise ((resolve, reject)=>{
        setTimeout(()=>resolve(num-1), 1000);
      });
    })
    .then(num => console.log(num)); // 5  총 2초 걸림

Promise Error Handling (오류대응)

💬 예제코드

const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve('🐓'), 1000);
  });
  
const getEgg = hen => // 위의 promise 정상 처리 완료시 닭을 전달받아 getEgg호출
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${hen} => 🥚`), 1000);
  });
  
const cook = egg => // 위의 promise 정상 처리 완료시 계란을 전달받아 getcook호출
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} => 🍳`), 1000);
  });
  
getHen()
  .then(getEgg) // 🐓 전달받아 getEgg 호출
  .then(cook) // 🥚 전달받아 cook 호출
  .then(console.log); //cook 완료 후 🍳 출력
  
// 3초 후 🐓=>🥚=>🍳

💬 오류상황

네트워크에 문제가 생겨 프로미스 .then(getEgg)가 실패한 상황

setTimeout(() => reject(new Error(`error! ${hen} => 🥚`)), 1000)

📝 해결방법 1

getHen()
  .then(getEgg)
  .then(cook)
  .then(console.log)
  .catch(console.log); 📌

달걀을 받아오는 부분에서 에러가 발생했어도 에러가 제일 밑으로 전달되며 console.log가 제일 마지막에 찍힌다.

📝 해결방법 2

getHen()
  .then(getEgg)
  .catch(error => {
    return '🍕';
  })
  .then(cook)
  .then(console.log)
  • 계란을 받아오는 것은 실패했으나 피자를 대신 전달해주었기 때문에 promise chain이 실패하지 않고 🐓 => 🥚 => 🍳 대신 🍕 => 🍳 가 출력됨.
  • error가 발생해도 전체적인 promise chain에 문제가 발생하지 않도록 빵꾸처리 한 것.
  • .then(getEgg)에서 발생한 에러를 바로 처리하고 싶다면 그 밑에 catch를 작성한다.

콜백 지옥 🔥 Promise로 해결해보자

그럼 이제 위에서 보았던 콜백 지옥 코드promise를 사용하여 해결해보자

  • 불필요한 콜백함수 onSuccess, onError 를 삭제하고 프로미스로 대신했다.
class UserStorage {
  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) {
    return new Promise ((resolve, reject) => {
    setTimeout(() => {
      if (user === 'ellie') {
        resolve({ name: 'ellie', role: 'admin' });
      } else {
        reject(new Error('no access'));
      }
    }, 1000);
  });
 }
}

const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your password');

userStorage 
  .loginUser(id, password)           // (1)로그인 성공하면
  .then(userStorage.getRoles)        // (2)를 수행, 성공하면
  .then(user => alert(               // (3)을 수행 - 로그인이 잘 됐다는 메세지 출력
    `Hello ${user.name}, you have a ${user.role} role`))
  .catch(error => alert('error'));   // error 대응


📚 참고자료

profile
https://emewjin.github.io

0개의 댓글