비동기 프로그래밍 Callback 지옥 / Promise로 변환

Callback 함수란?

함수를 하나의 파라미터 인자로 전달하는데, 바로 실행되는게 아닌, 특정한 시점에 호출되는 함수를 말한다.

보통 콜백 함수는 함수의 매개변수로 전달하여 특정 시점에서 콜백 함수를 호출한다.

synchronous vs Asynchronous

  • 자바스크립트는 synchronous (동기적)이다.
  • 호이스팅(hoisting)이 된 이후부터, 코드가 작성한 순서에 맞춰서 하나하나 동기적으로 실행된다.
  • asynchronous 란, 비 동기적으로 언제 코드가 실행될지 예측할 수 없는 것을 말한다.
// asynchronous의 예
// web api 중 setTimeout() => 지정한 시간이 지나면 지정한 콜백함수를 호출 해주는 api
console.log('1');
setTimeout(function() {
  console.log('2'); 
}, 1000);
console.log('3')  // 1 -> 3 -> 2

Synchronous callback

즉각적으로 실행되는 콜백 함수 (동기 콜백)

function printImmediately (print) {
  print(); // print 함수 즉시 호출
} // function hoisting

printImmediately(()=> console.log('hello'));

Asynchronous callback

언제 호출될지 알 수 없는 콜백 함수, 특정 시점에 호출 되는 함수 (비동기 콜백)

function printWithDelay(print, timeout){
  setTimeout(print, timeout); // 일정 시간 이후 인자로 받은 print 함수 호출
}
printWithDelay(()=> console.log('async callback'), 2000); // 2초후 async callback 출력

콜백 지옥!

class UserStorage {
  loginUser(id, password, onSuccess, onError) {
    setTimeout(() => {
      if (
        (id === "dylan" && password === "test") ||
        (id === "coder" && password === "test")
      ) {
        onSuccess(id); // onSuccess() callback 호출
      } else {
        onError(new Error("not found")); // onError() callback 호출
      }
    }, 2000);
  }
  getRoles(user, onSuccess, onError) {
    setTimeout(() => {
      if (user === "dylan") {
        onSuccess({ name: "dylan", role: "admin" }); //onSuccess() callback 호출
      } else {
        onError(new Error("no access")); // onError() callback 호출
      }
    }, 1000);
  }
}

// 1. id, password 를 받아옴
// 2. 로그인이 성공적으로 됨
// 3. 로그인에 성공한 아이디를 받아와서 서버에 역할 요청

const userStorage = new UserStorage();
const id = prompt("enter your id");
const password = prompt("enter password");
userStorage.loginUser(
  id,
  password,
  (user) => { //onSuccess 롤백 함수
    userStorage.getRoles(
      user,
      (userWithRole) => {
        alert(
          `hello ${userWithRole.name}, you have a ${userWithRole.role} role`
        );
      },
      (error) => { //onError 롤백 함수
        console.log(error);
      }
    );
  },
  (error) => {
    console.log(error);
  }
);
  • 콜백 지옥에서의 문제점
    • 콜백을 이용해서, 콜백 함수안에서 다른 것을 호출하고 그 안에서 또 다른 콜백을 호출하는 등.. 가독성이 너무 떨어짐
    • 비즈니스 로직을 한눈에 이해하기 어려움

Promise

  • 자바스크립트에서 제공하는 비동기를 간편하게 처리할 수 있도록 도와주는 오브젝트이다.
  • 정해진 시간동안 기능을 수행하고 나서, 정상적으로 기능을 수행했다면 성공한 결과값을, 예상치 못한 문제가 발생했다면 에러를 전달해준다.
  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fullfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과값을 반환해준 상태
  • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

Pending(대기)

먼저 아래와 같이 new Promise() 메서드를 호출하면 대기(Pending) 상태가 됨

new Promise();

new Promise() 메서드를 호출할 때 콜백 함수를 선언할 수 있고, 콜백 함수의 인자는 resolvereject 를 사용한다.

const promise = new Promise(function(resolve, reject) {
  // doing some heavy work(network, read files)
  console.log('doing something..'); //실행해보면 바로 resolve가 동작하는걸 알수있다.
// Promise를 호출하는 순간 resolve 처리됨을 알수있음.
});
new Promise(function(resolve, reject) {
  // ...
});

