비동기 ~ Promise ~ async ~ keywords

Jihoon Han·2021년 9월 12일
1

동기(synchronous) VS 비동기(asynchronous)

JavaScript는 기본적으로 동기적이다. hoisting이 된 이후에 작성한 순서대로 동기적으로 실행된다.
(hoisting? 변수 및 함수 선언이 자동적으로 제일 우선 되는 것)
비동기 처리를 하게 되면, 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하게 된다.
예) setTimeout(): Web API의 한종류, setTimeout(function, delay)의 구조로 사용


Callback 콜백함수

일단 함수를 등록하기만 하고, 어떤 이벤트가 발생하거나 특정 시점에 도달했을 때 시스템에서 호출하는 함수.

  • 콜백지옥: 비동기 처리 로직을 위해 콜백 함수들을 계속 묶어나간(nesting) 코드.
    문제점: ① 가독성이 떨어진다. ② 유지보수가 어렵다.

👉 이를 해결하기 위한 방법으로 Promiseasync가 있다.


Promise

Promise는 자바스크립트에 내장되어 있는 class 형태의 Object이다.
시간이 걸리는 일들(network, read files)은 Promise를 사용하여 비동기적으로 처리하는 것이 좋다.

  • state: 프로세스가 실행 중인지, 완료가 되어서 성공/실패했는지 등의 상태
    pending: Promise가 만들어져서 우리가 지정한 operation이 수행 중인 상태
    fulfilled: operation을 끝내게 되어 결과 값을 반환해준 상태
    rejected: 파일을 찾을 수 없거나 네트워크에 문제가 생겨 실패하거나 오류가 발생한 상태

  • Producer: 원하는 기능을 수행해서 해당하는 데이터를 만들어낸다.
    new Promise((resolve, reject함수) => {})
    resolve의 경우: resolve(값||Promise)
    reject의 경우: reject(new Error()) Error라는 클래스는 자바스크립트에서 제공하는 object 중의 하나이다.
    ❗ Promise를 만드는 순간, 전달한 executor라는 콜백함수가 바로 실행되는 점을 유의하자.

  • Comsumer: Producer로 생산한 데이터를 소비한다.
    promise.then(resolve에서 전달된 값) (성공적인 경우)
    .catch() (에러가 발생한 경우, then 바로 아래에 작성해서 대처가 가능하게 한다)
    .finally()(성공, 실패에 상관없이 무조건 호출)
    이렇게 .then().catch().finally()와 같은 방식을 chaining이라고 한다.

  • chaining

   getMoney()
   .then(money => buyMusic(money))
   .catch(error => { return 'card'; })
   .then(music => play(music))
   .then(enjoy => console.log(enjoy));
 // 👇 받아오는 value로 하나의 콜백 함수를  바로 호출 할 경우는 생략 가능
   getMoney()
   .then(buyMusic)
   .catch(error => { return 'card'; })
   .then(play)
   .then(console.log);

async/await

Promise들을 여러개 chaining하게 되면 코드가 난잡해지기 때문에 동기식에서 순서대로 작성하는 것처럼 간편하게 작성할 수 있도록 해주는 API이다.

  • Syntactic Sugar
    문법적인 기능은 그대로 유지하되, 코드를 작성하는 사람 입장에서 혹은 그 코드를 다시 읽는 사람의 입장에서 편의성이 높은 프로그래밍 문법을 의미한다.

  • async
    Promise 안에는 꼭 resolve나 reject를 이용해서 완료를 해야한다.
    또한 Promise에서의 콜백지옥이 생기는 경우도 있다.
    이를 더 간단하게 해주는 것이 async이다.
    async function 함수 앞에 async라는 키워드를 붙여주면 자동으로 함수 안의 블록들이 Promise로 변환된다.

  • await
    await이라는 키워드는 async가 붙은 함수 안에서만 쓸 수 있다.

   function delay(ms) {
     return new Promise(resolce => setTimeout(resolve, ms));
   }
   async function callMonkey() {
     await delay(1000);
     throw 'error';
     return '🐵';
   }
   async function callPanda() {
     await delay(1000);
     return '🐼';
   }
   async function callAnimals() {
     const monkeyPromise = callMonkey();
     const pandaPromise = callPanda();
     const monkey = await monkeyPromise;
     const panda = await pandaPromise;  //-> await의 병렬 처리(dalay 시간이 따로 적용되지 않도록)
     return `${monkey} + ${panda}`;
   }
   getAnimals().then(console.log);

   // 위의 병렬 처리된 부분을 더 깔끔하게 도와주는 것이 Promise.all이다.
   function callAllAnimals() {
     return Promise.all([callMonkey(), callPanda()])
   // -> Promise 배열을 전달하게 되면 모든 Promise들이 병렬적으로 다 받을 때까지 모아준다.
     .then(animals => animals.join('+')
     );
   }
   callAllAnimals().then(console.log);

비동기 핵심 KeyWords

자바스크립트는 single-thread 기반의 언어이다. 즉, 자바스크립트는 하나의 호출 스택을 가진다. 그렇다면 어떻게 동시성을 지원하는 것일까?
비동기로 동작하는 핵심요소는 자바스크립트 언어가 이니라 브라우저가 가지고 있다.
브라우저의 동작은 아래 그림으로 표현할 수 있다.

  • Event Loop: Call StackTask Queue를 감시하며 Call Stack이 비어 있을 경우, Task queue의 첫번째 콜백을 꺼내 Call Stack에 추가한다.

  • Web API: DOM, AJAX, setTimeout 등 브라우저가 제공하는 API.

  • Task Queue: 이벤트 발생 시 실행해야 할 callback 함수가 Task Queue에 추가된다.

  • Call Stack: 실행된 코드의 환경을 저장하는 자료 구조.
    엔진이 처음 script를 실행할 때, Global Execution Context를 생성하고 이를 Call Stack에 push한다. 그 후 엔진이 함수를 호출할 때 마다 함수를 위한 Execution Context를 생성하고 이를 Call Stack에 push한다. 자바스크립트 엔진은 Call StackTop에 위치한 함수를 실행하며 함수가 종료되면 stack에서 제거(pop)하고 제어를 다음 Top에 위치한 함수로 이동한다.

setTimeout으로 보는 예

console.log('hana');

setTimeout(() => {
  console.log('uno');
}, 3000);

console.log('first');

// hana
// first
// uno
  1. 우선 console.log('hana') 구문이 CallStack에 push되고 실행된 뒤 pop 된다. 그럼 콘솔창에는 hello가 출력된다.
  2. 다음은 setTimeout을 호출한다. setTimeout은 브라우저에서 제공하는 API이다. 때문에 Web API로 호출된다. 이 때 CallStack에서는 setTimeout 호출 자체는 완료되었으므로 스택에서 pop한다.
  3. 다음은 console.log('first'); 구문이 CallStack에 들어오고 실행되고 다시 지워진다. 동시에 WEB API에서는 이전에 호출했던 setTimeout의 타이머가 작동 중이다.
  4. 모든 Web API는 작동이 완료되면 콜백을 Task Queue로 넣는다.
  5. 이벤트 루프는 CallStackTask Queue의 상태를 체크하면서 스택이 비어있을 경우 Task Queue의 첫번째 콜백을 CallStack에 넣는다.
  6. 이제야 console.log('uno')가 CallStack에 들어와 실행된다.
profile
달려라 코린이!!

0개의 댓글