[JavaScript] JavaScript의 비동기(Callback, Promise, Async-await)

Hannahhh·2022년 7월 27일
0

JavaScript

목록 보기
32/47

👀 Why Async?


  • Client = 서버로 접속하는 컴퓨터(my PC)
  • Server = 무언가(서비스, 리소스 등)을 제공하는 컴퓨터(웹 서버, 게임 서버)

Synchronous 처리의 경우, server가 요청을 받아 작업을 하는 동안 client는 아무것도 하지 않고 기다린다.

그러나, Asyncronous는 server가 작업을 하는 동안 client는 기존에 하던 작업을 마저 수행한다.


위의 이미지를 보면,

각각의 수행시간을 가진 4가지 task를 모두 처리한다고 했을 때, synchoronous는 총 500ms, Asynchoronous는 총 200ms(= 가장 긴 수행시간을 갖는 task)이다.

즉, 일처리를 할 때 더 효율적이다.



✔ callback


아래의 예제는 각 task의 수행시간을 랜덤함수를 통해 설정하고 한 번에 출력하는 코드이다. 따라서, 수행 순서가 항상 다르므로, 출력값 또한 항상 다르다.

const printString = (string) => {
    setTimeout(()=>{
        console.log(string)
    },
    Math.floor(Math.random() * 100) + 1
    )
}

const printAll = () =>{
    printString('A');
    printString('B');
    printString('C');
}

printAll();

그렇다면, 위의 코드를 수정하여 순차적으로 각 task를 제어하고 싶을 때는?

callback 함수를 사용하여 제어할 수 있다.

아래의 코드는 위의 예제에서 각 task가 순차적으로 수행될 수 있도록 callback함수를 이용해 수정한 코드이다.

const printString = (string, callback) => { // callback이 setTimeout의 callback()을 실행
    setTimeout(
        () => {
        console.log(string);
        callback(); //
    },
    Math.floor(Math.random() * 100) + 1
    )
}

const printAll = () =>{
    printString('A', () => { // 화살표 함수~16번째줄 까지 A의 callback
        printString('B', ()=>{ // 화살표 함수~15번째줄 까지 B의 callback
            printString('C',() => {}); // 화살표 함수부터 14번째 줄 끝까지 C의 callback
        })
    })
}

printAll(); // A, B, C

✔ callback 예시

Callback Error Handling Design and Usage

const somethingGonnaHappen = callback => {
    waitingUntilSomethingHappens();
    if (isSomethingGood) {
      callback(null, something);
    }
    if (isSomethingBad) {
      callback(something, null);
    }
  }
  
  somethingGonnaHappen((err, data) => {
    if (err) {
      console.log('ERROR!!');
      return;
    }
    return data;
  });

callback 함수는 유용하지만, 코드가 길어질 경우, 가독성이 떨어지고 관리하기가 어려워진다.(callback HELL 발생)

그러나, 아래의 promise를 사용하여 개선할 수 있다.



✔ Promise


callback HELL을 벗어나기 위한 테크닉으로,

Promise는 일종의 class이며, new Promise를 통해 instance가 만들어진다.

new Promise 안에서 resolve, reject라는 명령어를 통해서 다음 액션으로 넘어가거나 error handling을 할 수 있다.


아래의 코드는 위의 callback함수에서 다뤘던 코드를 promise를 이용하여 수정한 코드이다.

const printString = (string) => {
  // 새로운 promise instance를 return, promise만의 callback인 resolve, reject를 인자로 받음.
    return new Promise((resolve, reject) => { 
        setTimeout(
            () => {
            console.log(string);
            resolve(); // resolve() 수행
              // error handling할 게 없기때문에 reject 생략
        },
        Math.floor(Math.random() * 100) + 1
        )
    })
} // promise를 리턴하는 경우,

const printAll = () =>{
    printString('A') 
    .then(()=>{ // .then을 이용하여 이어나갈 수 있음. 코드가 평평해짐 -> 가독성 상승
        return printString('B')
    })
    .then(()=>{
        return printString('C')
    })
  // reject가 있다면 마지막부분에 .catch를 사용하여 handling 할 수 있음.
}

printAll(); // A, B, C

첫 task가 끝나고나면 .then을 이용하여 다음 task를 이어서 진행할 수 있으며, reject로 error 처리가 되는 경우, .catch를 이용해서 마지막 chaining에서 error를 처리할 수 있다.


그러나, callback을 해결하기 위해 promise를 사용했지만, promise 또한 promise화된 함수가 많아져 코드가 길어질 경우, promise HELL이 발생한다.

따라서, promise HELL을 방지하기 위해 위의 예시 코드 처럼 .then()함수 안에 return 처리를 제대로 해주어 방지할 수 있다.(Promise Chaining)

아래는 위의 예시코드 중 printAll()을 실행하는 코드로, Promise Chaining을 해준 코드이다.

// Promise Chaining
const printAll = () =>{
    printString('A') 
    .then(()=>{ // .then
        return printString('B') // return
    })
    .then(()=>{ // .then
        return printString('C') // return
    })
}

그러나 Promise Chaining 또한 길어지면 Promise Hell에 빠질 수도 있다.

따라서, 아래의 promise.all이나 await를 사용하여 방지할 수 있다.



✔ Promise.all

Promise.all은 동시에 두 개 이상의 Promise 요청을 한꺼번에 실행하는 함수로, 배열을 전달인자로 받는다.

