Callback Hell & Promise & Async - await

동기와 비동기 차이

  • 자바스크립트는 동기적인 언어호이스팅 된 이후부터 코드가 작성한 순서에 맞춰 하나씩 동기적으로 실행된다.
  • 호이스팅? var 변수, function declaration이 제일 위 상단부로 올라가는 현상
  • 정해진 순서에 맞게 코드 실행되는 것이 동기적 실행 방식
<script>
console.log(1)
console.log(2)
console.log(3)
//1 2 3
</script>
  • 언제 코드가 실행될 지 예측할 수 없는 것이 비동기적 실행 방식
<script>
console.log(1)
setTimeout(function(){
	consoel.log('2')
}, 1000)
//	setTimeout(()=>consoel.log('2'), 1000)
// 보통 콜백 함수 arrow function으로 전달한다.
console.log(2)
console.log(3)
//1 3 2
</script>
  • setTimeout브라우저에서 제공되는 Web API지정한 시간이 지나면 전달한 함수를 실행시켜준다.
  • 콜백함수전달한 함수를 나중에다 불러주는 개념이다.
  • 이처럼 자바스크립트 엔진이 코드를 위에서 부터 밑으로 실행하다 setTimeout (브라우저 API)을 만나면 브라우저에게 1초 뒤에 콜백함수를 실행하라는 명령을 내리고 콘솔 창에 1, 3 로그가 찍히고 1초가 지나면 전달한 콜백함수의 내용인 2가 콘솔에 찍히게 된다.

- 콜백함수

  • 여기서 setTimeout에 전달한 함수바로 실행되는 것이 아니라 setTimeout 함수 안에 파라미터 인자로 지정해 만든 함수를 전달해준다
  • 따라서 지금 당장 실행하는게 아닌 일정한 시간이 지난 다음에 함수를 나중에 콜(불러)하여 실행해달라는 의미에서 Callback, 즉 나중에 다시 불러 서 전달하는 이런 함수를 콜백함수라고 한다.

동기적 콜백함수

  • 즉각적으로 동기적으로 실행한 콜백함수
<script>
console.log(1)
setTimeout(()=>consoel.log('2'), 1000)
// 보통 콜백 함수 arrow function으로 전달한다.
console.log(2)
console.log(3)
// synchronous callback
function printImmediately(print){
  print();
} 
printImmediately(()=>console.log('sync hello'))
// 1, 3, hello, 2
</script>
  • 함수의 선언을 가장 상단에 올리고 1 출력하고 setTimeout가 브라우저에 요청하고 3을 출력한 뒤 즉시 동기적 콜백함수 출력한 뒤 1초 뒤에 setTimeout에 전달한 콜백 함수의 내용인 2가 출력된다.

비동기적 콜백함수

  • 나중에 언제 실행될지 예측할 수 없는 비동기적으로 실행한 콜백함수
<script>
console.log(1)
setTimeout(()=>consoel.log('2'), 1000)
// 보통 콜백 함수 arrow function으로 전달한다.
console.log(2)
console.log(3)
// Asynchronous callback
function  (print, timeout) {
  setTimeout(print, timeout);
}
printWithDelay(()=>console.log('async callback'),2000)
// 1, 3, 2, async callback
</script>
  • 함수의 선언을 역시 가장 상단에 올리고 1을 출력하고 setTimeout을 브라우저에게 요청한 뒤 3을 출력하고 1초에 시간이 지나면 setTimeout에 전달한 2가 출력되고 2초가 되면 비동기적 콜백 함수를 실행하여 'async callback'이 출력된다.
  • 이는 함수에 전달한 비동기적 콜백 함수 print와 timeout 인자를 setTimeout에게 요청하여 2초뒤에 콜백함수가 호출된 것이다.

콜백지옥?!

  • 콜백 함수를 계속해서 묶어나가 nesting 하면서 사용해 콜백 함수 안에 다른 콜백 함수를 부르고 부르고 부르는 과정을 반복해서 사용되는 모습을 보고 콜백 지옥이라는 말이 나왔다.

  • 콜백 지옥 예시

  • 사용자의 데이터를 서버에서 받아오는 class가 존재

  • UserStorage에는 2개의 API가 있는데 하나는 loginUser 함수로 사용자를 로그인 하기 위해 id, psw 받는다.

  • 나머지 하난 getRoles 함수로 사용자의 데이터를 받아 사용자마다 가진 요청을 서버에게 요청에서 받는다.

