대학생 때 이자카야에서 서빙 아르바이트를 한 적이 있었습니다. 식당에는 손님이 많지 않아 혼자 일 했습니다. 손님들이 오시면 테이블에 안내해드리고 주문을 받습니다. 그동안 또 다른 손님들이 옵니다. 몸이 두 개가 아니므로 첫번째 테이블을 응대하고 있는 동안 '두번째 테이블에 가야한다'는 작업을 머릿 속에 생각해둡니다.
source: Java Brains youtube
"event loop"
이와 비슷하게 자바스크립트는 싱글 스레드(single thread)로 동작합니다. 홀로 서빙하는 종업원처럼 많은 작업을 하나의 스레드로 처리한다는 뜻입니다. 하지만 제가 동시에 여러 일을 못하듯이 자바스크립트도 동시에 여러 작업을 할 수 없습니다. 그래서 만약 현재 제가 어떤 일을 진행 중이라면, 당장 다른 일은 못하지만 머릿 속에서 다음 할 일을 생각하는 것처럼 자바스크립트도 task queue에 넣고 현재 하고 있는 일이 끝나면 queue에서 하나씩 실행합니다.
더 자세히 설명해볼게요. 아래 그림은 자바스크립트 엔진의 모델을 시각화한 것입니다.
•
스택(Stack): 지금 해야할 작업들을 쌓아놓는 공간입니다. 함수가 호출될 때 스택에 쌓입니다. 호출될 때 쌓여서 call stack이라고 합니다.
•
힙(Heap): 구조화되지 않은 넓은 메모리 공간입니다. 객체 데이터가 저장되는 공간입니다.
•
큐(Queue/Task Queue): 스택에서 지금 당장 소화하지 못하는 작업들은 큐에 쌓아놓습니다.
•
이벤트 루프(Event Loop): 스택과 큐를 지켜보고 있다가 스택이 비워지면 큐에서 대기 중인 작업들을 스택에 쌓습니다.
source: mdn
함수가 호출되면 스택에 프레임을 생성합니다. bar(7)을 호출하면 bar의 인자와 지역 변수를 포함하는 *스택프레임을 스택에 생성합니다. 그리고 bar 함수 안에 있는 foo 함수가 호출됩니다. foo 함수의 스택프레임이 bar 스택프레임 위에 생성됩니다. 스택은 나중에 들어온 작업이 먼저 실행됩니다. 따라서 foo가 먼저 실행되고, 함수가 종료되면 사라집니다. 그리고 그 다음 bar가 실행되고 프레임이 사라집니다.
*스택프레임: 함수 호출 시 필요한 지역 변수, 매개 변수를 저장하고 함수가 종료되면 사라집니다. 스택에서 이 함수를 위한 공간을 만들어서 '프레임'이라고 합니다.
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7)); //returns 42
JavaScript
복사
그런데 맨 위에 있는 foo 함수가 좀 오래 걸린다면 bar 함수의 실행은 지연(blocking)될 수 밖에 없겠죠? 사용자가 만약 이 상황에서 다른 작업을 요청했다면 스택이 아직 비어있지 않았기 때문에 작업은 대기열 큐에 쌓이게 됩니다. 그리고 마침내 스택이 일을 다 처리하면 노는 꼴을 못보는 이벤트 루프가 큐에 쌓여있는 작업을 다시 스택에 쌓습니다.
은행에서 나보다 나중에 온 고객을 더 먼저 상담해준다면 기분이 안좋겠죠? 마찬가지로 큐에서 작업을 꺼낼 때는 가장 오래된 작업부터 처리합니다. 스택에 작업이 쌓이면 비워질 때까지 일을 처리합니다. 이벤트 루프는 큐를 계속 지켜보면서 작업이 쌓이면 스택이 비워질 때 큐에서 꺼내 스택에 넣고, 큐에 작업이 없으면 생길 때까지 기다립니다.
브라우저 상에서 생각해보겠습니다. 사용자가 어떤 버튼을 눌렀는데 실행이 오래 걸려서 이것저것 눌렀습니다. 겨우 처음 요청이 끝나고 나니 그 동안 밀렸던 응답이 한꺼번에 실행되는 모습을 보게 됩니다. UX 상으로는 좋지 않겠죠? 그래서 blocking 없이 코드를 실행할 수 있는 방법을 생각하게 됩니다.
"async callback"
async callback은 비동기적으로 실행되는 콜백 함수입니다. 비동기적으로 실행된다는 게 무슨 의미일까요?
source: Java Brains youtube
다시 이자카야로 돌아가봅시다. 제가 첫번째 테이블 손님에게 주문을 받아 주방에 주문을 넣었습니다. 동시에 두번째 테이블에서 저를 호출합니다. 종업원에게 동기적으로 서빙하라고 하면 첫번째 테이블 요리가 나올 때까지 두번째 테이블로 가지 않습니다. 그럼 두번째 테이블에서 난리가 나겠죠. 그래서 우리는 종업원에게 음식이 나올 때까지 기다리지 말고 그동안 주문을 받으라고 명령할 것입니다.
코드로 표현해보겠습니다. setTimeout은 스택에 쌓였다가 바로 사라집니다. 그동안 getOrder 함수가 실행되며 종업원이 2번째, 3번째, 4번째 테이블에서 주문을 받습니다. 3분 뒤 setTimeout의 콜백함수인 serveFood 함수가 다시 스택에 쌓여 실행됩니다.
function getOrder(tableNumber) {
// yada yada yada
return order
}
function serveFood(order) {
// 요리킹 조리킹()
return food
}
const order = getOrder(1)
setTimeout(() => serveFood(order), 3 * 60 * 1000) // 3분 뒤 실행
getOrder(2)
getOrder(3)
getOrder(4)
JavaScript
복사
setTimeout에 있던 콜백은 어디있다가 갑자기 나타난 것일까요? 사실 setTimeout은 v8 엔진에서 지원하는 API가 아닙니다. setTimeout은 브라우저의 web API 입니다. 따라서 setTimeout이 콜 스택에 쌓이면 바로 사라지고 web API에서 3분 동안 대기합니다. 3분이 지나면 큐에 작업을 넣습니다. web API에서는 스택이 현재 어떤 일을 하고 있는지 모르기 때문에 바로 스택에 넣을 수는 없습니다. 그럼 이벤트 루프는 스택이 비워질 때까지 기다리고 있다가 큐에 있는 serveFood를 스택에 넣고 함수가 실행됩니다.
이렇게 setTimeout으로 비동기 콜백 함수를 만들 수 있습니다. 그런데 여기서 생각해봐야할 점이 있습니다.
serveFood는 정확히 3분 뒤에 바로 실행될까요?
"Zero Delays"
만약 getOrder 함수가 굉장히 느려서 3분 뒤에도 끝나지 않았다면 어떻게 될까요? 이미 스택에서 getOrder 함수를 처리하고 있기 때문에 이벤트루프는 serveFood를 스택에 쌓을 수 없습니다. 그래서 3분이 지나고도 나머지 getOrder 함수 실행이 끝날 때가지 serveFood는 실행될 수 없습니다. 즉, setTimeout에서의 대기 시간은 '최소 시간'입니다. 최소 3분은 기다려야 하고, 그 이상 걸릴 수도 있다는 의미입니다.
그럼 아래 코드는 어떨까요? 아시다시피 0초 뒤에 바로 실행되는 게 아닙니다. 스택에 작업이 쌓여있다면 작업이 끝날 때까지 기다려야 합니다. 그리고 스택이 비워지면 이벤트 루프가 큐에서 작업을 가져와 스택에 쌓고 그제서야 실행이 됩니다.
setTimeout(callback, 0)
JavaScript
복사
"browser repaint"
브라우저의 렌더링 작업은 스택에 바로 쌓이지 않고 render queue에 따로 쌓이게 됩니다. 이때 렌더 큐에 쌓이는 작업들은 task queue의 callback보다 더 우선시 됩니다. 하지만 render queue 역시 스택이 비워질 때까지 기다려야 합니다. 그래서 스택에 느린 코드가 실행되고 있다면 렌더링이 지연되는 상황이 발생합니다.
이런 상황에서 async callback을 사용하면 콜백은 task queue에 쌓이면서 렌더링 작업을 상대적으로 덜 blocking합니다. 앞서 말했듯이 렌더링이 task queue 콜백보다 더 우선시 되기 때문에 중간중간에 끼어들 수 있습니다. 만약 우리가 이벤트가 많이 발생하는 scroll event 핸들러의 콜백 함수를 실행한다고 하면 비동기로 실행하여 콜백 함수가 렌더링을 지연시키는 일을 방지할 수 있습니다.
references
•
youtu.be/8aGhZQkoFbQ what the heck is event loop anyway?
•
developer.mozilla.org/ko/docs/Web/JavaScript/EventLoop 동시성 모델과 이벤트 루프
•
www.youtube.com/watch?v=EI7sN1dDwcY&feature=youtu.be what is the javascript event loop
•