자바스크립트는 연산을 순차적으로 실행하도록 설계된 단일 스레드 아키텍처를 사용합니다. 그럼에도 불구하고 이벤트 루프는 멀티스레드 환경을 시뮬레이션할 수 있는 기능을 통해 비동기 처리를 가능하게 하여 JavaScript 애플리케이션에서 최적의 성능을 보장합니다.

자바스크립트 이벤트 루프란?

각 자바스크립트 애플리케이션 내의 유비쿼터스 구성 요소인 이벤트 루프는 기본 실행 흐름을 유지하면서 작업의 순차적 처리를 용이하게 하여 눈에 잘 띄지 않는 방식으로 작동합니다. 비동기 프로그래밍으로 알려진 이 프로세스를 통해 JavaScript를 사용하면 작업을 중단 없이 동시에 관리할 수 있습니다.

이벤트 루프는 실행할 작업 목록을 유지 관리하고 처리를 위해 적절한 웹 애플리케이션 프로그래밍 인터페이스(API)에 순차적으로 제시하며, 각 작업은 복잡성에 따라 자바스크립트에 의해 처리됩니다.

자바스크립트 이벤트 루프와 비동기 프로그래밍의 필요성을 파악하기 위해서는 이벤트 루프가 근본적으로 해결하는 문제를 이해해야 합니다.

앞서 언급한 코드를 예로 들어 보겠습니다:

 function longRunningFunction() {
  // This function does something that takes a long time to execute.
  for (var i = 0; i < 100000; i++) {
    console.log("Hello")
  }
}

function shortRunningFunction(a) {
  return a * 2 ;
}

function main() {
  var startTime = Date.now();
  longRunningFunction();
  
  var endTime = Date.now();

  // Prints the amount of time it took to execute functions
  console.log(shortRunningFunction(2));
  console.log("Time taken: " + (endTime - startTime) + " milliseconds");
}

main();

이 코드에는 “longRunningFunction”이라는 함수가 도입되어 있습니다. 이 함수의 목적은 복잡하고 리소스 집약적인 작업을 수행하는 것입니다. 구체적으로 이 함수는 총 10만 번 실행되는 for 루프로 구성됩니다. 따라서 “console.log(‘Hello’)” 명령은 동일한 횟수의 반복으로 실행되므로 콘솔에 상당한 양의 출력이 생성됩니다.

컴퓨터의 처리 능력과 관련하여 코드를 실행하는 데 걸리는 시간으로 인해 후속 함수의 실행을 진행하기 전에 대기 시간이 길어질 수 있습니다. 이러한 지연으로 인해 shortRunningFunction()의 즉각적인 실행이 방해될 수 있습니다.

두 함수의 상대적 성능을 이해하기 위해 두 함수의 실행 시간을 비교했습니다.

`singleShortRunningFunction()` 함수는 일반적으로 즉각적인 필요 이상의 외부 종속성이나 리소스 없이 제한된 범위와 기간 내에서 의도한 연산을 수행하는 독립적인 메서드입니다. 이 메서드는 작업을 간소화된 방식으로 실행하여 신속하고 효율적으로 완료하며 다른 프로세스나 시스템에 심각한 중단을 일으키지 않습니다. “단기 실행”이라는 용어는 최소한의 리소스 요구 사항으로 설계되어 오버헤드를 최소화하면서 작업을 빠르고 효과적으로 완료할 수 있다는 것을 의미합니다.

이 글도 확인해 보세요:  GitHub 코파일럿의 이점이 잠재적인 단점을 감수할 가치가 있을까요?

성능에 중점을 두고 애플리케이션을 개발할 때, 완료하는 데 2,351밀리초가 걸리는 절차와 전혀 시간이 걸리지 않는 절차의 차이가 얼마나 큰지 쉽게 알 수 있습니다.

이벤트 루프가 앱 성능에 도움이 되는 방법

이벤트 루프는 다양한 단계와 구성 요소로 구성되어 있으며, 이를 통해 시스템 작동을 종합적으로 촉진할 수 있습니다.

호출 스택

자바스크립트에서 호출 스택은 애플리케이션 내에서 함수 및 이벤트 호출을 관리하는 데 중요한 역할을 합니다. 코드는 위에서 아래로 순차적으로 컴파일되지만, Node.js로 실행할 경우 Node.js는 코드를 역순으로 읽고 함수 호출을 아래에서 위로 처리합니다. 따라서 읽는 동안 각 함수 정의를 만나면 Node.js는 해당 함수를 호출 스택에 프레임으로 추가하여 함수 실행을 효율적으로 관리할 수 있습니다.

