JS 런타임과 JS엔진 비동기 처리

 

 

01. 자바스크립트 런타임

런타임은 프로그래밍 언어가 구동되는 환경으로, 자바스크립트 런타임은 자바스크립트 코드를 실행할 수 있는 환경을 의미한다.

이 런타임 환경은 자바스크립트 엔진과 추가적인 API, 라이브러리, 바이너리로 구성된다.

 

#1. 런타임 환경의 구성

자바스크립트 런타임은 자바스크립트 엔진을 기반으로 자바스크립트 언어로 된 모든 코드를 실행할 수 있게 한 뒤 필요한 API들을 조합하여 다양한 작업을 수행할 수 있도록 한다.

 

💡 웹브라우저

  • V8 자바스크립트 엔진 : 브라우저 내에서 자바스크립트 코드를 실행
  • Web APIs : 브라우저에서 제공하는 API. DOM 조작, 네트워크 요청 등 처리

💡 웹서버

  • V8 자바스크립트 엔진 : 서버 측에서 자바스크립트 코드 실행
  • Node APIs : 데이터베이스 연동 등 서버 측 작업을 수행할 수 있는 API
  • LIBUV : Node.js의 비동기 I/O를 처리하는 라이브러리.

 

 

02. 자바스크립트 엔진 구성 : 크롬 V8기반

자바스크립트 엔진은 자바스크립트 코드를 해석하고 실행하는 핵심 구성 요소로, Google의 V8이 대표적이다.

 

#1. 싱글 스레드

자바스크립트는 싱글 스레드 언어로, 한 번에 한가지 작업을 처리한다는 것을 의미한다.

하지만 웹 어플리케이션에서는 네트워크 요청, 타이머 등과 같이 여러 작업들이 동시에 발생할 수 있다.

이러한 작업들이 동시에 처리되지 않으면, JS는 하나의 작업이 완료될 때까지 다른 작업을 대기시키며 이로 인해 성능에 좋지 않은 영향을 미치게 된다.

위와 같은 문제를 해결하기 위해 Event Loop 매커니즘을 사용해 비동기 작업을 처리하여 자바스크립트 엔진이 주어진 작업을 처리하는 동안 다른 작업들이 대기하는 것을 막아준다.

 

 

#2. Stack + Heap

자바스크립트 엔진은 메모리 관리를 위해 스택과 힙, 두가지 메모리 영역을 사용한다.

 

◼︎ Stack

: 함수 호출과 관련된 정보를 저장하는 메모리 영역

: 함수 실행 순서대로 적재 및 수행

: LIFO 방식으로 수행

 

◼︎ Heap

: 객체, 배열, 함수 등 동적으로 할당된 변수를 저장하는 메모리 영역

: 변수들이 저장될 때 특정 순서 없이 할당

=> 자바스크립트 프로그램에서 사용하는 모든 변수, 함수가 저장되는 바구니와 같은 역할

 

 

 

 

03. 자바스크립트 엔진 비동기 처리

#1. 웹브라우저에서 비동기 지원 : 멀티 스레드

자바스크립트는 싱글 스레드 언어로 설계되어 한 번에 하나의 작업만 처리할 수 있다. 이는 코드가 순차적으로 실행된다는 것으로 각 작업이 완료되기 전까지 다른 작업이 시작될 수 없다. 이런 특성으로 동시성 문제를 피할 수 있지만, 실행 시간이 긴 작업이 실행 중일때 전체 어플리케이션의 수행이 늦어지며 응답하지 않는 상태처럼 보일 수 도 있다.

하지만, 웹 브라우저의 도움을 받아 비동기 작업을 효율적으로 처리할 수 있다. Web API와 같이 웹 브라우저에서 비동기 작업을 관리할 수 있는 기능들을 제공하여 이를 통해 멀티 스레드인 것처럼 동작시킬 수 있는 것이다.

 

💡 Web APIs

setTimeout, Fetch API, Web Workers API등 비동기 작업을 처리하는데 사용되며, 자바스크립트 엔진이 직접 제어하지 않지만, 자바스크립트 코드에서 이 API들을 호출.

 

💡 Call Stack

자바스크립트 엔진은 모든 함수 호출을 Call Stack에 쌓아 처리하며, 싱글 스레드이기 때문에 Call Stack에선느 한 번에 하나의 함수만 실행된다. Call Stack이 비어있으면 새로운 작업을 실행할 수 있다.

 

💡 Task Queue

Task Queue는 실행 대기 중인 콜백 함수들의 목록으로, 비동기 작업이 완료되면 해당 작업의 콜백 함수가 Task Queue에 대기하게 된다.

 

