[ JavaScript ] 자바스크립트의 비동기 처리

ma.caron_g·2022년 12월 21일
0

Java Script

목록 보기
11/11
post-thumbnail

<참고 링크>

[ 비동기 함수란? ]

자바스크립트는 싱글 스레드이기 때문에 한 번에 하나의 작업만을 수행할 수 있습니다.

이를 해결하기 위해서 비동기라는 개념이 생겼습니다.

비동기란, 특정 코드의 처리가 끝나기 전에 다음 코드를 실행할 수 있는 것을 뜻합니다.

즉, 일 할 사람(스레드)이 한 명 늘어난다고 생각하면 됩니다.


(출처 : https://learnjs.vlpt.us/async/ )

자바스크립트는 즉시 처리하지 못 하는 이벤트들을 이벤트 루프에 모아 놓고, 먼저 처리해야하는 이벤트를 실행합니다.

자바스크립트에서 가장 대표적인 비동기 처리 사례에는 setTimeout()이 있으며 일정 시간 뒤에 함수를 실행시킵니다.

[ 비동기 종류 ]

setTimeout(), setInterval(), HTTP request, DOM Event

console.log도 비동기
console.* 메서드는 공식적으로 자바스크립트의 일부분이 아닙니다. 정확히는 호스팅 환경에 추가된 기능입니다.
따라서 console.log() 메소드는 브라우저의 유형과 상황에 따라 출력할 데이터가 만들어진 직후에도 콘솔창에 바로 뜨지 않을 수 있습니다.
브라우저가 console을 비동기적으로 처리해야 성능상 유리하기 때문입니다.
예상치 못한 결과값이 콘솔에 표시될 때에는 콘솔의 실행 지연으로 인한 원인일 가능성을 염두에 두어야합니다

console.log('Start')
setTimeout(function() {
    console.log('5초 후 실행')
}, 5000)

console.log('End');

위 코드는
다음과 같은 결과가 나옵니다.

Start
End
5초 후 실행

스레드(Thread)
스레드는 어떠한 프로그램이 실행되는 작업을 의미합니다.
싱글 스레드는 한 번에 하나의 작업만 수행할 수 이으며, 멀티 스레드는 한 번에 여러 개의 작업을 수행할 수 있습니다. 이는 이벤트 루프(Event Loop) 덕분입니다.


[ 이벤트 루프 ]


(출처 : https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5 )

다음 사진은 브라우저 환경을 그린 사진입니다.
이벤트 루프는 자바스크립트가 아닌 브라우저에 내장되어 있는 기능 중 하나입니다.
즉, 자바스크립트는 싱글스레드이지만, 브라우저에서는 이벤트 루프 덕분에 멀티 스레드로 동작하여 비동기 작업이 가능합니다.

  • 힙 메모리(Heap Meomory)
    메모리 할당이 일어나는 곳
  • 콜 스택(Call Stack)
    힙에 저장된 객체를 참조하여, 호출된 코드(함수)의 정보를 저장하고 실행하는 곳
  • 태스크 큐(Task Queue)*
    setTimeout이나 setInterval과 같은 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역
    태스크 큐에 일시적으로 보관된 함수들은 비동기 처리 방식으로 동작합니다.
  • 이벤트 루프
    이벤트 루프의 역할은 콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지, 그리고 태스크 큐에 대기 중인 함수가 있는지 계속해서 확인합니다.
    만약, 콜 스택이 비어있고, 태스큐 큐에 대기 중인 함수가 있다면 이벤트 루프는 순차적으로 태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킵니다.
    그리고 이렇게 반복되는 매 순회(Iterator)를 tick이라고 부릅니다.

하지만 만약 콜 스택에 while(true)나 React에 useEffect()의 두 번째 인자를 작성하지 않아 무한 호출되는 함수가 존재한다면, 콜 스택이 감당할 수 있는 범위를 초과합니다 이렇게 된다면, 브라우저의 동작이 멈춰버리니 주의해야합니다.

console.log('Hi');
setTimeout(function cb1() {
  console.log('cb1');
}, 5000);
console.log('Bye');

다음 코드의 실행을 표현하자면

콜 스택에서 바로 큐로 넘어가는게 아니라 중간에 Web APIs를 한 번 거쳐 큐로 넘어갑니다 이는 어떤 함수나 이벤트가 종료될 때까지 시간이 오래 걸릴 수 있기 때문에, 자바스크립트 엔진이 직접 처리하는 것이 아니라 브라우저에 위임합니다. 위 예제에서는 setTimeout()함수가 5초 뒤에 실행되기 때문에, Web APIs가 해당 연산을 마치고 (5초 후) 콜 스택에서 바로 실행될 수 있는 상태가 되었을 때 큐에 등록합니다.


[ 비동기 방식 ]

자바스크립트에는 콜백 함수, Promise, async await 이렇게 크게 3가지 비동기 방식이 존재합니다.

[ 비동기 처리 방식을 더 자세하게 풀어보자 ]

참고 - 모던 자바스크립트 Deep Dive


아래의 함수는 어떻게 동작할까?
천천히 생각해보자.

const foo = () => console.log('foo');
const bar = () => console.log('bar');

setTimeout(foo, 0);
bar();
  1. 전역 코드가 평가되어 전역 실행 컨텍스트가 생성되고 콜 스택에 push됩니다.
  1. 전역 코드가 실행되기 시작하며 setTimeout 함수가 호출됩니다.
    이때, setTimeout함수의 함수 실행 컨텍스트가 생성되고 콜 스택에 push되어 현재 실행 중인 실행 컨텍스트가 됩니다.
    브라우저의 Web API인 타이머 함수도 함수이므로 함수 실행 컨텍스트를 생성합니다.
  1. setTimeout 함수가 실행되면 콜백 함수를 호출 스케줄링하고 종료되어 콜스택에 pop됩니다.
    이때, 호출 스케줄링, 즉 타이머 설정과 타이머가 만료되면 콜백 함수를 태스크 큐에 표시하는 것은 브라우저의 역할입니다.
  1. 브라우저가 수행하는 4-1과 자바스크립트 엔진이 수행하는 4-2는 병렬로 처리됩니다.
    4-1) 브라우저는 타이머를 설정하고 타이머의 만료를 기다립니다.
    이후 타이머가 만료되면 콜백 함수 foo가 태스크 큐에 push됩니다.
    위 예제의 경우 시간이 0이지만, 지연 시간이 4ms 이하인 경우(eg.크롬 브라우저) 최소 지연 시간 4ms가 지정됩니다.
    따라서 4ms후에 콜백 함수 foo가 태스크 큐에 push되어 대기하게 됩니다. 이 처리 또한 자바스크립트 엔진이 아니라 브라우저가 수행합니다.
    이처럼 setTimeout 함수로 호출 스케줄링한 콜백 함수는 정확히 지연 시간 후에 호출된다는 보장은 없습니다.
    지연 시간 이후에 콜백 함수가 태스크 큐에 푸시되어 대기하게 되지만 콜 스택이 비어야 호출되므로 약간의 시간차가 발생할 수 있기 때문입니다.
    4-2) bar가 호출되어 bar 함수의 실행 컨텍스트가 실행되고 콜 스택에 푸시되어 현재 실행 중인 실행 컨텍스트가 됩니다.
    이후, bar 함수가 종료되어 콜 스택에 pop됩니다. 이때 브라우저가 타이머를 설정한 후 4ms가 경과했다면 foo 함수는 아직 태스크 큐에서 대기 중입니다.
  1. 전역 코드 실행이 종료되고 전역 실행 콘텍스트가 콜 스택에서 pop됩니다. 이로서 콜 스택에는 아무런 실행 컨텍스트가 존재하지 않게 됩니다.
  1. 이벤트 루프에 의해 콜 스택이 비어있음이 감지되고 태스크 큐에 대기 중인 콜백 함수 foo가 이벤트 루프에 의해 콜 스택에 push됩니다.
    즉, 콜백 함수 foo함수 실행 컨텍스트가 생성되고 콜 스택에 push되어 현재 실행 중인 실행 컨텍스트가 됩니다.
    이후 foo 함수가 종료되어 콜 스택에서 pop됩니다.

