1. 동기와 비동기

js는 동기적인 언어입니다. hoisting(var, function 선언이 최상단으로 올라가는 것)이 된 이후 부터 코드가 나타나는 순서대로 실행이 됩니다. callback 함수에 대해 다시 알아봅니다.
callback함수는 우리가 전달해준 함수를 원할때 다시 실행시켜 달라는 의미입니다. browserAPI인 setTimeout의 정의를 살펴보면 handeler라는 callback함수를 파라미터로 받는 것을 볼 수 있습니다.

console.log('1');
setTimeout(function () {
  console.log('2');
  }, 1000);
console.log('3');

image 절차대로 1을 찍고 2에서는 browserAPI를 만났으므로 browser에서 1초의 시간이 지난 후 2를 출력하라는 신호를 보내게 되어 콘솔이 아래와 같이 찍히게 됩니다. 이것이 async 입니다.

image

1.1. 콜백 정리

콜백은 비동기적 상황에서만 사용하는 것은 아닙니다. 비동기와 동기 콜백으로 나뉘어 집니다. 위의 자바스크립트와 합쳐서 생각해봅니다.

// 동기적인 콜백
function printImmeditaely(print) {  // 함수는 hositng됩니다.
  print();
}
printImmediately(()=>console.log('hello'));

image

이제 비동기적 콜백을 알아봅니다.

function printWithDelay(print, timeout){
  setTimeout(print, timeout);
}
printWithDelay(()=>console.log('async callback'), 2000);

image

1.2. 콜백 지옥(콜백 )

콜백은 유용하지만 너무 남발하게되면 콜백지옥에 빠지기 쉽습니다. 콜백지옥을 체험하는 코드를 작성해봅니다.

// 콜백지옥 로그인 성공시 성공했다는 onSuccess 콜백함수, 에러시 onError 콜백 함수, 로그인 성공시 역할을 불러오는 함수실행
class UserStorage {
  loginUser(id, password, onSuccess, onError) {
    setTimeout(()=>{
      if(로그인 로직 생략){
        onSuccess(id)
      }else{
        onError(new Error('not found')); 
      }      
    }, 2000);
  }
  
  getRoles(user, onSuccess, onError) {
    setTimeout(()=>{
      if(user === 'lala') {
        onSuccess({ name: 'lala', 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 => {
    userStorage.getRoles(
      user,
      userWithRole => {
        alert(`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`);
    },
    error => {
      console.log(error);
     }
    );
  },
  error => {
    console.log(error);
  }
);

image

이처럼 콜백안에 콜백을 계속 전달하는 것을 콜백 체인이라하며 이는 가독성이 매우 떨어집니다.

2. promise

promise는 callback을 사용하지 않고 비동기코드를 처리할 수 있는 API입니다. promise는 js안에 내장된 object입니다.

2.1. state

promise의 상태에는 pending(진행중), fulfilled(성공), rejected(실패) 상태가 있습니다. resolve 처리를 해주지 않으면 계속 pending 상태가 됩니다.

2.2. producer

const promise = new Promise(resolve, reject) => { // 결과가 성공일 때 호출하는 resolve, 오류일때 실행되는 reject
  // doing executor something(network, read files...)
  resolve('lala');
  reject(new Error('no network'));
})

promise object 생성시 executor함수가 바로 실행됩니다. executor 안에는 상태에 따라 호출되는 resolve와 reject함수가 들어갑니다. 만약 버튼을 눌렀을 때 비동기동작을 해야한다면, promise가 생성되는 동시에 executor가 실행되는 점에 주의해야 합니다.

2.3. consumers

then, catch, finally를 통해 값을 받아 올 수 있습니다.

promise
  .then((vlaue) => { // 올바르게 promise를 수행하면 value로 resolve 콜백함수에 전달한 값이 파라미터로 들어옵니다.
    console.log(value);
  })
  .catch(error => { // reject의 error 를 전달 받아 처리합니다.
    console.log(error); 
  })
  .finally(()=>{console.log('finaylly')});  // 무조건 한번 실행됩니다.

image

2.4. promise chaining

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

fetchNumber
  .then(num => num *2)
  .then(num => num *3)
  .then(num => {
    return new Promise((resolve, reject) => {   // 새로운 promise 전달
      setTimeout(() => resolve(num -1), 1000);
    });
  })
  .then(num => console.log(num));

image

2.5. call back 지옥 함수 리팩토링

1.2. 에서 보았던 콜백 지옥 함수를 promise chainnig으로 변경해 봅니다.

class UserStorage {
  loginUser(id, password) {
    return new Promise((resolve, reject) => {
      setTimeout(()=>{
        if(로그인 로직 생략){
          resolve(id);
        }else{
          reject(new Error('not found'));
        }      
      }, 2000);
    });
  }
  
  getRoles(user) {
    return new Promise((resolve, reject) => {
      setTimeout(()=>{
        if(user === 'lala') {
          resolve({ name: 'lala', 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)
  .then(user => userStorage.getRoles) // 인자가 같다면 생략가능
  .then(alert(`Hello ${user.name}, you have a ${user.role} role!`)) // user => 를 생략
  .catch(console.log);

3. async와 await

async와 await는 promise를 좀더 간편하고 동기적으로 실행되는 것 처럼 보이게 만들어줍니다. 계속 then으로 chaining을 하게되면 코드가 난잡해질 수도 있습니다. 이때 좀더 간편한 async, await API를 사용하면 간결하게 사용할 수 있도록 도와줍니다. async와 await은 promise위에 존재하는 API입니다. 이런 API를 syntactic sugar라고 합니다.

3.1. async

function fetchUser() {
  // do network request in 10 secs.. 오래 걸리는 코드라고 가정
  return 'lala';
}

const user = fetchUser();
console.log(user);

위와 같은 코드는 동기적으로 수행되어 10초동안 멈춰있다가 남은 데이터를 모두 보여주게 됩니다. 이럴때는 비동기 처리를 하여 보여줄 수 있는 페이지는 먼저 보여준 후, 오래걸리는 작업을 비동기 처리를 해주는게 좋습니다.

function fetchUser() {
  return new Promise((resolve, rejected) => {
    // do network request in 10secs...
    resolve 'lala'; // resolve를 사용하지않으면 계속 pending 상태
  });
}

const user = fetchUser();
user.then(console.log);
console.log(user);

위의 비동기적 코드를 async API를 이용하여 더 간결하게 바꿀 수 있습니다. aync는 promise를 만들어주는 키워드라고 볼 수 있습니다.

async function fetchUser() {
  // do network request in 10 secs...
  return 'lala';
}

const user = fetchUser();
user.then(console.log);
console.log(user);

3.2. await

await는 async가 붙은 함수에서만 사용할 수 있습니다. await 키워드를 사용하게 되면 then을 사용하지 않고 일정시간 기다린 후 값을 return시킬 수 있습니다.
image

promise도 chaining을 반복적으로 사용하면 콜백지옥에 빠질 수 있습니다.
image

이러한 코드도 async API를 사용하면 간단하게 작성 할 수 있습니다.
image

3.3. await 병렬처리

비동기들을 각각 병렬처리를 하도록 개선해봅니다. async 키워드를 붙이면 promise를 만드므로 execute가 바로 실행되는 점을 참고합니다.
image

위의 코드는 예시를 위한 코드일 뿐, 병렬처리를 위한 promise API를 지원합니다.(all, race)
image

댓글남기기