Fulfilled(이행)

여기서 콜백 함수의 인자 resolve를 아래와 같이 실행하면 이행(Fulfilled) 상태가 된다.

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

그리고 이행 상태가 되면 아래와 같이 then()을 이용하여 처리 결과 값을 받을 수 있음.

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

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

Rejected(실패)

new Promise()로 프로미스 객체를 생성하면 콜백 함수 인자로 resolve와 reject를 사용할 수 있다. 여기서 reject를 아래와 같이 호출하면 실패(Rejected) 상태가 된다.

new Promise(function(resolve, reject) {
  reject();
});
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
});

그리고, 실패 상태가 되면 실패한 이유(실패 처리의 결과 값)를 catch()로 받을 수 있다.

// Promise is a JavaScript object for asynchronous operation
// State : pending -> fulfilled or rejected
// Producer vs Consumer

// 1. Producer
// When new Promise is created, the executor runs automaticailly. (새로운 promise가 만들어졌을때, executor가 자동으로 실행됨)

const promise = new Promise((resolve, reject) => {
  // doing some heavy work (네트워크에서 데이터를 받아오거나, 파일에서 큰 데이터를 읽어오는등의 시간이 걸리는 일은 비동기식으로 처리한다. 받아오는 동안 다음 라인의 코드가 실행되지 않기 때문)
  // network, read files..
  console.log("doing something..."); // doing something... (promise가 만들어지는 순간 실행)
  setTimeout(() => {
    resolve("Dylan"); //네트워크로부터 파일 받아오는걸 성공했을때 resolve라는 콜백함수를 호출(파라미터로 Dylan 전달)
    // reject(new Error("no network")); //실패했을때 에러 콜백함수
  }, 2000);
});

// 2. Consumers : then, catch, finally
promise
  .then((value) => {
    console.log(value);
  }) ////값이 정상적으로 잘 수행이 되었다면, value를 받아와서 원하는 함수 실행 (여기서 value는 'Dylan')
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log("finally"); // 성공했던 실패했던 상관없이 무조건 마지막에 실행
  });

Promise chaining

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

fetchNumber
  .then((num) => num * 2) // 2
  .then((num) => num * 3) // 6
  .then((num) => {
    return new Promise((resolve, reject) => { //return 값으로 새로운 promise 를 넘기는것도가능
      setTimeout(() => {
        resolve(num - 1); // 5
      }, 1000);
    });
  })
  .then((num) => console.log(num)); // 2초후 5 출력

Error Handling

const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve("🐔"), 1000);
  });
const getEgg = (hen) =>
  new Promise((resolve, reject) => {
    // setTimeout(() => resolve(`${hen} => 🥚`), 1000);
    setTimeout(() => reject(new Error(`error! `)), 1000);
  });
const cook = (egg) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} => 🍳`), 1000);
  });

getHen()
  .then(getEgg) // 받아오는 리턴값을 그대로 전달할때는 생략가능 .then((hen) => getEgg(hen))와 같음
  .catch((error) => {
    return "🐥"; //getEgg에서 값을 얻어오는걸 실패했을때 에러 처리, 뒤에 문장들은 실행됨
  }) // 에러 핸들링 (getEgg가 실패했을시, 🐥로 대체)
  .then(cook) //.then((egg) => cook(egg))
  .then(console.log) //.then((meal) => console.log(meal)); //🐥 ->  🍳
  .catch(console.log);

앞선 콜백 지옥 예제 해결하기

class UserStorage {
  loginUser(id, password) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (
          (id === "dylan" && password === "test") ||
          (id === "coder" && password === "test")
        ) {
          resolve(id);
        } else {
          reject(new Error("not found"));
        }
      }, 2000);
    });
  }
  getRoles(user) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (user === "dylan") {
          resolve({ name: "dylan", role: "admin" });
        } else {
          reject(new Error("no access"));
        }
      }, 1000);
    });
  }
}

const userStorage = new UserStorage();
const id = prompt("enter your id");
const password = prompt("enter password");
userStorage
  .loginUser(id, password)
  .then(userStorage.getRoles)
  .then((user) => alert(`hello ${user.name}, you have a ${user.role} role`))
  .catch(console.log);