매일 업데이트
2022-11-22 05:19 8 min

Event Loop는 JavaScript에서 어떻게 작동합니까?

실제 프로덕션 코드를 작성하려면 C++이나 C와 같은 언어에 대한 깊은 이해가 필요할 수 있지만, JavaScript는 종종 언어의 기본적인 작동 방식만 알아도 충분히 코딩할 수 있습니다.

함수에 콜백을 전달하거나 비동기 코드를 만드는 등의 개념은 일반적으로 어렵지 않기 때문에, 대부분의 JavaScript 개발자들은 내부적으로 무슨 일이 일어나는지에 대해 크게 신경 쓰지 않습니다. 그들은 언어에 의해 추상화된 복잡한 부분에 대해 깊이 이해하려 하지 않습니다.

하지만 JavaScript 개발자로서, 내부에서 실제로 무슨 일이 벌어지는지, 그리고 우리에게서 추상화된 복잡한 부분들이 어떻게 작동하는지 이해하는 것이 점점 더 중요해지고 있습니다. 이러한 지식은 더 나은 결정을 내리는 데 도움을 주며, 결과적으로 코드 성능을 크게 향상시킬 수 있습니다.

이 글에서는 JavaScript에서 매우 중요하지만 종종 제대로 이해되지 않는 개념 중 하나인 '이벤트 루프'에 대해 집중적으로 다룰 것입니다.

JavaScript에서 비동기 코드를 작성하는 것은 피할 수 없지만, 비동기적으로 실행되는 코드가 실제로 무엇을 의미할까요? 바로 이벤트 루프를 통해 그 의미를 알 수 있습니다.

이벤트 루프가 어떻게 작동하는지 이해하기 전에, 먼저 JavaScript 자체가 무엇이고 어떻게 동작하는지 알아야 합니다!

자바스크립트란 무엇일까요?

본론으로 들어가기 전에, 잠시 기본적인 내용으로 돌아가 봅시다. JavaScript란 무엇일까요? 간단히 정의하자면 다음과 같습니다.

JavaScript는 고수준의 인터프리터 언어이며, 단일 스레드, 논블로킹, 동시적, 그리고 비동기적인 특징을 가지고 있습니다.

잠깐, 이게 무슨 뜻이지? 마치 교과서에 나오는 정의 같네요. 🤔

좀 더 자세히 풀어보겠습니다!

이 글과 관련된 핵심 키워드는 '단일 스레드', '논블로킹', '동시성', 그리고 '비동기'입니다.

단일 스레드

실행 스레드는 스케줄러가 독립적으로 관리할 수 있는 명령의 최소 단위입니다. 프로그래밍 언어가 단일 스레드라는 것은, 한 번에 하나의 작업만 수행할 수 있다는 의미입니다. 즉, 스레드는 중단되거나 멈추지 않고 처음부터 끝까지 하나의 프로세스를 실행합니다.

이는 여러 스레드에서 동시에 코드를 실행할 수 있는 다중 스레드 언어와는 대조적입니다. 다중 스레드 언어에서는 여러 프로세스가 서로를 방해하지 않고 동시에 실행될 수 있습니다.

그렇다면 JavaScript는 어떻게 단일 스레드이면서 동시에 논블로킹일 수 있을까요?

먼저 '블로킹'이 무엇을 의미하는지 알아봅시다.

논블로킹

'블로킹'에 대한 명확한 정의는 없지만, 스레드에서 실행 시간이 오래 걸리는 작업을 의미합니다. 따라서 '논블로킹'은 스레드에서 실행 시간이 오래 걸리지 않는다는 의미입니다.

잠깐, JavaScript가 단일 스레드에서 실행된다고 말했나요? 그리고 논블로킹이라고 했는데, 이는 모든 작업이 호출 스택에서 빠르게 실행된다는 의미일까요? 하지만 어떻게 가능할까요? 타이머를 실행하거나 루프를 돌 때는 어떻게 해야 할까요?

걱정하지 마세요! 곧 그 해답을 찾을 수 있을 겁니다. 😉

동시성

동시성은 코드가 둘 이상의 스레드에 의해 동시에 실행되는 것처럼 보이는 것을 의미합니다.

상황이 점점 흥미로워지고 있네요. 기이하게 들릴 수도 있지만, JavaScript는 어떻게 단일 스레드이면서 동시에 동시적일 수 있을까요? 즉, 어떻게 두 개 이상의 스레드에서 코드를 실행할 수 있을까요?

