[JavaScript] 비동기프로그래밍

zmin·2022년 5월 8일
0

모던 자바스크립트 Deep Dive, 이웅모

싱글 스레드 자바스크립트

자바스크립트 엔진은 싱글 스레드 방식
그래서 한 가지 작업이 끝날 때까지 다른 작업을 할 수 없음(Blocking)

function sleep(delay){
  const finishTime = Date.now() + delay;
  
  // 시간이 될 때까지 계속 loop를 돌게 됨 -> 함수가 계속 실행 중
  while(Date.now() < finishTime);
}

sleep(3*1000);
console.log('finish');

위 코드에서 sleep 함수 속 while loop가 끝나 콜 스택에서 해당 함수의 실행 컨택스트가 제거될 때까지 console.log('finish');는 실행되지 못하고 기다리게 됨

그래서 자바스크립트가 실행되는 환경들(브라우저, Node.js 등)에서 비동기 작업이 가능하도록 지원

브라우저의 비동기작업

  • 타이머
  • 이벤트 핸들러
  • WebAPI

와 같은 작업은 비동기적으로 진행

→ 브라우저는 이벤트 루프테스크 큐라는 것을 지원

setTimeout과 같은 타이머 함수, 이벤트 핸들러 호출, HTTP 요청에 대한 코드를 평가하고 실행하는 것까진 자바스크립트 엔진이 수행하고
해당 작업들에 대해 응답을 기다리고 타이머를 설정하고 콜백함수를 호출하는 등의 비동기 작업은 브라우저가 실행

  • 자바스크립트 엔진
    • 콜 스택 (= 실행 컨텍스트 스택) : 함수가 실행될 때 실행 컨텍스트가 생성되어 푸쉬되는 곳, 실제로 작업순서를 정하는 위치라고 보면 될듯
    • 힙 : 객체가 저장되는 메모리 공간 → 동적할당, 실행 컨텍스트가 이를 참조
  • 브라우저
    • 태스크 : 브라우저에서 처리가 끝난 후(타이머 완료, 이벤트 발생 등) 실행 시키고자 하는 콜백 함수, 이벤트 핸들러가 보관되는 곳
    • 이벤트 루프 : 태스크 큐에 대기 중인 함수가 있는지 + 콜 스택이 비었는지 계속해서 확인
      콜 스택이 비어있는 경우 태스크 큐의 함수를 FIFO로 이동시킴 → 실행
const foo = () => {
  console.log(Date.now()+' - foo end');
  console.log();
};

console.log(Date.now());
const t = Date.now() + 10000;

setTimeout(foo, 1000);

while(Date.now() < t);
console.log(Date.now()+' - global end');

생각대로라면 setTimeout에 의해 1000ms가 지난 후 함수 foo가 호출되어 'foo end' 가 출력되어야 하지만...

실제로는 while loop 때문에 아직 콜 스택에 아직 전역 컨텍스트가 실행되고 있음
그래서 foo는 이미 타이머가 끝나서 테스크 큐에 들어와있음에도 콜 스택이 비어있지 않아 계속 기다리게 됨

'global end' 까지 출력되고 난 뒤에 'foo end' 출력

순수하게 콜백 함수 사용

비동기적 작업의 대표 예시
주의할 것은 모든 콜백함수가 비동기적으로 수행되는 것은 아님

사람이 직접 호출하는 것이 아니라 코드(비동기 함수)가 스스로 판단하여 콜백함수를 호출하는 것, 콜백 함수의 제어권은 사람에게 있지 않음

setTimeoutsetInterval 같은 타이머, 이벤트 핸들러 등이 콜백 함수를 사용

const callBackFunc = () => {
  console.log('1초가 지났습니다');
}

setInterval(callBackFunc, 1000);

서버와 통신하는 XMLHttpRequest의 경우에도 서버에서 답이 오면 그제서야 onload 이벤트 핸들러가 실행되는데 이 또한 태스크 큐에 저장되어 대기하게 됨 → 비동기적인 작업

콜백 지옥

비동기 함수는 상위 스코프와의 상호작용이 불가능
비동기 처리 결과 반환X, 상위 스코프 변수 할당X

따라서 뭔가 해당 결과에 대한 처리가 필요하다면 비동기 함수 내부에서 모두 처리해야 하는데 일반적으로 콜백함수를 전달하여 사용하는 경우가 많음
→ 비동기 함수 내에서 비동기 함수를 호출하고 또 그 안에서 비동기 함수를 호출하고... 해당 함수들 처리 과정에서 콜백함수도 호출하고...

코드와 실행 과정이 아주아주 복잡해짐

에러 처리

보통 에러를 처리할 때 '해당 코드를 실행했을 때(try) 에러가 발생하면 이 코드를 실행한다(catch)'try-catch문을 많이 사용

그런데 만약 try문에 비동기 함수가 존재하고, 그 함수의 콜백함수에서 에러가 발생한다면 이는 catch되지 않음

try {
  setTimeout(()=> 에러 , 1000);
} catch(e) {
  console.error('에러발생', e); // 실행되지 않음
}