setTimeout()이 바로 실행되지 않는 이유를 이젠 이해할 수 있습니다.
대기시간을 0초에 걸어두어 바로 실행되지 않는 이유

그래도 이해가 되지 않는다면...
그 유명한 Philip Roberts의 What the heck is the event loop (12분 50초) 영상을 보고 오자!

[ 이벤트 루프는 어떤 형태일까? ]

앞서 본 예제에서 setTimeout의 콜백 함수는 태스크 큐에 푸시되어 대기하다가 콜 스택이 비어있으면
"아직 할 일이 남아있다며" 태스크 큐에 있던 실행 컨텍스트를 콜 스택에 push합니다.

이벤트 루프의 역할을 코드로 간단히 나타내자면

const eventLoop = [];
const event = null;

whilte(true) {
  // 콜 스택이 비어있는지 계속 확인
  if(eventLoop.Length > 0) {
    event = eventLoop.shift(); // 가장 먼저 들어온 것부터 스케줄링 합니다.
    try {
      event(); // 이벤트 루프에 있던 함수를 실행.
    } catch(e) {
      reportError(e);
    }
  }
}

MDN에서 이벤트 루프를 작성하면 다음과 같습니다.

while(queue.waitForMessage()) {
  queue.processNextMessage();
}

이벤트 루프는 매번 순회하면서 콜 스택이 깨끗한지 체크합니다.
이걸 Tick이라고 합니다.
setTimeout()과 같은 함수는 타이머만 설정할 뿐, 타이머가 끝나면 환경이 콜백을 이벤트 루프에 삽입한 뒤 틱에서 콜백을 꺼내어 실행하는 것입니다.
setTimeout()에 인자로 넘긴 지연 시간이 지켜지지 않은 이유가 여기에 있습니다.
setTimeout(()=>console.log, 0)에서 0은 보장된 시간이 아니라 요청을 처리하기 위해 필요한 최소의 시간입니다.
이벤트 루프는 '현재 실행 중인 태스크가 없는지', '태스크 큐에 태스크가 있는지' 확인하며 매번 Tick하면서 기회를 엿보고 있을 것입니다.





