본문 바로가기

Language/Javascript

[Javascript] 싱글스레드 기반 이벤트 루프와 비동기

동기/비동기란?

1) 동기

  • 동기란 Request를 보내는 시기와 Response를 받는 시기가 일치.
  • 요청을 하게 되면 응답이 올 때까지 프로그램은 정지하고 응답이 오면 다시 진행됨.
const longTimeTask = () => {
  // 시간이 오래 걸리는 작업
  console.log('시간이 오래 걸리는 작업');
  for(let i = 0 ; i < 100000000 ; i++){
  	console.log(".");
  }
};

console.log('시작');

longTimeTask();

console.log('종료');

 

2) 비동기

  • Request와 Response가 동시에 일어나지 않음.
  • 비동기로 수행을 할 경우, Request를 보낸 후, Response를 기다리지 않고 다음 작업을 바로 진행.
const longTimeTask = () => {
  // 시간이 오래 걸리는 작업
  console.log('시간이 오래 걸리는 작업');
  for(let i = 0; i < 10000000; i++){
  	console.log('.');
  }
};

console.log('시작');

setTimeout(longTimeTask, 0);

console.log('종료');

*비동기는 종료코드가 먼저 수행된 다음에 시간이 오래 걸리는 작업이 수행됨.

*setTimeout은 대표적인 비동기 코드. 0초 후 실행이더라도 바로 출력되지 않음. (이후 이벤트 루프에 대한 설명 참고)

 

 

블로킹/논블로킹

동기 비동기와 블로킹 논블로킹의 의미적 차이.

  • 동기와 비동기 : 함수가 바로 return되는지의 여부
  • 블로킹과 논블로킹 : 백그라운드 작업 완료 여부

블로킹-동기

  • 위 예시에서는 동기 코드는 함수 작업을 기다리기 위해 코드가 멈춤 -> 블로킹
  • 그리고 해당 함수의 리턴 값을 (리턴값이 있다고 가정) 기다렸다가 다음 코드를 실행 -> 동기

논블로킹-비동기

  • 함수 작업을 넘기고 멈추지 않고 바로 다음 코드를 실행. -> 논블로킹
  • 그리고 0초 후에 콜백 함수로 longTime함수를 넣어주었음 -> 비동기

 

 

이벤트 루프란?

자바스크립트는 싱글스레드이지만 비동기처리를 이용해 가볍고 효율적이다. 여기서 싱글스레드는 이벤트 루프가 싱글 스레드로 동작하기 때문에 그렇게 불리는 것이다.

단, 실제 구동환경의 경우(브라우저,Node.js등)에서는 주로 여러 개의 스레드를 사용한다.

  • ECMAScript 스펙에는 이벤트 루프에 대한 내용이 없다.(es6부터는 조금 달라짐.)
  • V8과 같은 JS 엔진은 단일 호출 스택(Call Stack)을 사용하며, 요청이 들어올 때마다 순차적으로 처리.
  • 비동기 처리 함수에 대한 내용은 실제로 JS엔진 외부 API에 존재
    *Web의 경우 setTimeout, XMLHttpRequest은 Web APIs에 속함
    *Node.js의 경우에는 Node.js의 API 호출을 통해 비동기 처리. 이벤트 루프를 통해 스케쥴되고 실행됨.

 

[브라우저 환경]

 

[node.js 환경]

 

Javascript의 Run-to-Complete 특징

Run to Complete이란,

  • 자바스크립트 함수가 실행되는 방식을 말함. 
  • 하나의 함수가 실행되면, 이 함수의 실행이 끝날 때까지는 다른 어떤 작업도 중간에 끼어들지 못한다는 의미.
  • 즉 현재의 스택에 쌓여있는 모든 함수들이 실행을 마치고 스택에서 제거되기 전까지는 다른 어떤 함수도 실행 될 수 없음.
const longTimeTask = () => {
  // 시간이 오래 걸리는 작업
  console.log('시간이 오래 걸리는 작업');
  for(let i = 0; i < 10000000; i++){
  	console.log('.');
  }
};
const asyncTask = () => {
	//비동기 실행 함수.
	console.log("asyncTask");
}
console.log('시작');

setTimeout(asyncTask,0);
longTimeTask();

console.log('종료');
//0초 후에 바로 실행되는 것이 아닌,
//call stack에 쌓인 longTimeTask의 모든 stack이 비워진 후, 
//asyncTask가 실행된다.