비동기식

비동기 프로그래밍은 코드가 이벤트 루프에서 실행된다는 것을 의미합니다. 블로킹 작업이 발생하면 이벤트가 시작됩니다. 블로킹 코드는 메인 실행 스레드를 막지 않고 계속 실행됩니다. 블로킹 코드 실행이 완료되면 결과가 큐에 추가되고 다시 스택으로 푸시됩니다.

하지만 JavaScript에는 단일 스레드만 존재한다고 하지 않았나요? 그렇다면 스레드의 다른 코드가 실행되도록 하면서 이 블로킹 코드를 실행하는 것은 무엇일까요?

본론으로 들어가기 전에 지금까지의 내용을 요약해 보겠습니다.

  • JavaScript는 단일 스레드입니다.
  • JavaScript는 논블로킹입니다. 즉, 실행 시간이 오래 걸리는 프로세스가 실행을 막지 않습니다.
  • JavaScript는 동시적입니다. 즉, 여러 스레드에서 동시에 코드를 실행하는 것처럼 보입니다.
  • JavaScript는 비동기식입니다. 즉, 블로킹 코드는 다른 곳에서 실행됩니다.

하지만 위의 내용을 모두 합쳐보면 논리적으로 맞지 않는 부분이 있습니다. 어떻게 단일 스레드 언어가 논블로킹, 동시적, 그리고 비동기적일 수 있을까요?

좀 더 깊이 들어가서 JavaScript 런타임 엔진인 V8을 살펴보겠습니다. 아마 우리가 모르는 숨겨진 스레드가 있을지도 모릅니다.

V8 엔진

V8 엔진은 Google에서 C++로 만든 오픈 소스 고성능 웹 어셈블리 런타임 엔진입니다. 대부분의 브라우저가 V8 엔진을 사용하여 JavaScript를 실행하며, 인기 있는 Node.js 런타임 환경도 이를 사용합니다.

간단히 말해서, V8은 JavaScript 코드를 받아 컴파일하고 실행하는 C++ 프로그램입니다.

V8은 두 가지 주요 작업을 수행합니다.

  • 힙 메모리 할당
  • 호출 스택 실행 컨텍스트

안타깝게도 우리의 의심은 빗나갔습니다. V8에는 호출 스택이 하나밖에 없습니다. 호출 스택을 스레드라고 생각하면 됩니다.

하나의 스레드 === 하나의 호출 스택 === 한 번에 하나의 실행.

V8에는 호출 스택이 하나밖에 없는데, JavaScript는 어떻게 메인 실행 스레드를 차단하지 않고 동시적이고 비동기적으로 실행될 수 있을까요?

간단하지만 일반적인 비동기 코드를 작성하여 함께 알아보고 분석해 봅시다.

JavaScript는 각 코드를 한 줄씩, 순서대로 실행합니다(단일 스레드). 예상대로 첫 번째 줄은 콘솔에 출력되지만, 왜 마지막 줄이 타임아웃 코드보다 먼저 출력될까요? 왜 실행 프로세스는 마지막 줄을 실행하기 전에 타임아웃 코드(블로킹)를 기다리지 않을까요?

스레드는 한 번에 하나의 작업만 실행할 수 있다고 확신하기 때문에, 다른 스레드가 그 제한 시간을 처리하는 데 도움을 준 것 같습니다.

잠시 동안 V8 소스 코드를 들여다봅시다.

어떻게 된 거죠? V8에는 타이머 기능도 없고, DOM도 없나요? 이벤트도 없고, Ajax도 없다니... 예!!!

이벤트, DOM, 타이머 등은 JavaScript 코어 구현의 일부가 아닙니다. JavaScript는 EcmaScript 사양을 엄격하게 따르며, 다양한 버전은 종종 EcmaScript 사양(ES X)에 따라 참조됩니다.

실행 워크플로우

이벤트, 타이머, Ajax 요청은 모두 클라이언트 측에서 브라우저에 의해 제공되며, 종종 웹 API라고 불립니다. 이것이 단일 스레드 JavaScript가 논블로킹, 동시적, 그리고 비동기적으로 실행될 수 있게 하는 이유입니다! 하지만 어떻게 가능할까요?

모든 JavaScript 프로그램의 실행 워크플로우에는 호출 스택, 웹 API, 그리고 작업 큐의 세 가지 주요 부분이 있습니다.