호출 스택은 실행 컨텍스트를 보존하고 함수 호출의 적절한 순서를 보장하는 데 필수적인 역할을 하며, 이를 위해 LIFO(Last-In-First-Out) 구조로 구현됩니다.

프로그램 내 함수의 배열은 함수가 호출 스택에 푸시되고 이후 호출 스택에서 팝되는 방식에 따라 결정됩니다. 이 프로세스를 통해 호출 스택에 추가된 마지막 함수 프레임이 먼저 실행되므로 함수 호출의 시간 순서가 유지됩니다.

자바스크립트를 사용하면 해당 함수가 실행을 완료하면 모든 프레임이 스택에서 제거되어 스택이 비어 있게 됩니다.

Libuv 웹 API

자바스크립트 비동기 프로그램의 핵심은 libuv 입니다. libuv 라이브러리는 운영 체제의 저수준 API와 상호 작용할 수 있는 C 프로그래밍 언어로 작성되었습니다. 이 라이브러리는 JavaScript 코드를 다른 코드와 병렬로 실행할 수 있는 여러 API를 제공합니다. 스레드 생성을 위한 API, 스레드 간 통신을 위한 API, 스레드 동기화를 관리하기 위한 API가 있습니다.

Node.js는 libuv 라이브러리를 사용하여 이벤트 루프를 쉽게 관리하고 setTimeout()을 사용하여 특정 기간이 경과한 후 콜백 함수를 실행하기 위한 타이머를 예약할 수 있습니다. 이 프로세스에는 원하는 시간에 콜백 함수를 트리거할 타이머를 설정하여 해당 간격이 충족되면 자바스크립트 엔진이 실행을 재개할 수 있도록 하는 작업이 포함됩니다.

비슷한 맥락에서 비동기 네트워크 작업이 수행될 때, libuv는 입출력 프로세스가 완료되기를 기다리는 동안 다른 작업을 진행할 수 있도록 허용함으로써 비방해적인 방식으로 작업을 관리합니다.

콜백 및 이벤트 큐

콜백 및 이벤트 큐는 실행을 예상하는 콜백 함수를 위한 대기 영역 역할을 합니다. 비동기 작업이 libuv에 의해 완료되면 관련 콜백 함수가 이 큐에 배치됩니다.

이 글도 확인해 보세요:  Python을 사용하여 할 일 목록 프로그램 만들기

일련의 이벤트는 다음과 같습니다:

JavaScript는 libuv의 기능을 활용하여 비동기 작업을 효율적으로 관리하면서 지연 없이 후속 작업을 처리하도록 원활하게 전환합니다.

JavaScript에서 비동기 작업이 완료되면 해당 콜백 함수가 해당 작업을 위해 지정된 대기열에 추가됩니다.

자바스크립트는 ‘호출 스택’으로 알려진 특정 실행 순서를 따르며, 각 함수 호출은 이전 프레임 위에 새 프레임을 생성합니다. 엔진은 모든 함수가 각각의 순서대로 완료될 때까지 이 프레임 내에서 후속 작업을 계속 실행합니다.

호출 스택이 모두 소진되면 자바스크립트는 콜백 대기열을 검사하여 추가 지침을 확인합니다.

콜백이 대기열 내에 있는 경우 콜백 시퀀스의 초기 항목을 호출 스택으로 푸시하여 실행합니다.

이러한 방식으로 비동기적으로 실행되는 작업은 기본 실행 스레드를 방해하지 않으며, 콜백 큐는 연결된 콜백 함수가 완료된 정확한 순서대로 수행되도록 보장합니다.

이벤트 루프 사이클

이벤트 루프에는 호출 스택에서 현재 작업이 완료되면 즉시 실행하기 위한 마이크로태스크를 수용하는 이벤트 루프 내의 고유한 큐인 마이크로태스크 큐가 있습니다. 이러한 마이크로태스크는 후속 렌더링 또는 이벤트 루프 반복 전에 실행됩니다. 특히 마이크로태스크는 이벤트 루프 내의 다른 작업보다 우선순위가 높으며, 다른 작업보다 우선합니다.

프로그래밍에서 프로미스를 활용할 때, 개발자는 프로미스의 각 .then() 또는 .catch() 콜백이 해결되거나 거부되면 마이크로태스크 대기열에 추가되는 마이크로태스크를 만드는 것이 일반적입니다. 이러한 마이크로태스크는 사용자 인터페이스 요소를 업데이트하거나 애플리케이션 내에서 상태 전환을 관리하는 등 즉각적인 실행이 필요한 작업을 수행하는 데 사용할 수 있습니다.

