*잘못된 부분이 있으면 피드백 주시면 감사하겠습니다!
자바스크립트에서 타이머 이벤트를 쓸 수 있는 방법으로는 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라는 세부적인 개념도 있는데 생략한다)
참고)
싱글 쓰레드와 이벤트 루프
그리고, 자바스크립트가 단일 쓰레드 기반이라는 말이 정확히는 '자바스크립트 엔진'은 단일 호출 스택을 사용한다 라는 것이다.
위 그림같은 경우는 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이 끝난 후 다음 계획을 실행한다.
그래서 Date 객체의 now 값을 이용해서 실시간으로 다음 계획까지 걸리는 타이머를 보정받는다.
콜스택에 의해 지금 function의 실행이 늦었다면 다음 실행의 경우 늦은만큼 보정한다.
하지만 브라우저가 inactive(다른 화면을 보고있다던지)하게 되면 타이머는 정지되게 되고, 다시 브라우저가 활성화된다면 그때 황급히 카운트를 쭉 세게 된다.
100% 완벽한가? 까지는 아니지만 괜찮은 방법인거 같다.
함께 읽어보면 좋은 글)
운영체제의 프로세스 - 쓰레드 구조가 실제로 크롬 브라우저에서 어떻게 쓰이는지
https://developers.google.com/web/updates/2018/09/inside-browser-part1?hl=ko
reference
https://meetup.toast.com/posts/89
https://ko.javascript.info/settimeout-setinterval
https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript
'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 |