호출 스택

스택은 마지막으로 추가된 요소가 항상 스택에서 가장 먼저 제거되는 데이터 구조입니다. 쌓아 놓은 접시 더미와 비슷하다고 생각하면 됩니다. 가장 위에 놓인 접시만 먼저 꺼낼 수 있죠. 호출 스택은 작업이나 코드가 실행되는 스택 데이터 구조일 뿐입니다.

아래의 예시를 살펴보겠습니다.

출처 – https://youtu.be/8aGhZQkoFbQ

printSquare() 함수를 호출하면 호출 스택에 푸시되고, printSquare() 함수는 다시 square() 함수를 호출합니다. square() 함수는 스택에 푸시되고, 또한 multiply() 함수를 호출합니다. multiply() 함수가 스택에 푸시됩니다. multiply() 함수가 값을 반환하고 스택에 마지막으로 푸시되었기 때문에 먼저 실행되고 스택에서 제거된 다음, square() 함수와 printSquare() 함수가 순서대로 실행됩니다.

웹 API

여기에서 V8 엔진이 처리하지 않는 코드가 실행되며, 메인 실행 스레드를 "블로킹"하지 않습니다. 호출 스택에서 웹 API 함수를 만나면, 프로세스는 즉시 웹 API로 넘겨져 실행되고 호출 스택은 실행 중에 다른 작업을 수행할 수 있습니다.

위의 setTimeout 예시로 돌아가 봅시다.

코드를 실행하면 첫 번째 console.log 라인이 스택으로 푸시되고 거의 즉시 출력됩니다. 타임아웃에 도달하면 타이머는 브라우저에서 처리되며, V8 코어 구현의 일부가 아닙니다. 대신 웹 API로 넘겨져 스택이 비워지고 다른 작업을 수행할 수 있게 됩니다.

타임아웃이 계속 실행되는 동안 스택은 다음 작업 라인으로 이동하여 마지막 console.log를 실행합니다. 이것이 왜 타이머 출력 전에 출력되는지를 설명해 줍니다. 타이머가 완료되면 무슨 일이 일어날까요? 다음 타이머의 console.log가 마법처럼 호출 스택에 다시 나타납니다!

어떻게 가능할까요?

이벤트 루프

이벤트 루프에 대해 논의하기 전에, 먼저 태스크 큐의 역할을 살펴보겠습니다.

타임아웃 예시로 돌아가서, 웹 API가 작업 실행을 완료하면 자동으로 호출 스택으로 다시 푸시하지 않습니다. 대신 작업 큐로 이동합니다.

큐는 선입선출 원칙에 따라 작동하는 데이터 구조이므로, 태스크가 큐에 푸시되면 동일한 순서대로 큐에서 나옵니다. 웹 API에 의해 실행되어 태스크 큐로 푸시된 작업은 호출 스택으로 돌아가서 결과를 출력합니다.

잠깐, 이벤트 루프는 도대체 무엇일까요???

출처 – https://youtu.be/8aGhZQkoFbQ

이벤트 루프는 태스크 큐에서 콜백을 호출 스택으로 푸시하기 전에 호출 스택이 완전히 비워질 때까지 기다리는 프로세스입니다. 스택이 비워지면 이벤트 루프가 트리거되어 사용 가능한 콜백에 대한 태스크 큐를 확인합니다. 만약 있다면 호출 스택으로 푸시하고, 호출 스택이 다시 비워질 때까지 기다린 다음 같은 과정을 반복합니다.

출처 – https://www.quora.com/How-does-an-event-loop-work/answer/Timothy-Maxwell

위의 다이어그램은 이벤트 루프와 태스크 큐 사이의 기본적인 워크플로우를 보여줍니다.

결론

이 글은 매우 기본적인 소개에 불과하지만, JavaScript의 비동기 프로그래밍 개념이 내부에서 어떻게 작동하는지, 그리고 JavaScript가 어떻게 단일 스레드로 동시적이고 비동기적으로 실행될 수 있는지에 대한 충분한 통찰력을 제공합니다.

JavaScript는 항상 수요가 높으며, 배우고 싶다면 다음 Udemy 강좌를 확인해 보는 것이 좋습니다. 유데미 강좌.

저자
Korea

기술 트렌드와 실용적인 팁을 전하는 लेखक입니다.