에러가 발생하는 콜백함수는 현재 실행중인 실행 컨텍스트(try-catch문 포함)가 종료된 후에 실행되게 되므로 이미 try 문에 에러가 없다고 판단한 뒤에 실행

∴ 콜백함수 에러 처리 불가

Promise

위와 같은 콜백 함수의 단점을 보완하기 위해 ES6에서 등장(콜백패턴을 사용)
비동기 처리 상태와 처리 결과를 담고있는 객체

Promise는 new 연산자를 이용하여 생성 가능하고 보통 비동기 함수의 처리 결과로 사용하기 때문에 다음과 같이 사용

new Promise((resolve, reject) => {
  // 
  if ( /* 성공적으로 비동기 처리가 된 경우 */ ){
    resolve();
  }
  else {	/* 안 된 경우(오류, 원하는 값이 아님) */
    reject();
  }
}

프로미스의 상태값은 총 세 가지

  • pending : 프로미스가 생성된 직후, 비동기 처리 전
  • fulfilled : 비동기 처리가 수행되고 결과 값을 처리했을 때 성공한 경우 resolve 함수가 실행되며 pendingfulfilled
  • rejected : 비동기 처리가 수행되고 결과 값을 처리했을 때 실패한 경우 reject 함수가 실행되며 pendingrejected

위 상태값을 가지는 프로미스들이 반환

후속 처리 메서드

모두 프로미스를 반환하며 비동기로 동작
메서드의 인수로는 보통 이를 호출한 프로미스의 처리 결과가 인수로 전달

∴ 메서드를 연속적으로 사용하는 것이 가능(프로미스 체이닝)
∴ 각 비동기 함수에 대한 에러처리 가능

후속 처리 메서드에 전달된 콜백함수들은 마이크로태스크 큐에 저장 -> 프로미스를 반환하기 때문

then

두 개의 함수를 인수로 받아 해당 프로미스의 상태값에 따라 지정된 함수를 수행하는 메서드

/*
 Promise의 상태값이 fulfilled : 콜백함수 asyncSuccess 호출, 인수로 결과값 전달
 Promise의 상태값이 rejected : 콜백함수 asyncFail 호출, 인수로 에러
*/
new Promise((resolve, reject) => { /*...*/ })
  .then(asyncSuccess, asyncFail);

자신이 인수로 받아 실행한 콜백함수에 대한 처리 결과와 상태를 담은 프로미스를 반환

catch

then과 동일하나 rejected에 대한 처리만 진행

// 아래 두 코드는 같음
new Promise((resolve, reject) => { /*...*/ })
  .then(undefined, asyncFail);

new Promise((resolve, reject) => { /*...*/ })
  .catch(asyncFail);

보통 프로미스 체이닝의 가장 마지막에 호출하여 전체 프로미스 체인에 대한 에러 처리를 하는 것에 사용
에러 처리 목적이라면 then보다 가독성도 좋음

finally

프로미스 상태값에 관련 없이 콜백함수를 무조건 한 번 실행하는 메서드

new Promise((resolve, reject) => { /*...*/ })
  .finally(func);

정적 메서드

프로미스를 요소로 가지는 이터러블을 인수로 받아 병렬로 한꺼번에 실행하며
메소드에 따라 처리한 결과를 담은 새로운 프로미스를 반환

  • Promise.all

    • 모두 fulfilled 상태가 됐을 때 결과 값을 담은 배열을 resolve하는 프로미스 반환 (하나라도 rejected가 되면 즉시 종료)
    • 처리순서 보장(인수로 넘겨진 프로미스의 인덱스와 결과값의 인덱스가 동일)
  • Promise.race

    • 가장 먼저 fulfilled 상태가 되는 프로미스의 결과를 resolve하는 프로미스를 반환
  • Promise.allSettled

    • 비동기 처리가 수행된 모든 프로미스의 결과를 배열에 담아 프로미스로 반환
    • 상태와는 관련 없음

async/await

프로미스를 이용하게 되지만 후속 처리 메서드를 사용하지 않음
∴ 콜백패턴을 사용하지 않는 방식 → 가독성이 좋음

함수 선언시 async 키워드를 이용하여 비동기 처리가 발생할 것을 알리고 이 내부에서 await키워드를 사용하여 비동기 처리를 할 수 있음

// 함수 선언문, 표현식 사용시 function 키워드 앞에 사용
async function(){}
const test = async function(){};

// 화살표 함수 사용시 파라미터 선언 앞에 사용
const func = async param => {
  // await 키워드는 무조건 프로미스 앞에서 사용
  const a = await new Promise(resolve => setTimeout(()=>resolve('await'), 2000));
  console.log(a);
};

await 키워드를 try문에 포함되도록 하여 함수 내부에 try-catch문을 추가하여 에러처리가 가능함
또한 async 함수가 프로미스를 반환하기 때문에 실행 값에 대한 후속처리 메서드도 사용가능


위와 같은 자바스크립트의 비동기 프로그래밍 원리를 시각적으로 확인할 수 있는 사이트
http://latentflip.com/loupe

profile
308 Permanent Redirect

0개의 댓글