본문 바로가기

JavaScript

2023.12.25 기록 ( callback, promise, async, await)

서버와 통신을 배우며, 비동기 과정을 학습하여 javascript가 동작하는 순서를 익히는 것도 필요해졌다.

 

가장먼저 callback 함수라는 것의 이론정리가 필요했다.

 

JS엔진은 기본적으로 함수를 호이스팅, 출력, 호이스팅 된 함수 출력, 서버요청, 출력 이런방식으로 진행된다.

 

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

 

*화살표 함수는 원칙적으로 호이스팅 되지않는다. setTimeout이 비동기적으로 작동하기 때문에 event queue에서 대기하게 되는데 이 부분이 화살표 함수가 호이스팅 된 탓인지 착각할 수 있다.

 

위의 함수는 1과 3이 출력된 이후 1초를 기다리고 2를 출력하게된다.

 

콜백 함수는 요청하고, 받아오는 것

서버나 브라우저에 값을 요청하고 그 값을 어떻게 받을 것인지를 정한다. 

// function printGameName(print){
//   print();
// }

const printGameName = (print) => print();
printGameName(() => console.log('Reverse1999'));

 

다음의 코드는 서버에 요청하고 동기처리를 거쳐 Javascript에 기본 규칙인 위에서 아래로 실행되는 순서에 따라 출력한다.

 

setTimeout(()=>console.log('Godgame'), 1000);

 

다음의 코드는 1000ms이라는 시간을 지정 한 뒤 메시지를 출력한다. 와 같은 함수들은 동기 처리가 된 이후 순서를 기다리게 되는 것이다.

 

그런데 만약 비동기 처리가 된 함수가 많다면 매번 출력되는 순서를 각각의 시간에 설정해서 두어야 하는 것일까.

 

그럴 수도 있지만, 그를 위한 콜백함수가 따로 존재한다.

 

콜백함수의 존재의의는 비동기 처리가 끝난 뒤, 즉 비동기 함수가 포함된 코드를 순서에 맞게 처리 할 수 있도록 보장해줄 수 있다.

class UserStorage {
  loginUser(id, password, onSuccess, onError) {
    setTimeout(() => {
      if (
        (id === 'sotheby' && password === 'potion') ||
        (id === 'sonetto' && password === 'support')
      ) {
        onSuccess(id);
      } else {
        onError(new Error('not found'));
      }
    }, 2000)
  }

  getRoles(user, onSuccess, onError) {
    setTimeout(() => {
      if (user === 'sotheby') {
        onSuccess({ name: 'sotheby', role: 'healer' });
      } 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);
  }
);

 

 id와 password를 주고 로그인 시, 아이디와 역할을 알림창으로 띄워주는 매커니즘을 만들었다고 할 때,

성공하면 onSuccess 를 실행하고, 실패시 onError와 error를 띄우는 문장이다.

 

이러한 과정은 비동기로 이루어지며, 누적될수록 문장이 굉장히 길어지는 단점이 있다.

 

이 부분은 promise 오브젝트를 이용하면 조금 더 간략하게 표현 할 수 있다.


class UserStorage {
  loginUser(id, password) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (
          (id === 'sotheby' && password === 'potion') ||
          (id === 'sonetto' && password === 'vertin')
        ) {
          resolve(id);
        } else {
          reject(new Error('not found'));
        }
      }, 2000)
    });
  }

  getRoles(user) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (user === 'sotheby') {
          resolve({ name: 'sotheby', role: 'healer' });
        } 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(userStorage.getRoles)
  .then(user => alert(`hello ${user.name}, you have a ${user.role} role`))
  .catch(console.log)

 

가장먼저 onSucces, onError, userWithRole 부분이 사라졌다.

promise를 이용하면 자체적으로 resolve와 reject로 구현이 되므로 따로 선언할 필요가 없어지는 것이다.

그에 맞는 then과 catch부분도 존재한다.

resolve일 때 then을 실행하고, reject일때 catch로 어떻게 처리할 지를 정하는 것이다.

 

promise 체이닝시 (연속으로 promise 데이터를 받아오는 과정을 의미한다.) 인자가 같은 경우 생략이 가능한데

위의 코드에서는 다음의 문장이 축소되었다.

  .then(userStorage.getRoles)
  //.then (user => userStorage.getRoles)

 

getRoles는 이미 함수이며 ( promise를 가지고 있다.) 이 getRoles가 호출되기 위해서는 loginUser의 resolve를 받아야 한다.

따라서 getRoles는 이미 user를 받고 있으므로 적지 않고 생략해줘도 무관하다.

 

마찬가지로

.then(alert(`hello ${user.name}, you have a ${user.role} role`))

//.then(user => alert(`hello ${user.name}, you have a ${user.role} role`))

이렇게 생략도 가능하지만, 호이스팅이 되어 실행될 때 getRoles 함수 부분이 먼저 실행이 '완료'  되어있어야 하므로 오류가 발생할 수 있다. 따라서 이 부분은 한 번 더 명시해 사용하게 된다.


 

위의 promise 또한 async , await 으로 정리가 가능한데,  에이싱크, 어웨잇 으로 읽는다 (어싱크 ㄴㄴ)

promise를 선언하지 않고 함수 앞에 async를 붙이는 것 만으로도 해결이 된다.

 

function delay(ms){
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function getcleardrop() {
  await delay(2000);
  return '☔';
}

async function getdust() {
  await delay(2000);
  return '🪙';
}

async function levelup() {
  const rain = await getcleardrop();
  const dust = await getdust();
  return `${rain} + ${dust}`;
}

levelup().then(console.log);

 

delay라는 함수를 선언하고

async 함수를 선언하면 자동으로 promise로 적용이 된다.

await는 async를 사용중인 함수 내에서만 사용이 가능한데, 뒤에 선언된 시간만큼 (즉, promise가 끝날 때까지) 기다려준 뒤 그 다음 문장을 실행하게 된다.

 

그런데, 다음과 같은 문장에서 await가 중복되게 되면, 받아오는 시간동안 순차적으로 기다리게 된다.

(즉 getcleardrop과 getdust가 서로 delay 2000ms만큼을 각각 기다린 뒤 실행되게 된다)

 

그때, 각 promise가 서로에게 영향을 주는 것이 없다면 병렬적으로 실행한다면 더욱 빠르게 데이터를 받아올 수 있는데

다음과 같은 함수를 사용하게 된다.

function getlevelup(){
  return Promise.all([getcleardrop(), getdust()])
  .then(level => level.join(' + '));
}
levelup().then(console.log);

 

Promise.all 같은 경우는 promise를 배열에 넣어 모두 병렬적으로 실행해서 모든 값을 받아두고,

배열을 문자열로 묶는 join이라는 메서드를 이용해서 묶어주게 되어도 같은 값이 된다.

 

다만 이 과정에서 순차적으로 실행 후, 정렬하는 것과 병렬적으로 실행 후 정렬하는것의 차이는 거의 나지 않았다.

이유는 요소가 2개인 탓에 각각 2000ms 만 기다리면 되기 때문이다. 하지만 추후 더 많은 작업을 한 번에 진행하게 될 때는

확실하게 Promise.all 통해 진행하는 것이 좋다고 하였으니, 이 부분이 체감이 될 때 다시 적게 될 것 같다.