Blocking
자바스크립트는 동기적으로 실행되는 싱글 스레드 프로그래밍 언어이다. 한 타임에 한 문장을 순차적으로 실행한다. 그러나 API 호출처럼 요청받는 데이터 크기에 의존한 시간, 네트워크 연결 속도 등등과 같이 많은 이유를 통해서 불확실한 실행 시간을 가진다. 만약 API 호출이 동기적으로 실해 된다면 브라우저는 API동작이 완료되기 전까지 스크롤링이나 버튼 클릭과 같은 유저의 동작을 받아들일 수 없게 된다. 이것이 Blocking(블록킹) 이다.
블록킹 이슈를 해결하기 위해서 브라우저 환경은 자바스크립트가 비동기적으로 접근 가능한 많은 Web API를 갖고 있다. 비동기적이란 병렬적으로 다른 연산들을 실행하는 것을 말하며, 순차적으로 실행하는 동기적과 반대되는 행위이다. 비동기적인 실행은 사용자가 계속해서 브라우저를 일반적으로 사용해줄수 있게 하기 때문에 유용하다.
Javascript 개발자라면 WebAPI와 응답 혹은 에러 처리의 비동기적 동작의 방법을 알고있을 필요가 있다.
Event Loop
WebAPI를 사용하지 않는 자바스크립트 코드는 한 타임에 한줄씩 동기적으로 실행된다.
다음과 같이 WebAPI를 사용하지 않는 함수 세개를 동시에 실행시키는 코드는 순차적으로 실행된다.
// Define three example functions
function first() {
console.log(1)
}
function second() {
console.log(2)
}
function third() {
console.log(3)
}
// Execute the functions
first()
second()
third()
Output
1
2
3
비동기 WebAPI가 사용되면 좀 더 복잡해진다. 대표적인 비동기 함수인 setTimeout
을 통해서 테스트해볼 수 있다. setTimeout
은 타이머를 설정하여 주어진 시간동안 대기한 이후에 실행된다. 대기시간에도 브라우저는 유저의 동작을 처리해야 하므로 비동기적으로 실행되어야 한다.
비동기요청을 테스트하기 위해서 위의 예제 코드의 second
함수에 setTimeout
를 추가하여 실행한다.
// Define three example functions, but one of them contains asynchronous code
function first() {
console.log(1)
}
function second() {
setTimeout(() => {
console.log(2)
}, 0)
}
function third() {
console.log(3)
}
setTimeout
은 두가지 인자를 가진다. 첫번째 인자는 대기시간 이후에 비동기적으로 실행될 함수이고 두번째 인자는 대기 시간이다. 위 코드를 실행해보면 0초를 기입했기 때문에 대기 없이 실행되어 순차적으로 실행될것 처럼 보인다. 그러나 setTimeout
함수 자체가 비동기로 실행되기 때문에 실행 순서를 보장할 수 없다.
Output
1
3
2
대기 시간에 0초를 쓰든, 5분을 쓰든 차이가 없다. setTimeout
안의 console.log
는 비동기적으로 호출되고, 상위의 동기적인 함수들이 실행되고 난 후 호출된다. 이러한 동작은 자바스크립트의 호스트 환경때문에 일어난다. 브라우저는concurrency(병행성)와 parallel events(벙렬성)을 다루기 위한 이벤트 루프를 사용한다. 자바스크립트는 동기적으로 실행되기때문에 정확히 어떤 문장이 실행될것인지를 알고있는 이벤트 루프가 필요하다. 이벤트 루프는 stack
과 queue
를 이용하여 이를 다룬다.
Stack
스택 혹은 콜스택은 현재 실행중인 함수의 상태를 기록한다. 스택은 나중에 들어온것이 먼저 나오는 LIFO(Last In, frist out) 구조이다. 이는 아이템을 오직 스택의 끝에만 삽입/삭제가 가능하다. 자바스크립트는 스택의 현재 프레임(혹은 함수)을 실행시키고 스택에서 삭제한다.
동기적 코드를 포함하는 예제에서, 브라우저는 다음과 같은 순서로 실행된다.
first()
를 스택에 넣고first()
를 실행하여1
을 출력하고 스택에서first()
를 삭제한다.second()
를 스택에 넣고second()
를 실행하여2
을 출력하고 스택에서second()
를 삭제한다.thrid()
를 스택에 넣고thrid()
를 실행하여3
을 출력하고 스택에서thrid()
를 삭제한다.
setTime
을 포함하고 있는 예제에서는 다음과 같이 실행된다.
first()
를 스택에 넣고first()
를 실행하여1
을 출력하고 스택에서first()
를 삭제한다.second()
를 스택에 넣고second()
를 실행한다setTimeout()
을 스택에 넣고 WebAPI인setTimeout
을 실행시킨다. 이는 타이머를 실행시키고, 타이머가 종료되면 실행할 callback함수를 queue에 삽입하고 스택으로부터setTimeout()
을 삭제한다.
second()
를 스택으로부터 삭제한다.thrid()
를 스택에 넣고thrid()
를 실행하여3
을 출력하고 스택에서thrid()
를 삭제한다.- 이벤트루프가 pending된 메세지를 큐에서 확인하고,
setTImeout()
로부터 비동기로 실행될 함수를 찾는다. 찾은 함수(2
를 출력하는)는 스택에 넣고 이를 실행시킨 후에 스택에서 제거한다.
비동기 WebAPI인 setTimeout
는 queue를 이용한다.
Queue
Queue(큐)는 message queue 혹은 task queue라고도 불린다. 콜 스택이 empty가 될 때 마다 이벤트루프는 큐에 대기중인 메세지를 확인하고 오래된 메세지부터 실행한다. 큐에서 실행시킬것을 발견하면 이를 스택에 넣고 함수를 실행시킨다.
setTimeout
예제에서 비동기 함수는 0
초를 셋팅했기 때문에 다른 상위 레벨의 실행이 모두 이루어지고 난후에 즉시 실행된다. 중요한점은 타이머는 코드가 정확히 0
초 이후에 실행되는것을 의미하지 않는것이다. 타이머는 비동기 함수를 셋팅한 시간 만큼 큐에 넣어둔다. 큐가 존재 하는 이유는 타이머가 종료될 때 실행될 함수를 스택에 바로 추가할 경우, 현재 실행중인 함수가 중단될 수 있기 때문에 존재한다.
job queue 혹은 microtask queue 라고 불리는 promise를 다루는 또 다른 큐도 존재한다. promise와 같은 microtask는 setTimeout과 같은 macrotask보다 높은 우선순위를 가진다.
이 글에서는 이벤트루프가 스택과 큐를 사용하여 코드의 순서를 다루는 방법을 알아보았다. 다음 글에서는 코드 내에서 실행 순서를 컨트롤 하는방법을 알아보겠다.
공부하기 위해 원문을 개인적으로 번역한 글입니다.