<script>
// Callback Hell example
class UserStorage{
  // 사용자의 데이터를 서버에게서 데이터를 받아오는 클래스
  loginUser(id, psw, onSuc, onErr){
    // 사용자로부터 id,psw받아 로그인하고 로그인 이뤄지면 onSuc 콜백함수를, 실패시 onErr 콜백함수를 호출
    setTimeout(()=>{
      if (
        (id === 'ellie' && psw === 'dream') ||
        (id === 'coder' && psw === 'academy')
      ) {
        onSuc(id);
        // 아이디 비밀번호가 위 조건에 맞으면 전달한 onSuc 콜백함수에 id전달
      } else {
        onErr(new Error('not found'));
        // 만약 여기에 포함되지 않는 경우라면 onErr콜백을 불러주면서 
        // Error라는 오브젝트 만들어'not found' 전달
      }
    },2000);
  }
  getRoles(user, onSuc, onErr){
    //사용자의 데이터를 받아서 사용자 마다 가지고 admin, guest처럼 역할을 서버에게 다시 용청해서 정보를 받아오는 api
    setTimeout(()=>{
      if (user === 'ellie') {
        onSuc({ name: 'ellie', role: 'admin' });
      } else {
        onErr(new Error('no access'));
                // Error라는 오브젝트를 만들어서 'no access' 전달해 줄 거에요
      }
    },1000);

  }

}

const userStorage = new UserStorage();
const id = prompt('ent your id');
const psw = prompt('ent your psw');

userStorage.loginUser(
  id, 
  psw, 
  user => {
    userStorage.getRoles(
      user, 
      userWithRole=>{
        alert(`welcome ${userWithRole.name}, you have a ${userWithRole.role} role`)
      },
      err => {

      }
    );
  }, 
  error => {console.log(err)}
);
</script>

콜백 지옥을 대체하기 위해, 어떻게하면 깔끔하게 작성할 수 있는지알아보자.

Promise

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


예를 들어서 코딩 아카데미에서 지금 준비중인 코스가 있는데 언제 코스가 완료 될지 모르는 시점이다.

하지만 관심있는 학생분들은 이렇게 이메일을 통해서 미리 등록할 수 있는 시스템이 있다고 가정하에 만약 학생A가 등록을 완료하게 되면 시간이 지난 다음에 코스가 오픈 되면 바로 학생A에게 메일로 공지를 받을 수 있게된다.


반면 학생B는 수업이 이미 오픈된 뒤에 뒤늦게 사전 공지 창을 발견하여 뒤늦게 이메일 주소를 입력하고 등록을 하게된다.

하지만 수업은 이미 오픈이 되었기 때문에 기다릴 필요가 없이 학생B에게 이렇게 메일로 공지가 오게되고 수업에 바로 참여할 수가 있게된다.

어떻게 콜백을 쓰지 않고 프로미스 오브젝트를 통해서 비동기 코드를 깔끔하게 처리할 수 있는지에 대해서 알아보자!

  1. 프로미스

Promise는 자바 스크립트 안에 내장 되어져 있는 오브젝트이다.
Promise는 오브젝트는 asynchronous operation을 위해서 쓰이는데 바로 비동기 적인 것을 수행할 때 콜백 함수 대신에 유용하게 쓸 수 있는 오브젝트이다.

프로미스에 2가지 포인트

  • 첫 번째는 스테이 상태, Promise가 무거운 오퍼레이션을 수행하고 있는 중인지 아니면
    이 기능 수행이 완료가 되어서 성공했는지 실패했는지 이런 상태에 이해해야한다.

-두번째는 producer와 consumer의 차이점을 아는 것으로 우리가 원하는 데이터를 producing하는 (정보를 제공하는) producer와 이 제공된 데이터를 소비하는, 필요로 하는 사람 consumer 두 가지의 차이점을 잘이해해야한다.

// State: pending -> fulfilled or rejected
-Promise의 상태는 Promise가 만들어져서 지정한 오퍼레이션이 수행 중일 땐 Promise의 상태이며 오퍼레이션을 성공적으로 다 끝내게 되면 fulfilled, 완벽하게 완료한 상태가 된다. 혹은 파일을 찾을 수가 없거나 네트워크에 문제가 생긴다면 rejected 상태가 된다.