💡 Event Loop

Event Loop는 Call Stack과 Task Queue를 관리하는 중요한 역할을 담당하여, Call Stack을 지속적으로 모니터링하고, Call Stack이 비어있으면 Task Queue에서 대기중인 콜백 함수를 가져와 Call Stack에 넣어 실행한다.

 

 

🎯 비동기 처리 과정 🎯

  1. 비동기 작업 요청 : setTimeout, fetch와 같은 비동기 API가 호출되면, 해당 작업이 Call Stack에서 실행된 후 Web APIs로 제어가 넘어감
  2. 작업 대기 : Web API가 비동기 작업을 처리하며, setTimeout의 경우 지정된 시간이 경과한 후 콜백 함수를 실행할 준비 상태로 대기시킴
  3. Task Queue로 이동 : 비동기 작업 완료되면, Web API가 해당 작업의 콜백 함수를 Task Queue로 보냄
  4. Event loop : Call Stack이 비어있는지 확인하고, 비어있으면 Task Queue에서 대기 중인 콜백 함수를 Call Stack에 넣어 실행

위와 같은 과정을 통해 자바스크립트는 싱글 스레드이지만 멀티 스레드처럼 비동기 작업을 처리할 수 있다. 

https://www.youtube.com/watch?si=OGK6uaEr4WIahVev&v=eiC58R16hb8&feature=youtu.be

더 자세한 내용은 위 영상을 통해 참고하면 된다.

 

 

 

 

#2. 웹서버에서의 비동기 지원 : 멀티 스레드 + 비동기 I/O 라이브러리

웹 브라우저에서는 주로 클라이언트 측의 비동기 작업을 처리하지만, 웹서버에서는 대규모 요청을 처리하기 위해 효율적인 비동기 I/O 처리가 필수적이다. Node.js는 이러한 비동기 처리를 지원하는 대표적인 런타임 환경으로 이를 가능하게 하는 주요 요소로는 Node API와 libuv가 있다.

 

Node.js는 자바스크립트 엔진을 기반으로 구축된 서버 측 런타임 환경이다.

싱글 스레드 이벤트 루프를 통해 대규모 네트워크 요청을 효율적으로 처리할 수 있도록 설계되었으며, 비동기 I/O 처리를 지원하여 네트워크 요청 등의 작업이 완료될 때까지 서버가 다른 작업을 계속 수행할 수 있게 해준다.

 

💡 Node APIs

HTTP 요청 처리, 데이터베이스 연결 등 대부분의 I/O 작업이 비동기적으로 수행되며, 이러한 비동기 작업을 처리하는데 사용

 

💡 libuv

Node.js의 비동기 I/O 모델을 지원하는 라이브러리이다. 다양한 운영 체제에서 일관된 비동기 I/O처리를 가능하게 해준다.

libuv는 이벤트 루프와 스레드 풀을 관리하며, Node.js에서 비동기 작업을 원활하게 처리하도록 돕는다.

libuv 내에서 이벤트 큐, 이벤트 루프, worker threads(스레드풀)을 가지고 있어 worker threads가 작업을 할당해 비동기 작업을 처리하도록 돕는다.

 

💡 Event Queue

비동기 작업이 완료되면 그 작업의 콜백 함수가 Event Queue에 추가된다. Event Queue는 웹브라우저에서의 Task Queue와 유사한 역할을 하며, 비동기 작업의 결과를 처리할 콜백 함수들이 대기하는 장소이다.

 

💡 Event Loop

Event Loop는 Event Queue에서 대기중인 콜백 함수들을 가져와서 Worker Threads로 작업들을 할당한다.

 

💡 Worker Threads

 

 

 

 

04. 자바스크립트 프레임워크 동작 원리

#1. 번들러 : 다수의 JS를 하나의 JS 파일로 압축

만약 1000개의 자바스크립트 파일을 전송할 때 이를 개별적으로 각 파일을 전송하게 되면 1000번의 네트워크 요청이 발생하게 되어 성능 저하를 발생시킬 수 있다. 이를 해결하기 위해 번들링과 코드 스플리팅 과정이 수행된다.

번들링은 여러 자바스크립트 파일들을 하나의 파일로 묶어 네트워크의 요청 수를 줄이는 기술로, 번들러는 웹 어플리케이션에서 사용되는 여러 자바스크립트 파일들을 하나의 파일로 묶어 관리하는 도구이다.

번들링을 통해 파일 수를 줄이고, 네트워크 요청을 최소화하여 페이지 로딩 속도를 줄일 수 있다.

 

