본문 바로가기
Language/Javascript

이벤트 루프 (setTimeout의 시간은 정확할까?)

by joooing 2021. 6. 7.
반응형

이벤트 기반 프로그래밍

브라우저는 어떤 특정 이벤트가 발생하면 이를 감지해 미리 정해둔 작업을 수행한다. 예를들어 회원가입을 하는 상황을 생각해보자. 키보드로 아이디를 입력하면 화면에 입력한 글자가 쓰여지는 것, 제출 버튼을 클릭하면 회원가입이 완료되었다는 메시지가 뜨는 것 모두 어떤 이벤트(키보드 입력, 마우스 클릭)가 발생했을 때, 정해둔 작업(글자 보여주기, 메시지 띄우기)을 실행하는 것이다. 이런 방식을 이벤트 기반(event-driven) 이라고 한다.

 

이벤트 기반의 시스템에서는 어떤 이벤트가 발생할 때, 어떤 작업을 실행할지 미리 등록해 두어야 한다. 이걸 조금 어려운(?)말로 표현하면 '이벤트 리스너(event listener)에 콜백(callback)을 등록한다'고 한다. 이벤트 리스너에 등록된 콜백함수는 이벤트 핸들러(event handler)라고도 한다.

 

Javascript는 이벤트를 기반으로 동작한다. 이벤트가 발생하면 이벤트 리스너에 등록한 콜백함수를 호출하는 것이다. 한 이벤트가 처리되면, 그다음 이벤트가 발생할 때까지 대기한다. 사용자는 이런 이벤트, 그리고 그에 대응하는 어떤 함수(작업)를 통해 웹사이트와 상호작용을 할 수 있게 된다. 이렇게 프로그램을 이벤트 중심으로 제어하는 프로그래밍 방식이벤트 기반 프로그래밍이라고 한다.

출처 : http://onepixelahead.com/2010/08/11/leveraging-event-driven-architectures/

 

이벤트 루프

여러개의 이벤트가 한번에 발생한다면 과연 어떤 순서로 콜백함수들이 실행될까? 바로 이벤트 루프가 이 순서를 정해주는 역할을 한다.

 

1. 콜스택

기본적으로 Javascript 코드는 위에서 아래로 한 줄씩 실행된다. 그러다가 함수가 호출된걸 발견하면, 해당 함수를 콜스택(call stack)이라는 보관함에 넣어둔다. (혹시 스택, 큐 구조에 대해 모른다면 이 글을 먼저 보고오자) 이제 아래 코드의 결과를 한번 예측해보자. 어떤 실행결과가 나오게될까?

function 첫번째() {
  second();
  console.log('1');
}

function 두번째() {
  console.log('2');
}

첫번째();

콘솔에는 2, 1 순서로 찍히게 될 것이다. 분명 첫번째 함수가 먼저 호출되고, 두번째 함수가 그 다음 호출되는데 출력순서는 2가 먼저이다. 콜스택의 모습을 보면 왜 이런 결과가 나오는 지 이해할 수 있을 것이다.

여기서 뜬금없이 등장한 전역 컨텍스트는 어떤 함수가 호출되었을 때 생성되는 환경을 의미한다. 이번 글에서는 그렇게 중요하지 않으니 간단히 설명하면, 함수를 실행시키는데 필요한 정보들을 모아둔 곳 정도로 이해하면 될 것 같다. 변수, 함수나 scope, this..등의 정보가 이곳에 들어있다. 콜스택 밑바닥에는 항상 이 전역 컨텍스트가 존재한다. (더 궁금하다면 이 글을 참고)

 

다시 본론으로 돌아가서, 어떤 함수가 실행되면, 이 함수는 콜스택에 보관되어 있다가 실행이 완료되면 스택에서 나오게 된다. 스택은 나중에 들어온걸 먼저 내보내는 구조이기 때문에, 첫번째 함수가 먼저 들어왔지만 실행은 두번째 함수가 먼저 되어 콜스택을 빠져나가는 것이다.

 

2. Javascript 엔진

방금까지 설명했던 콜스택은 Javascript 엔진에 포함된다. Javascript엔진은 대충 이렇게 생겼다.

이 엔진은 우리가 Javascript 로 작성한 코드를 컴퓨터가 알아들을 수 있게 해석하고, 실행해주는 매우 중요한! 역할을 한다. 크게 힙(Memory Heap)과 콜스택(Call Stack)으로 구성된다. 우선, 변수나 객체들이 저장되는 창고역할을 한다는 정도로만 알고 넘어가자. 콜스택은 앞서 말했듯이 함수들이 실행순으로 쌓이는 곳이고, 함수들은 맨위부터 하나씩 실행되면서 이 콜스택에서 제거된다. (사실 엄밀히 말하면 함수 자체는 아니고, 함수의 실행 컨텍스트이다..!)

 

3. 이벤트 루프

그럼 이번에는 아래 코드를 실행하면 어떤 결과가 나올까?

 

function second() {
  console.log('2');
}

console.log('1');
setTimeout(second, 5000);
console.log('3');