-그리고 Promise는 원하는 기능을 수행해서 해당하는 데이터를 만들어 내는 producer와 오브젝트, 원하는 데이터를 소비하는 consumer 오브젝트로 나누어 진다.

  1. producer와
    Promise는 클래스이기 때문에 new 키워드를 이용해서 오브젝트를 생성하고
    Promise의 생성자를 보면 executor 라는 콜백 함수를 전달해주는데 executor 콜백함수는 또 다른 두 가지 콜백함수를 받는다.
    기능을 정상적으로 수행해서 마지막에 최종 데이터를 전달하는 resolve 콜백함수,
    기능을 수행하다가 중간에 문제가 생기면 호출하게 될 reject 콜백함수로 나눠진다.

보통 Promise는 헤비한 일들을 주로한다.
왜냐하면 네트워크에서 데이터를 받거나 파일에서 큰 데이터를 읽어오는 과정이 시간이 꽤 걸리는데 그런 것을 동기적으로 처리하게 되면 파일을 읽어오고 네트워크에서 데이터를 받아오는 동안 다음 라인에 코드가 오게되더라도 네트워크에서 데이터를 받아오는 동안에는 그 다음 라인에 코드가 실행되지 않기 때문이다. 따라서 시간이 걸리는 일들은 이렇게 프로미스를 만들어 비동기적으로 처리하는 것이 좋다.

즉 그래서 네트워크 통신 하던지 파일을 읽어서 오는 등의 것들은 비동기적인 처리를 하는 것이 좋다.

  1. producer

Promise를 만드는 순간 우리가 전달한 executor 콜백 함수가 바로 실행된다.
이말은 이 Promise 안에 네트워크 통신을 하는 코드를 작성했다면 Promise 가 만들어지는 그 순간 바로 네트워크 통신을 수행하게 된다.
따라서 중요한 건 네트워크 요청을 사용자가 요구 했을 때만 해야 되는 경우라면 예를 들어 사용자가 버튼을 눌렀을 때 네트워크 요청을 해야 되는 경우에는 이러한 식으로 작성하게 되면 사용자가 요구하지도 않았는데 불필요한 네트워크 통신이 일어나게된다.

// when new Promise is created, the executor runs automatically.
const promise = new Promise((resolve, reject)=> {
  console.log('doing sth')
}) ;

-Promise 만들어보기
이제 Promise 안에서 네트워크 통제 늘 하는 것처럼 셋타임아웃을 함수로 딜레이를 주어 우리가 원하는 콜백 함수를 몇 초 정도 있다가 불러오면 그 콜백함수 안에서는 기능을 성공적으로 해내면 resolve라는 콜백함수를 호출하도록 설계한다.
우리가 데이터를 받아오고 사용자의 이름이 ~이다라고 성공적으로 네트워크 에서 받아오고 또는 파일에서 읽어온 그리고 그것들을 가공한 데이터를 resolve라는 콜백함수를 통해서 전달하면 된다.

  1. Consumers: then, catch, finally, (promise 사용하기)
    then, catch, finally를 이용해서 값을 받아올 수 있다.
    Promise 값이 정상적으로 잘 수행이 된다면 then 그러면 이제 값, value을 받아와 원하는 기능을 수행하는 콜백함수를 전달해준다. 이 value 라는 파라미터는 Promise 가 정상적으로 잘 수행이 되어서 마지막으로 resolve 콜백 함수에서 전달된 사용자의 이름 값이 들어온다

then이라는 건 프로미스 가 정상적으로 잘 수행이 되어서 마지막에 최종적으로 리졸브 라는
콜백 함수를 통해서 전달한 이 값이 여기 밸리의 파라미터로 전달 되어져서 들어오는 걸 볼 수가 있어요

유의할 건 Promise의 then을 호출하게되면 then은 결국 똑같은 Promise를 return하기 때문에 return된 Promise에 catch를 다시 호출 할 수 있는 것이다. 이러한 것을 chaining이라고 한다. array에서 map도 똑같은 array를 리턴하여 chaining할 수 있듯이 promise도 Promise에서도 then 호출은 Promise를 return하고 다시 return된 Promise에 catch를 등록할 수 가 있다.

'use strict';

const promise = new Promise((resolve, reject)=> {
  // promise 오브젝트를 만들 때 비동기적으로 수행 하고 싶은 그런 기능들을 블록안에 작성
  // 성공적으로 잘했다면 resolve를 호출하고
  // 실패했다면 reject를 호출해 왜 실패했는지 에러를 전달해준다.
  console.log('doing sth')
  setTimeout(()=> {
    resolve('jamie');
    //reject(new Error('no network'));
  }, 2000)
}) ;