Promise.all의 전달인자에 배열이 전달된다면, 요소 순서는 Promise.all의 Promise 순서와 상응하며,Promise.all 에 전달되는 Promise는 어느 하나라도 거부되는 순간 모든 요청이 거부된다.

따라서, 직전 Promise가 거부된 직후, 모든 Promise 요청이 거부되면서 .catch 메서드를 수행한다.



✔ Async await


await를 사용해 비동기 처리 함수를 마치 동기 처리 함수(일반함수)처럼 사용할 수 있다.

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(
      () => {
        console.log(string);
        resolve();
      }, 
      Math.floor(Math.random() * 100) + 1
    );
  });
};

const printAll = async () => { // async 함수라는 것을 반드시 표기해야함.
  const resultA = await printString("A"); // await
  const resultB = await printString("B"); // await
  const resultC = await printString("C"); // await
  return resultA, resultB, resultC;
};
printAll(); // A, B, C




🔥 실습 질문


1. Promise 실행 함수가 가지고 있는 두 개의 파라미터 resolve 와 reject 는 각각 무엇을 의미하나요?

//promise? => 함수 자체를 만들 때, callback을 인자로 받지 않고 새로운 promise인자를 return 하며, 자기만의 callback을 받는다.(아래 2개)

resolve: 성공하는 경우.
비동기 작업을 실행한 후, 작업이 끝나면 resolve를 호출한다.

reject: 실패하는 경우,
오류가 발생한 경우, reject를 호출해 거부한다.


2. resolve, reject함수에는 전달인자를 넘길 수 있습니다. 이때 넘기는 전달인자는 어떻게 사용할 수 있나요?

resolve: .then( )으로 이어나가게 한다.
앞의 함수 수행이 '끝나면' .then( )을 실행한다.(예- A끝나면 B수행, C끝나면, D수행...)

// 실습 코드에서는 sleep( )실행 후, .then( )실행하면 =>

promise 클릭 시, 제목 초기화&영상 재생
1초후, 영상멈춤&제목표시&Hello출력&world 출력 
5초후,
제목 강조
2초후,
제목 초기화

reject: 마지막에 .catch( )로 인자를 받는다.


3. new Promise()를 통해 생성한 Promise 인스턴스에는 어떤 메서드가 존재하나요? 각각은 어떤 용도인가요?

Promise.prototype.catch()
프로미스가 거부되면, catch( )를 호출하여 callback의 반환값을 받을 수 있다.

Promise.prototype.then()
프로미스가 이행되면, then( )을 이용해 반환값을 이용할 수 있고, 새 프로미스를 반환할 수 있다.

Promise.prototype.finally()
프로미스의 이행과 거부 여부에 상관없이 항상 호출되며, 이행한 값 그대로 이행하는 새로운 프로미스를 반환한다.


4. Promise.prototype.then 메서드는 무엇을 리턴하나요?

then()은 promise를 리턴하며 두 개의 콜백함수를 인수로 받는다.

Promise가 이행될 때, onfulfilled 함수를 인자로 받으며, onFulfiled 함수는 이행 값 하나를 인자로 받는다.
Promise가 거부될 때, onRejected 함수를 인자로 받으며, onRejected 함수는 거부 이유 하나를 인자로 받는다.

var p1 = new Promise(function(resolve, reject) {
  resolve("성공!");
  // 또는
  // reject("오류!");
});

p1.then(function(value) { // onfulfilled()
  console.log(value); // 성공!
}, function(reason) { // onRejected()
  console.log(reason); // 오류!
});

5. Promise.prototype.catch 메서드는 무엇을 리턴하나요?

catch()promise를 리턴하며, Promise가 거부된 사례만 리턴하고 처리한다.
promise.prototype.then과 동일하게 동작하며, 실제로 catch()에서 내부적으로 obj.then(undefined, onRejected)를 호출하는 것이다.

const p1 = new Promise(function(resolve, reject) {
  resolve('Success');
});

p1.then(function(value) {
  console.log(value); // "Success!"
  throw new Error('oh, no!');
}).catch(function(e) {
  console.error(e.message); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

6. Promise의 세 가지 상태는 각각 무엇이며, 어떤 의미를 가지나요?

  • 대기(Pending): 이행하지도, 거부하지도 않은 초기 상태
  • 이행(Fulfilled): 연산이 성공적으로 완료됨.
  • 거부(rejected): 연산이 실패함.

7. await 키워드 다음에 등장하는 함수 실행은 어떤 타입을 리턴할 경우에만 의미가 있나요?

Promise을 리턴하며, Promise에 의해 만족되는 값이 반환된다.

Promise가 아닌 경우에는 그 값 자체가 반환된다.

await 문은 Promisefulfill되거나 reject 될 때까지 async 함수의 실행을 일시 정지하고, Promisefulfill되면 async 함수를 일시 정지한 부분부터 실행한다.


8. await 키워드를 사용할 경우, 어떤 값이 리턴되나요?

await 문의 반환값은 Promise 에서 fulfill된 값이다.

만약 Promisereject되면, await 문은 reject된 값을 throw한다.

await 연산자 다음에 나오는 문의 값이 Promise가 아니면 해당 값을 resolved Promise로 변환시킨다.




Reference:
코드스테이츠
MDN:
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/await

0개의 댓글