웹 기반 애플리케이션의 사용자 인터페이스는 버튼이 클릭될 때마다 새 데이터로 업데이트되며, 이는 소스에서 정보를 검색하는 비동기 프로세스를 트리거합니다. 원하는 업데이트를 얻기 위해 버튼을 눌러야 하는 빈도는 시스템의 특정 요구 사항에 따라 달라질 수 있습니다.

이 작업에 대한 이벤트 루프 작동은 마이크로태스크가 없는 경우 다음 단계를 실행하여 수행됩니다:

인터페이스와 상호 작용할 때 개인은 디지털 장치를 사용하여 지정된 버튼을 연속으로 여러 번 탭하거나 클릭합니다.

버튼을 누를 때마다 비동기식으로 개별적인 정보 검색이 시작됩니다.

데이터 검색 작업이 완료되면 JavaScript는 처리를 위해 해당 콜백 함수를 표준 작업 대기열에 추가합니다.

이 글도 확인해 보세요:  JES를 활용한 흥미로운 사운드 처리 기법 3가지

기존 작업 큐에 할당된 작업을 포함하여 이벤트 루프 내에서 작업 실행 프로세스가 시작됩니다.

데이터 검색 프로세스의 결과에 따라 달라지는 사용자 인터페이스 업데이트의 실행은 사용자 지정 작업에서 진행을 허용한 후에 발생합니다.

기존 작업과 달리 마이크로태스크 활용 시 이벤트 루프 작동이 구분됩니다.

지정된 버튼을 클릭하면 비동기적으로 데이터를 검색하는 작업이 시작됩니다.

데이터 검색에 성공하면 이벤트 루프가 실행을 위해 연결된 콜백 함수를 마이크로태스크 대기열에 추가합니다.

이벤트 루프는 버튼 클릭과 같은 현재 작업이 완료되면 즉시 마이크로태스크 대기열 내의 작업 실행을 시작합니다.

데이터 검색 프로세스의 결과에서 파생된 사용자 인터페이스 개선의 구현이 후속 일상 작업에 앞서 수행되어 사용자 경험이 개선되고 반응성이 높아집니다.

물론, 여기에 제공된 코드 조각의 우아한 버전이 있습니다: “`python def my_function(x): if x > 0: 반환 “양수” elif x < 0: 반환 "음수" else: 반환 "0" ```

 const fetchData = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve('Data from fetch'), 2000);
  });
};

document.getElementById('fetch-button').addEventListener('click', () => {
  fetchData().then(data => {
    // This UI update will run before the next rendering cycle
    updateUI(data);
  });
});

이 예제에서 “Fetch” 버튼을 클릭하면 fetchData() 함수가 호출됩니다. 데이터 검색 프로세스는 마이크로태스크로 처리되며, 완료 후 다른 렌더링 또는 작업 대기열 작업보다 먼저 지체 없이 사용자 인터페이스 업데이트가 실행됩니다.

이 접근 방식을 구현하면 사용자는 이벤트 루프 내에서 진행 중인 작업으로 인한 지연 없이 최신 정보를 볼 수 있습니다.

이러한 컨텍스트 내에서 마이크로태스크를 활용하면 보다 원활한 사용자 인터페이스 환경을 제공하여 애플리케이션의 전반적인 응답성을 향상시키면서 인지할 수 있는 ‘끊김 현상’을 완화할 수 있습니다.

웹 개발을 위한 이벤트 루프의 시사점

고성능의 반응형 소프트웨어 솔루션을 제작하려면 이벤트 루프의 특성을 포괄적으로 파악하고 그 기능을 능숙하게 활용하는 것이 필수적입니다. 이벤트 루프는 원활한 사용자 인터페이스를 유지하면서 애플리케이션 내에서 복잡한 작업을 효율적으로 실행할 수 있는 비동기 및 동시 기능을 제공합니다.

Node.js는 기본 JavaScript 실행 컨텍스트 외에 추가적인 병렬 처리를 위한 웹 워커 활용을 포함하는 포괄적인 솔루션을 제공합니다.

By 김민수

안드로이드, 서버 개발을 시작으로 여러 분야를 넘나들고 있는 풀스택(Full-stack) 개발자입니다. 오픈소스 기술과 혁신에 큰 관심을 가지고 있고, 보다 많은 사람이 기술을 통해 꿈꾸던 일을 실현하도록 돕기를 희망하고 있습니다.