Sitemap
9 min readAug 16, 2020

우리가 기존에 해오던 프로그래밍은 주로 ‘동기적’ 방식이다. ‘동기적’이라고 함은, 어떤일을 순차적으로 처리하는 것이라고 볼 수 있다. 즉, 어떤 코드상의 흐름을 따라가면서 실행하는 것이다. 그러나 동기적 방식의 문제점은 앞서 처리되지 못한 코드가 있다면 프로그램이 그대로 멈춰버린다. 그 코드가 다 실행이 되기 전까지는, 다음 코드가 실행이 될 수 없다.

자바스크립트의 경우에는 Single Thread 언어이기 때문에, 하나의 스레드에서 작업들이 순차적으로 수행된다. 그런데 동기적으로 프로그램 언어가 실행된다면 프로그램이 멈추는 경우가 굉장히 많을 수 있다.

이 단점은 ‘비동기식’으로 해결할 수 있다. ‘비동기식’의 특징은 기다리지 않는다는 것이다. 함수가 호출된 후, 곧바로 결과값을 먼저 주고, 안의 내용은 나중에 처리되면서 처리되는 순서대로 return된다. 따라서 앞에서 처리되지 못해도 프로그램은 죽지 않는다. 이런 방식으로 Single Thread에서 돌아갈 때의 단점을 극복할 수 있다.

이것들이 자바스크립트 내에서 어떻게 이루어지는지 보자.

1. 자바스크립트 Runtime(실행환경)

비동기 처리가 어떻게 이루어지는 지 알기전에, 자바스크립트의 Runtime을 알면 도움이 된다. 먼저 자바스크립트 Runtime이 어떻게 구성되어 있는지 살펴보자.

- 자바스크립트의 엔진은 memory heap과 call stack을 가지고 있다. Memory heap에는 객체나 변수들이 관리되고, call stack에서는 코드가 실행되고 함수가 쌓인다.

- 뒤에서 또 언급하겠지만, Event loop + Event Queue는 비동기 처리를 하는 부분이다.

- 그리고 자바스크립트는 브라우저와 함께 돌아가기 때문에 web에서 제공하는 API들과 함께 실행된다. (node 환경일 경우에는 node에서 제공하는 API들)

그렇다면, 이 실행 환경에서 자바스크립트는 비동기 처리를 어떻게 할까?

2. 비동기 처리 로직

비동기처리를 알기 위해서는 ‘Callback’함수가 뭔지 먼저 알아야한다. ‘Callback’함수란 나중에 호출할 함수를 의미한다. 정말 간단한 예시를 하나 보자.

setTimeout(function() {
console.log('setTimeout');
}, 3000);

자바스크립트에서 자주 쓰는 setTimeout 함수이다. 우리가 그동안 보던 함수의 형태와는 다르게, 이 함수는 parameter로 익명함수가 넘어가고 있다. 이처럼, callback 함수는 다른 함수의 parameter로 넘어가거나 객체의 property로 사용하게 된다. 그렇다면, setTimeout 함수는 어떤식으로 Runtime에서 처리하게 될까?

먼저, setTimeout 함수는 ‘함수’이므로, Call Stack에 제일 먼저 push된다.

push된 후, setTimeout 함수는 비동기 함수이므로 web API를 호출한다. setTimeout의 실행이 스택에서 끝나면 setTimeout은 Call Stack에서 pop하여비워지고, web API는 요청된 시간동안(위 snippet에서는 3000) pending 상태에 들어간다.

요청된 시간이 지나면, web API는 콜백함수를 이벤트 큐에 밀어넣는다. 이벤트 큐는 대기하다가 Call Stack이 비는 시점에 이벤트 루프를 돌려서 Call Stack에 콜백 함수를 집어넣는다. Call Stack이 비는 시점에 스택에 집어넣는 이유는, 이미 실행되고 있는 함수가 있을 수 있기 때문이다.

그 후에, 실행되면서 Callback 함수 안에 있는 console.log 함수도 Call Stack에 쌓인다. console.log 함수가 실행되면서콘솔에 ‘setTimeout’이라는 String 값이 표시되고, 표시가 끝나면 console.log 함수가 Call Stack에서 pop하여 꺼내지고 Callback함수도 실행이 완료되었으므로 Call Stack에서 pop하여 꺼내진다.

예시에서 보면 알 수 있듯이, 하나의 함수가 호출되면 곧바로 Call Stack에 쌓이고, web API에 호출되면서 바로 Call Stack에서 pop되면서 Callback 함수를 기다리지 않고 실행이 완료된다. 그리고 나중에 Callback 함수를 Event Loop로부터 받아서 실행을 완료한다. 이것이 비동기 처리의 전체적인 로직이다.

3. Event Loop란 무엇인가?

