반응형

*잘못된 부분이 있으면 피드백 주시면 감사하겠습니다!

 

자바스크립트에서 타이머 이벤트를 쓸 수 있는 방법으로는 setTimeout, setInterval 두가지가 있다.

 

이 함수들이 어떻게 비동기를 지원할까?

 

자바스크립트는 단일 쓰레드 기반의 언어이지만 이벤트 루프를 사용해서 동시성을 지원할 수 있다.

타이머를 실행시키면 Web API에서 타이머의 기한이 끝날때까지 대기하다가 테스크 큐로 들어오게 된다.

콜스택이 비었다면 테스크 큐에서 테스크를 가져와 실행한다.

 

이 말이 무엇인가를 다시 풀어써보기로 하자.

우선 Web API가 뭔지부터 알아야 할것 같다.

Web API 와 Javascript 런타임 

런타임이란 해당 프로그램 언어가 작성된 코드가 구동되는 환경을 뜻하고, 웹 브라우저나 Node.js 가 대표적 JS 런타임이다. 그리고 JS 이외에 다양한 기능을 제공하기 위해서 브라우저에서는 웹 API가 여러 함수(setTimeout등)을 지원하고, Node에서는 화면의 예시처럼 fs 등 함수를 지원하게 된다. 

 

브라우저 환경 구조 

자바스크립트 엔진

 

- Heap : 객체들은 힙 메모리에 할당된다. 크기가 동적으로 변하는 값들의 참조 값을 갖고 있다.
- Call Stack : 함수 호출 시, 실행 컨텍스트가 생성되며, 이러한 실행 컨텍스트들이 콜 스택을 구성한다.

 

Web API or Browser API


- 웹 브라우저에 구현된 API이다.
- DOM event, AJAX, Timer 등이 있다.

 

이벤트 루프

- 콜 스택이 비었다면, 태스크 큐에 있는 콜백 함수를 처리한다. (이후 자세히 설명)

 

태스크 큐

- 비동기 함수(setTimeout, setInterval 등)는 처리 전 이곳에서 대기하게 된다. 
- 이벤트 루프는 하나 이상의 태스크 큐를 갖는다.
- 태스크 큐는 태스크의 Set이다.
- 이벤트 루프가 큐의 첫 번째 태스크를 가져오는 것이 아니라, 태스크 큐에서 실행 가능한(runnable) 첫 번째 태스크를 가져오는 것이다. 태스크 중에서 가장 오래된 태스크를 가져온다.

 

(태스크 큐 관련해서 Microtask Queue, Animation Frames라는 세부적인 개념도 있는데 생략한다)

 

참고)

https://velog.io/@titu/JavaScript-Task-Queue%EB%A7%90%EA%B3%A0-%EB%8B%A4%EB%A5%B8-%ED%81%90%EA%B0%80-%EB%8D%94-%EC%9E%88%EB%8B%A4%EA%B3%A0-MicroTask-Queue-Animation-Frames-Render-Queue

 

[JavaScript] Task Queue말고 다른 큐가 더 있다고? (MicroTask Queue, Animation Frames)

자바스크립트에서 비동기 함수가 동작하는 원리에 대해서 공부했다면, Task Queue에 대해 들어보았을 것이다. Task Queue는 Web API가 수행한 비동기 함수를 넘겨받아 Event Loop가 해당 함수를 Call Stack에

velog.io

 

싱글 쓰레드와 이벤트 루프

 

setTimeout같은 타이머는 JS 엔진이 아니라 외부에 구현되어 있다!

 

그리고, 자바스크립트가 단일 쓰레드 기반이라는 말이 정확히는 '자바스크립트 엔진'은 단일 호출 스택을 사용한다 라는 것이다.

 

위 그림같은 경우는 Node.js는 비동기 IO를 지원하기 위해 libuv 라이브러리를 사용하며, 이 libuv가 이벤트 루프를 제공한다. 브라우저에서는 Web API가 setTimeout 같은 함수를 지원하며, 타이머를 JS의 싱글 쓰레드가 아닌, 브라우저의 자체 쓰레드에서 세다가 자바스크립트의 테스크 큐에 넣어주게 된다. 

 

즉, 실제 자바스크립트가 구동되는 환경(브라우저, Node.js)는 여러개의 쓰레드를 사용하고, 이 다중 쓰레드 환경과 자바스크립트 엔진이 상호 연동하기 위해 내부적으로 이벤트 루프를 이용해 처리하게 된다.

 

(또 다른 예시로는 Node.js에서 I/O를 처리할때 자바스크립트의 싱글 쓰레드로만 동작하는게 아니고, 커널 쓰레드에서 처리하는것?이라 볼 수 있을것 같다?)

 

그럼 자바스크립트가 어떻게 비동기 함수를 처리하는지, 이벤트 루프를 어떻게 사용하는지 예시를 보자

 

function delay() {
    for (var i = 0; i < 100000; i++);
}
function foo() {
    delay();
    bar();
    console.log('foo!'); // (3)
}
function bar() {
    delay();
    console.log('bar!'); // (2)
}
function baz() {
    console.log('baz!'); // (4)
}

setTimeout(baz, 10); // (1)
foo();

 