*setTimeout은 실제 설정된 timeout보다 늦게 실행됨.

 

 

태스크 큐와 이벤트 루프.

setTimeout에서 실행되는 함수는 어디서 대기하고 있다가 누구를 통해 실행될까?

  • 태스크 큐는 콜백함수들이 대기하고 있는 큐(FIFO)형태의 배열
  • 이벤트 루프는 CallStack이 비워질 때마다 테스크 큐에서 콜백 함수를 꺼내와서 실행하는 역할.
  • setTimeout에서 정한 시간이 지나면 그 함수를 바로 실행한다기 보다는 테스크 큐에 추가한다.
  • 이벤트 루프는 현재 실행중인 테스크가 종료되면 테스크큐에 대기중인 첫 번째 태스크를 실행한다.
    즉 모든 비동기 API들은 작업이 완료되면 콜백 함수를 태스크 큐에 추가.
    이벤트 루프는 ‘현재 실행중인 태스크가 없을 때’ (주로 호출 스택이 비워졌을 때) 태스크 큐의 첫 번째 태스크를 꺼내와 실행.

[참고 http://www.2ality.com/2014/09/es6-promises-foundations.html]

 

 

setTimeout(fn,0) 을 사용하는 이유

프론트엔드의 JS 코드를 보다보면 setTimeout(fn,0)과 같은 코드를 종종 목격하게 된다.

이벤트 루프에 대한 이해가 없이 이 코드를 보게되면, 별로 의미가 없는 코드 처럼 느껴진다. 하지만 실제 코드는 그냥 fn을 실행되는 것과 다른 프로세스를 따르게 됨.

  • setTimeout함수는 콜백 함수를 바로 실행하지 않고 호출 스택이 아닌 테스크 큐에 추가
setTimeout(function() {
    console.log('A');
}, 0);
console.log('B');
//B A 출력
  • FE에서는 렌더링 엔진과 관련해서 요긴하게 쓰임.
  • 렌더링 엔진은 JS엔진과 함께 단일 태스크 큐를 통해서 관리됨.
$('.btn').click(function() {
    renderWait();
    mainFunc();
    hideWait();
    showResult();
});
  • 일반적으로 renderWait를 통해서 대기화면을 뿌린후, mainFunc가 실행될 것이라고 예상하겠지만, 실제로는 renderWait부터 showResult까지 실행된 후, 렌더링 task가 실행되게 된다.
  • renderWait를 통해서는 렌더링요청을 task큐에 추가시키게 될 테지만, 이를 위해서는 현재 실행중인 Call stack이 모두 비워져야함. 
$('.btn').click(function() {
    renderWait();
    setTimeout(function() {
        mainFunc();
        hideWait();
        showResult();
    }, 0);
});
  • 따라서 위와 같이 renderWait이후 코드를 다른 태스크로 나누어 주면, 예상한 순서대로 실행이 될 수 있다.
    renderWait -> 렌더링 task 실행 -> setTimeout내 함수 task 실행
  • 참고로 setTimeout에서 0이라는 숫자는 실제 ‘즉시’를 의미하지 않음. 부라우저 내부적으로 타이머 최소 tick을 정하여 관리하는데 이 최소단위만큼 지난 후, task 큐에 추가된다고 보면됨.
    (크롬 브라우저는 최소단위 tick이 4ms 즉, setTimeout(fn,0)은 setTimeout(fn,4)라고 보면됨)

 

Promise와 이벤트 루프

예시를 통해 먼저 Promise의 행동방식을 파악해보자.

setTimeout(function() { // (A)
    console.log('A');
}, 0);
Promise.resolve().then(function() { // (B)
    console.log('B');
}).then(function() { // (C)
    console.log('C');
});
  • 해당 코드는 B->C->A 순서로 실행.
  • Promise는Micro Task를 사용하게 되기 때문.

Micro task란?

  • 일반 태스크보다 더 높은 우선순위의 task.
  • setTimeout함수의 콜백 A는 일반 task 큐에 추가됨.
  • Promise의 then() 메서드 콜백 B와 C는 일반 task가 아닌 마이크로 task큐에 추가된다.
  • 이벤트루프는 micro task 큐가 먼저 비었는지 확인 후, 일반 task 큐를 보게 됨.
  • 따라서 BCA순서로 출력.
  • HTML 스펙에서 perform a microtask checkpoint 항복에 명시

 

[참고]