[ Promise와 이벤트 루프 ]

ES6부터 추가된 Promise를 알아보겠습니다.

이제 우린 setTimeout(foo, 0)같은 것들이 바로 실행되지 않는 이유를 알게되었습니다.
하지만 Promise가 들어간 코드는 어떻게 실행될까?

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve()
  .then(function() {
    console.log('promise1');
  })
  .then(function() {
    console.log('promise2');
});

console.log('script end');

출력 결과는 다음과 같습니다.

script start
script end
promise1
promise2
setTimeout

setTimeout()의 콜백이 Promise 콜백보다 느리게 동작할까요?

[ ES6 microtask queue ]

Microtak queue(혹은 Job queue)는 ES6에서 Promise와 함께 소개된 개념입니다.
마이크로 테스크 큐는 태스크 큐와 다른 별도의 큐입니다.
마이크로 태스크 큐를 사용하는 대표적인 함수가 Promise입니다.

기존의 태스크 큐 = 매크로태스크(Macrotask) 큐라고 합니다.

Macrotask queue를 이용하는 함수
: setTimeout(), setInterval(), setImmediate(), requestAnimationFrame, I/O, UI 렌더링


Microtask queue를 이용하는 함수 process.nextTick(), Promise, queueMicrotask

마이크로태스크 큐는 기존의 태스크 큐와 비교해서 보다 우선순위가 높습니다.
따라서, 이벤트 루프는 콜 스택이 비면 먼저 마이크로태스크 큐에서 대기하고 있는 함수를 가져와서 실행합니다.
그리고 마이크로태스크 큐가 빈 후에야 태스크 큐에서 대기하고 있는 하수를 가져와서 실행합니다.

이미지로 보면 다음과 같습니다.

이미지를 보면 콜 스택이 비면 우선순위가 높은 Microtask Queue(마이크로태스크 큐)부터 처리하고
Microtask queue가 비면 그떄야 Macrotask Queue(매크로태스크 큐)를 실행합니다.

정리하자면

  • Macrotask queue (= task queue, message queue)
    • HTML 파싱, DOM 생성, 메인 스레드를 구성하는 JS code, 그리고 페이지 로드나 네트워크 이벤트, 타이머와 같은 여러 이벤트를 포함합니다.
    • 지연 시간이 0초인 setTimeout()으로 새로운 매크로태스크를 스케줄링 할 수 있습니다.
  • Microtask queue (= job queue)
    • Promise의 후속 처리 메서드의 콜백 함수를 처리한다.
    • 다른 이벤트 핸들링이나 렌더링 혹은 또 다른 매크로태스크가 실행되기 전에 완료됩니다.
    • 마이크로태스크는 바로 다음 마이크로태스크를 실행합니다. 따라서, 그 사이에는 UI 혹은 네트워크 변화가 업습니다.
    • 브라우저가 리렌더 되기 전에 실행되므로, 마이크로태스크 큐의 작업이 늦게 처리된다면 브라우저의 UI렌더링이 지연될 수 있습니다.




[ 이벤트 루프를 막지 말자 ]

Think about async.
Don't block the event loop.
-philip Roberts-

우선 event loop를 막지 않아야 합니다.

스택에 필요없는 느린 코드를 쌓아서 브라우저가 할 일을 못하게 하지 말아야합니다.
예를 들어, 콜 스택에 어떤 함수가 너무 오랫동안 실행되고 있으면 이벤트 루프가 메시지큐를 확인하지 않습니다.
그러면 함수의 동작이 길어져서 화면을 클릭하더라도 이벤트가 발생하지 않고, 화면이 버벅거리거나 심한 경우 동작하지 않는 문제가 발생합니다.
따라서 함수의 단위는 작게 잘라서 작성하는 것이 좋습니다.


또 너무 오래 걸리는 작업이 있다면 앞서 배운 setTimeout(callback, 0)과 같은 문법으로 지연시키는 방법도 있습니다.
태스크 큐로 callback을 넘겨주면서 적절하게 태스크를 분산시키는 것입니다.


특히, 이미지 처리나 애니메이션이 너무 잦아졌을 때, 큐 관리에 주의를 기울여야합니다.
이 경우 싱글스레드인 자바스크립트의 단점을 보완해서 멀티스레딩을 가능하게 해주는 웹 워커 API를 활용하는 것도 방법이 될 수 있습니다.

profile
다른 사람이 만든 것을 소비하는 활동보다, 내가 생산적인 활동을 하는 시간이 더 많도록 생활화 하자.

0개의 댓글