하지만, 번들링 된 파일의 크기가 매우 커질 수 있으며, 큰 파일을 한 번에 전송하는 것은 네트워크 효율성에 좋지 않을 수 있다. 이는 특히 네트워크 속도가 느리거나 모바일 환경에서 문제가 될 수 있다. 이러한 번들링의 단점을 보완하기 위해 코드 스플리팅을 사용한다.

 

코드 스플리팅은 번들링 된 파일을 다시 쪼개, 필요한 코드만들 다운로드 하도록 하는 기술이다.

이러면 한꺼번에 코든 코드를 다운로드 할 필요 없이, 사용자가 보고자 하는 페이지에 필요한 코드만 다운로드하여 로드 속도를 개선할 수 있다. 코드 스플리팅은 페이지별로 적용할 수 있고, 다이나믹 로딩으로 페이지에서 필요한 부분만, 즉 사용자에게 먼저 노출되어지는 부분만 먼저 다운로드하여 미리 보여지게 할 수 있다.

 

다이나믹 로딩(Dynamic Loading)
다이나믹 로딩은 사용자가 현재 필요한 컴포넌트만 다운로드하여 페이지 로드 속도를 높이는 기술이다.
이 방식을 사용하면 사용자가 페이지를 처음 방문했을 때 필요한 최소한의 코드만 로드되며, 나머지 코드는 필요할 때 비동기적으로 로드된다. 뷰포트를 기준으로 뷰포트에 들어오지 않은 콘텐츠는 나중에 로드하도록 설정하여 다이나믹 로딩을 이용할 수 있다.

 

 

💡 Webpack

Webpack은 가장 많이 사용되는 번들러 중 하나로, 모든 자바스크립트 파일을 하나의 번들로 묶어 웹 어플리케이션의 성능을 최적화할 수 있다. Webpack은 세부 설정을 통해 특정 페이지에 필요한 자바스크립트 파일들만 별도의 번들로 묶는 코드 스플리팅을 지원한다. 이 방식을 사용하면 사용자가 페이지에 접근할 때 전체 번들 파일을 다운로드 하지 않고도, 해당 페이지에 필요한 코드만 로드하게 된다.

 

💡 Vite

Vite는 최신 번들러로, 빠른 개발 서버와 효율적인 번들링을 제공한다. ESBuild라는 고속 번들러를 기반으로 하며, 각 페이지에서 필요한 자바스크립트 모듈들만 번들링한다. 코드 스플리팅과 Dynamic Import를 통해 페이지마다 필요한 모듈만 로드해 최적화된 성능을 제공하며, 이로 인해 개발 중 빠른 피드백을 받을 수 있는 장점도 있다.
 

 

 

#2. 트랜스파일러/컴파일러 : 고버전 JS/TS를 저버전 JS로 변환

웹 어플리케이션이 다양한 브라우저 환경에서 정상적으로 동작하기 위해 자바스크립트 코드가 여러 버전의 ECMAScript(ES) 표준을 지원해야 한다. 트랜스파일러와 컴파일러는 고버전의 JS 혹은 TS 코드를 저버전의 자바스크립트로 변환하여 호환성을 보장해준다.

 

💡 다양한 웹 브라우저에서의 저버전 ES 지원을 위한 Babel과 Polyfill

  • Babel : 자바스크립트 트랜스파일러로, 최신 ES6/ES7 등의 코드를 더 오래된 버전인 ES5 등으로 변환해 모든 브라우저에서 호환 가능하게 한다. 
  • Polyfill : 일부 최신 자바스크립트 기능은 단순히 문법 변환으로 해결되지 않는데, 이 경우 Polyfill을 사용하여 오래된 브라우저 환경에서도 최신 기능을 사용할 수 있도록 추가적인 코드나 라이브러리 등을 제공받을 수 있다. promise, fetch와 같은 기능은 polyfill을 통해 지원되기도 한다.

 

 

💡 TypeScript : 정적 타입을 통한 JS 안정성 보장

TypeScript는 자바스크립트에 정적 타입을 추가한 언어로, 컴파일 시 오류를 발견할 수 있게 도와준다. TypeScript도 결국 자바스크립트로 변환되어 실행되기 때문에 개발 단계에서 타입 검사를 통해 코드의 안정성을 높일 수 있다.

 

TypeScript Loader는 TypeScript 코드를 자바스크립트로 변환하는 과정에서 사용되는 도구이다. TypeScript코드를 트랜스파일링하여 자바스크립트로 변환하는 과정에서 성능 이슈가 발생할 수 있는데, 이 경우 Babel과 함께 사용하면 타입스크립트에서 자바스크립트 변환을 효율적으로 처리할 수 있다.

 

 

반응형