setTimeout 함수에 대해 알고 있다면 1, 3, 2 순서로 찍힐거라고 쉽게 예상할 수 있다. setTimeout 은 지정한 밀리초(ms) 이후에 코드를 실행하는 메서드이다. (더 알아보기..) 답을 맞췄다면 이것도 한번 생각해보자. setTimeout의 첫 인자로 들어간 second 함수는 언제 콜스택에 들어갈까? 사실 이 답은 콜스택 하나로만 설명할 수가 없다. 지금까지 이야기 한 것들은 Javascript 엔진에 대한 것들지만 이 문제는 시야를 좀 더 넓혀 브라우저 전체를 봐야한다. 이와 관련된 개념인 태스크 큐, Web API에 대해서도 알아보자.

 

브라우저 전체 모습이다. 왼쪽에는 아까 봤던 Javascipt 엔진도 보인다. 뭔가 복잡해보이지만 하나씩 천천히 보면 사실 별거없다..!

우선 태스크 큐(task queue)는 이벤트핸들러, setTimeout 같은 비동기 함수의 콜백함수 등이 잠시 보관되는 대기소같은 역할을 한다. 실행되기 전까지 여기에 보관된 함수들은 순서대로 줄서서 기다리다가, 순서대로 빠져나가게 된다.

 

브라우저는 Javascript 엔진 밖에서, WebAPI라는 것도 제공한다. 정말 단순화시켜 말하면 Javascript 가 아닌 언어로 작성된 프로그램이다. 아까 봤던 setTimeout같은 타이머 API 뿐만 아니라 DOM 메서드, HTTP 요청같은 것들은 전부 이 WebAPI에서 제공한다. 그리고 이 메서드들은 전부 비동기 메서드이기 때문에 콜백함수를 가지고 있다. 이 메서드들의 실행이 끝나면, 가지고 있던 콜백함수를 태스크 큐로 보낸다.

 

이벤트 루프(event loop)는 실행할 콜백함수들을 관리한다. 먼저 콜스택에 실행 중인 함수가 있는지 확인하고, 모든 함수들이 실행되었다면 태스크 큐로 눈을 돌려 대기중이던 함수들을 콜스택으로 올려보낸다. 내 방식대로 간단히 정리해보면 아래와 같다.

 

콜스택 = 함수가 실행되는곳
태스크 큐 = 함수들이 콜스택으로 가기 전에 대기하는 곳
이벤트 루프 = 콜스택이 비었는지 지켜보다가, 비면 태스크 큐에 대기중이던 함수들을 콜스택으로 보냄

 

이제 아까의 코드를 다시 가져와 이 중 setTimeout이 실행되는 과정을 보면서, webAPI는 어떻게 동작하는지에 대해서도 알아보려고 한다. 그림과 함께 살펴보자!

 

function second() {
  console.log('2');
}

console.log('1');
setTimeout(second, 5000);
console.log('3');

1. setTimeout이 스택에 쌓인다.
2. setTimeout이 실행되고, 콜백함수(second)를 WebAPI로 보낸다.
3. 5초 후, 콜백함수를 태스크큐로 보낸다.
4. 이벤트루프가 스택이 비었는지 지켜보다가, 스택이 비면
5. 태스크 큐의 콜백함수를 콜스택으로 옮긴다.
6. 옮겨진 콜백함수가 실행된다.

 

setTimeout은 위와 같은 순서로 실행이 된다. 이 중 4번과정을 잘 생각해봐야하는데 '스택이 비면'이라는 말은, 5초가 지나 WebAPI가 태스크 큐에 콜백을 넣었다고 해도, 콜스택이 비지 않았다면 계속 태스크 큐에서 대기하며 기다려야 한다는 뜻이다. 콜스택에 아직 실행되지 않은 함수들이 한가득 들어있다면, 콜백함수는 몇초가 되든 일단 대기상태로 머무를 수 있다. 따라서 setTimeout에 입력한 시간은 정확하게 지켜지지 않을 수도 있다.

 

조금만 더 응용해보자. 위의 상황에 에러를 잡기 위해 try catch문을 추가한 경우이다. 이 경우, try catch문은 second 함수에서 발생한 에러를 잡지 못한다. 방금까지 설명했던 것들을 적용시켜 생각해보면 try 의 setTimeout과 catch 부분은 바로 실행되어 콜스택에서 제거되지만, setTimeout의 콜백함수인 second는 WebAPI를 들리고, 태스크큐를 거쳐 콜스택에 쌓였던 모든 함수들이 실행된 후에야 콜스택에 들어가게 되고 실행되기 때문이다. 이미 에러를 잡을 수 있는 catch부분은 실행을 마치고 제거되었기 때문에 뒤늦게 실행된 second의 에러는 처리할 수 없는 것이다.

 

try {
	setTimeout(second, 5000);
} catch (e) {
	console.log(e.message);
}

 

마무리

여기까지 이벤트 기반 프로그래밍이 무엇이고, 이런 프로그래밍을 가능하게 해주는 이벤트 루프에 대해서도 알아보았다. 더 넓게는 함수가 실행되는 콜스택, 실행 전에 대기하는 곳인 태스크큐 등을 보며 브라우저 전반에 걸쳐 함수가 실행되는 과정도 살펴봤다. 더 나아가서는 WebAPI 가 제공하는 setTimeout같은 비동기 메서드의 콜백함수가 언제 실행되는지, setTimeout에 입력한 시간이 정확하지 않을 수도 있다는 것까지도 알게되었다. 이 글에서는 다루지 않았지만 사실 태스크 큐는 하나가 아니라 여러개로 이루어지는데, 이후 글에서는 이런 부분에 대해서도 좀 더 다루어보려고 한다.

반응형

댓글