위 코드를 작동시키면 setTimeout으로 타이머를 걸어놓은 baz가 정확히 10ms 이후에 실행되지 않는다.

 

그 이유는 자바스크립트가 비동기 함수를 지원하는 방식은 '이벤트 루프'를 사용해서 구현하고,

이벤트 루프는 '콜스텍에 현재 실행중인 task가 없는지'와 'task 큐에 태스크가 있는지'를 반복적으로 확인한다.

그리고 현재 실행중인 테스크가 없다면 이제서야 테스크 큐에 담겨있는 테스크를 꺼내와서 실행하게 되는것이다.

즉, 콜스택이 지금 바쁘다면 타이머 함수는 제 시간에 실행되지 않을 수 있다.

 

이를 하나의 함수가 실행이 완료되기 전에는 다른 함수가 실행될 수 없는 "Run to Completion" 라 부른다.

 

아래 코드 또한 좋은 예시이다.

let i = 0;

setTimeout(() => alert(i), 100); // ?

// 아래 반복문을 다 도는 데 100ms 이상의 시간이 걸린다고 가정합시다.
for(let j = 0; j < 100000000; j++) {
  i++;
}

실제 실행해보면 100000000이 나오게 된다.

 

(그나마?) 정확한 타이머를 구현하는 방법은?

 

(이 방법말고 다른 좋은 방법도 추천 받습니다!)

스택오버플로우 형님들의 경우는 아래의 코드와 비슷한 방식을 이용한다.

 

https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript 

const interval = INTERVAL; // 1000ms
let time = Date.now() + interval;
let timerId = -1;

const exactlyTimeInterval = () => {
      const dt = Date.now() - time;
      time += interval;
		
      if (dt > interval) {
        // something really bad happened. Maybe the browser (tab) was inactive?
        // possibly special handling to avoid futile "catch up" run
        // 브라우저 말고 다른곳을 보고 있다면 브라우저의 타이머가 멈춰있고, 제대로 작동하지 않는다.
        // 그때 이 if문 안에 들어오게 된다
      }
      ...(생략)
      timerId = setTimeout(exactlyTimeInterval, Math.max(0, interval - dt));
    };
    
timerId = setTimeout(exactlyTimeInterval, interval);

 

setTimeout를 재귀적으로 실행하면 다음과 같이 function이 끝난 후 다음 계획을 실행한다.

setTimeout 예시

그래서 Date 객체의 now 값을 이용해서 실시간으로 다음 계획까지 걸리는 타이머를 보정받는다.

콜스택에 의해 지금 function의 실행이 늦었다면 다음 실행의 경우 늦은만큼 보정한다.

 

하지만 브라우저가 inactive(다른 화면을 보고있다던지)하게 되면 타이머는 정지되게 되고, 다시 브라우저가 활성화된다면 그때 황급히 카운트를 쭉 세게 된다.

 

100% 완벽한가? 까지는 아니지만 괜찮은 방법인거 같다.

 

함께 읽어보면 좋은 글)

 

운영체제의 프로세스 - 쓰레드 구조가 실제로 크롬 브라우저에서 어떻게 쓰이는지 

https://developers.google.com/web/updates/2018/09/inside-browser-part1?hl=ko 

 

모던 웹 브라우저 들여다보기 (파트 1)  |  Web  |  Google Developers

브라우저에서 사용자 코드를 하이레벨 아키텍처부터 렌더링 파이프라인 세부 기능에 이르는 기능성 웹사이트로 전환하는 방법

developers.google.com

 

reference 

 

https://meetup.toast.com/posts/89

 

자바스크립트와 이벤트 루프 : NHN Cloud Meetup

자바스크립트와 이벤트 루프

meetup.toast.com

 

https://velog.io/@yejineee/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84%EC%99%80-%ED%83%9C%EC%8A%A4%ED%81%AC-%ED%81%90-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%ED%83%9C%EC%8A%A4%ED%81%AC-%EB%A7%A4%ED%81%AC%EB%A1%9C-%ED%83%9C%EC%8A%A4%ED%81%AC-g6f0joxx

 

이벤트 루프와 태스크 큐 (마이크로 태스크, 매크로 태스크)

자바스크립트는 싱글 스레드 기반의 언어이고, 자바스크립트 엔진은 하나의 호출 스택만을 사용한다. 이는 요청이 동기적으로 처리되어, 한 번에 한 가지 일만 처리할 수 있음을 의미한다. 만약

velog.io

 

https://ko.javascript.info/settimeout-setinterval

 

setTimeout과 setInterval을 이용한 호출 스케줄링

 

ko.javascript.info

https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript

 

How to create an accurate timer in javascript?

I need to create a simple but accurate timer. This is my code: var seconds = 0; setInterval(function() { timer.innerHTML = seconds++; }, 1000); After exactly 3600 seconds, it prints about 3500 s...

stackoverflow.com

 

반응형

'Front-end > JavaScript' 카테고리의 다른 글

pnpm 도입기 (feat: 모노 레포)  (0) 2023.07.15
자바스크립트의 async/await에 대해서  (0) 2022.03.31
JS로 Single Page Application 구현  (0) 2022.03.12
Generator  (0) 2022.02.02
호이스팅과 var, let에 대하여  (0) 2022.01.29

+ Recent posts