처음에 비동기 처리를 이해할 때, 가장 이해가 안되는 부분이 Event Loop 부분이었다. 이 Event Loop는 어디서 돌아가는 것이며, 언제 시작되는가?에 대해서 궁금증을 가졌었다. 그 궁금증에 대한 답은 다음과 같다.

위 그림에서 보면 알 수 있듯이, Event Loop는 단순히 Javascript Runtime안에 있다. 다만, 우리가 알고있는 Javascript 엔진 밖에 있는 것이다. 밖에서 대기하면서, Event가 발생하면서 Event Queue에 콜백함수가 들어오게 되고, Call Stack이 비게되면 그 시점에 Event Loop가 시작되면서 Call Stack에 함수를 밀어넣는 것이다.

이 시점에서 또 하나의 궁금증이 생겼다. Event Loop는 내부에서 어떻게 돌아가는가? 에 대한 궁금증이었다. 이제부터 그 부분에 대해서 글을 쓰려고 한다.

-1. Event Loop의 구조

우선 Event Loop의 구조는 위와 같다. (굉장한 발그림이다)

각각의 box는 각각 하나의 Queue를 가지고있다. Event Loop가 시작되면, Timer 큐에는 말 그대로 Timer의 Queue에 Timer Callback 함수가 들어간다. Timer들을 heap에 유지하고 있다가 실행할때가 된 Callback 함수를 Timer Queue에 넣고 실행한다. 이 때 heap에는 무조건 오름차순으로 Timer들이 들어가있다.

Event Loop가 실행되면서 Timer Callback Queue에 callback이 들어가있는지 확인한다. Callback을 실행할 시간이 되었는지 검사하면서, 실행할 시간이 되었다면 Timer Callback을 실행한다.

예를 들어, Timer Queue에 다음과 같이 ABCDE 콜백이 들어가있다고 가정하고, 이들의 delay 시간이 각각 500이라고 해보자. 만약 Event Loop가 t + 1700인 시점에 진입했다면 힙을 하나씩 돌면서 t + 1700 전에 있는 Callback함수들은 모두 실행되면서 Queue에서 빠진다. D의 경우에는 t + 2000 이므로 더 이상 Callback이 실행되지 않고 바로 종료된다. heap에는 오름차순으로 들어가 있으므로, 바로 종료되는 것에 대해 걱정하지 않아도 된다.

timer callback이 실행되면, pending Queue 안에 수행해야 할 작업이 있는 지 먼저 확인한다. 이 pending Queue 안에는 지금 돌고 있는 Event Loop 이전에 Callback들이 들어가있다. 없으면 지나가고, 있다면 먼저 다 실행한다.

그 후에는 poll 상태로 가는데, 이 때는 watch Queue라는 큐 안에 수행해야 할 작업이 있는지 확인한다. 있다면 다 실행시켜서 비우고, 비어있다면 잠시 pending 상태가 된다.

poll이 완료되면, check를 하는데 이 때 다른 Queue들에 작업이 남아있다면, Event Loop가 종료되지 않고 다시 반복하게 된다.

이 과정들을 거쳐, Event Queue가 주는 Callback 함수들을 Call Stack에 밀어넣는다.

여기까지, 비동기 처리 로직과 Event Loop가 무엇인가에 대해 알아보았다. 결론적으로 정리하자면 다음과 같다.

  • Code가 한줄 씩 실행되면서 Call Stack에 수행될 작업이 push 된다.
  • 동기식 프로그램처럼 push된 작업이 다 수행된 후 pop하는 것이 아니라 바로 결과만 넘겨받고 pop한 후, 나머지 작업들(Call back 함수)이 실행된다.
  • 이 때, 비동기 함수들은 Web API에서 호출하고, 요청대기시간동안 pending 상태에 있다가 timeout 이후 이 작업을 Event Queue에 밀어넣는다.
  • Call Stack이 비워지는 시점에 Event Loop가 시작되고, 이 때 대기하고 있던 Event Queue는 Event Loop에 작업을 넘긴다.
  • Event Loop는 단계별 작업을 거쳐서 Callback함수를 Call Stack에 다시 push한다.
  • Call Stack은 push된 작업들을 하나씩 실행하면서 pop해 나가고, 모두 비워지면 한 turn은 끝난다.

비동기 처리 로직은 그동안에 순차적으로 처리하는 방식이 아니라서 어떤 순서로 실행되는지 예측하기 힘들다는 단점이 있어서 그동안 이해하기 어려웠지만, 구조 자체를 이해하니 이해가 되는 느낌이 든다. 이 글을 보는 독자들도, Javascript는 single thread 언어이고, 작업이 수행될 때 어떤 방식으로 수행되는지 하나씩 보면서 이해하다보면 비동기 처리 로직이 이해될 것이다.

No responses yet