// 이후 promise를 이용해then과 catch를 이용해 성공한 값 실패한 에러를 받아와서 원하는 방식으로 처리
promise
// then api는 성공적인 케이스를 다루고
.then((value)=>{
  console.log(value)
})
// catch api 함수는 에러가 발생했을 때 어떻게 처리할 것인지를 콜백함수로 등록한다.
.catch(error =>{
  console.log(error)
})
// finally는 성공 실패 상관없이 무조건 마지막에 호출되어 어떠한 기능을 마지막으로 수행하고 싶을 때 사용한다
.finally(()=>{
  console.log('finally')
});
    1. Promise chaining
const fetchNumber = new Promise((resolve, reject)=>{
  setTimeout(()=> resolve(1), 1000)

})
fetchNumber
.then(num => num * 2)
.then(num => num * 3)
// return값을 다른 서버에 보내서 다른 숫자로 변환된 값을 받아온다
.then(num => {
  return new Promise((resolve, reject)=>{
    setTimeout(()=>resolve(num -1), 1000)
  })
})
.then(num => console.log(num))

then은 값을 바로 전달할 수도 있고 또 다른 비동기인 Promise를 전달할 수 도 있다.

이렇게 then, then, then, then처럼 여러 가지를 동시에 비동기적인 아이들을 묶어서 처리할 수 도 있다.

  1. Error Handling
    Promise를 chaining했을때 어떻게 에러를 핸들링 할 수 있는지에 대해서

// 3개의 프로미스를 리턴하하는 함수
const getHen = () => 
  //암탉을 받아서 1초후에 닭을 받아온다.  
  new Promise((resolve, reject)=>{
    setTimeout(() => resolve('🐓'), 1000);
});

// 치킨을 받아서 그 치킨 으로부터 달걀을 얻어오는 프로미스를 리턴하게
const getEgg = (hen) => 
  new Promise((resolve, reject)=>{
  setTimeout(() => resolve((`${hen} => 🥚`)), 1000);
});

//  세번째 쿡은 달걀을 받아와서 달걀을 가지고 프라이드 egg를 만드는 함수 3가지
const cook = (egg) => 
  new Promise((resolve, reject)=>{
    setTimeout(() => resolve(`${egg} => 🍳`), 1000);
  });

//콜백 함수를 전달할 때 받아오는 밸류를 다른 함수에도 바로 호출하는 경우에는 
// .then(getEgg) 생략가능한다.
getHen()
.then(hen => getEgg(hen))
.then(egg => cook(egg))
.then(meal => console.log(meal))
/*
getHen()
.then(getEgg)
.then(cook)
.then(console.log);
*/ 처럼 줄여서 쓸수도 잇따.
const getHen = () => 
  //암탉을 받아서 1초후에 닭을 받아온다.  
  new Promise((resolve, reject)=>{
    setTimeout(() => resolve('🐓'), 1000);
});

// 치킨을 받아서 그 치킨 으로부터 달걀을 얻어오는 프로미스를 리턴하게
const getEgg = (hen) => 
  new Promise((resolve, reject)=>{
  //만약 이 달걀을 받아오는 부분에서 네트워크에 문제가 생겨서 실패 시 리젝트 콜백함수에 에러 오브젝트를 전달하고 이러한 에러를 마찬가지로 핸들링해준다.
  setTimeout(() => reject(new Error(`error ! ${hen} => 🥚`)), 1000);
});

//  세번째 쿡은 달걀을 받아와서 달걀을 가지고 프라이드 egg를 만드는 함수 3가지
const cook = (egg) => 
  new Promise((resolve, reject)=>{
    setTimeout(() => resolve(`${egg} => 🍳`), 1000);
 });
  
getHen()
.then(getEgg)
//우리가 달걀을 받아올 때 생긴 문제를 다른 재료로 대체할 수 있는데 
catch(error =>{
// 다른 것을 전달해 주면
//빵 => 🍳 대체된다.
  return '빵';
})
.then(cook)
.then(console.log)
.catch(console.log)
// 처럼 줄여서 쓸수도 잇따.  
  

5.callback hell -> promise로 재작성해보기

'use strict';
const promise = new Promise((resolve, reject) =>{

})
// Callback Hell example
class UserStorage{
  loginUser(id, psw){
    return new Promise((resolve, reject) => {
      setTimeout(()=>{
        if (
          (id === 'ellie' && psw === 'dream') ||
          (id === 'coder' && psw === '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('ent your id');
const psw = prompt('ent your psw');
userStorage.loginUser(id, psw)
.then(userStorage.getRoles)
.then(user => alert(`welcome ${user.name}, you have a ${user.role} role`))
.catch(console.log);

좋은 웹페이지 즐겨찾기