메뉴 건너뛰기

조회 수 138 추천 수 0 댓글 0
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄 첨부
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄 첨부

JavaScript를 공부하는 사람이라면 한 번쯤은 JavaScript가 단일 쓰레드 기반의 프로그래밍 언어라는 말을 들은 적이 있을 것이다. 그렇다면 setTimeout() 함수로 특정 시간을 기다림과 동시에 다른 코드를 동작시키는 등의 비동기 처리는 어떻게 가능한 것일까? 단일 쓰레드라면 스택이 하나이므로 모든 작업은 하나씩 순서대로 이뤄져야 하기에 이는 말이 안 된다. 이를 이해하기 위해서는 JavaScript의 런타임에 대한 이해가 필요하다. 필자도 이에 대한 이해가 부족하여 이번 기회에 직접 찾아보며 공부해보았고, 그 결과를 이 포스팅에 정리하고자 한다. 혹시 틀린 점이 있다면 댓글로 지적해주기 바란다.

 

1. JavaScript 런타임

런타임이란 해당 프로그래밍 언어로 작성된 코드가 구동되는 환경을 말한다. 웹 브라우저와 Node.js가 대표적인 JavaScript 런타임이다. 웹 브라우저의 JavaScript 런타임은 크게 두 가지의 구성 요소로 이뤄져 있다. 바로 JavaScript 엔진과 웹 API이다. 이 중에서 JavaScript 엔진이 JavaScript 코드를 읽고 해석해서 실행하는 것을 담당하는 것이며, 이는 단일 쓰레드로만 동작한다. 이것이 바로 JavaScript를 단일 쓰레드 기반의 프로그래밍 언어라 하는 이유이다.

 

하지만 미리 말했듯 JavaScript 런타임은 JavaScript 엔진으로만 구성되는 것이 아니다. JavaScript 엔진의 바깥에도 웹 API라는 부분이 존재한다. 또한, JavaScript 엔진 자체에도 태스크 큐와 이벤트 루프라는 개념이 존재한다. 이러한 것들로 인해 단일 쓰레드 기반의 프로그래밍 언어라 불리는 JavaScript도 비동기적으로 코드를 실행시킬 수 있게 되는 것이다. 먼저 그 원리를 도식화해보자면 다음과 같다. 자세한 내용은 바로 뒤에 이어지는 설명을 참조하자.

 

 

 

* 본 포스팅의 내용을 이해하고 나서는 아래의 링크에 들어가서 동작 원리를 애니메이션으로 확인해보기 바란다.
▶ http://latentflip.com/loupe

 

2. JavaScript 엔진

JavaScript 코드를 읽고 해석해서 실행하는 것을 담당하는 일종의 인터프리터이다. 가장 유명한 것은 구글에서 만든 크롬 브라우저의 V8 엔진이다. 기본적으로는 웹 브라우저에 탑재되어 있고, 이후 등장한 Node.js에도 탑재가 되어 있다. JavaScript 엔진의 가장 큰 특징은 단일 쓰레드 기반이라는 것이다. 즉, 쓰레드 하나로만 동작하기 때문에 스택 영역도 단 하나만 사용하게 된다. 이로 인해 하나의 함수가 실행이 완료되기 전에는 다른 함수가 실행될 수 없는 "Run to Completion" 특성을 가지게 된다. 이러한 맥락에서 JavaScript 엔진에 해당하는 쓰레드는 JavaScript 코드가 실행되는 "JavaScript 전용 특급 선로"로 비유할 수 있다.

 

그리고 앞서 말했듯 JavaScript 엔진 자체에는 태스크 큐와 이벤트 루프라는 개념도 존재한다. 이에 대해서는 뒤에서 설명한다.

 

JavaScript 엔진의 메모리 모델은 크게 두 가지 영역을 정의한다. 하나는 힙(Heap) 영역으로, 동적으로 생성되는 객체들을 할당하는 곳이며 구조화되지 않은 넓은 메모리 영역을 지칭한다. 나머지 하나는 스택(Stack) 영역으로, 함수가 실행될 때마다 해당 스택 프레임이 푸시되고 함수가 종료될 때마다 해당 스택 프레임이 팝 되는 영역을 지칭한다. 멀티 프로세싱 환경의 경우 각 프로세스가 독자적인 스택 영역과 힙 영역을 가지지만, 멀티 쓰레딩 환경의 경우 각 쓰레드가 독자적인 스택 영역을 갖고 힙 영역은 함께 공유한다.

 

* 스택 영역에 푸시되는 함수의 스택 프레임은 곧 그 함수의 실행 맥락(Execution Context)을 의미한다. 실행 맥락이란 해당 함수를 실행하기 위해 필요한 각종 정보들(ex. 지역 변수)의 집합이라고 생각하면 된다. JavaScript 엔진은 JavaScript 코드의 실행을 시작함과 동시에 전역 실행 맥락(Global Execution Context)을 스택 영역에 푸시하고, 이후 특정 함수의 호출 문을 만나는 순간 그 함수의 실행 맥락을 스택 영역에 푸시하고 그 함수가 종료될 때 그 실행 맥락을 팝 한다. 여기서 주목할 만한 사실은 JavaScript 코드의 실행이 전부 다 끝나기 전까지는 전역 실행 맥락이 스택 영역에서 팝 되지 않고 남아 있다는 사실이다. 이를 기억하고 있어야 뒤에서 설명할 "스택이 비는 순간"이라는 말의 의미를 조금 더 명확히 이해할 수 있다. 즉 스택이 비는 순간이라는 것은 결국 더 이상 실행할 JavaScript 코드가 없어서 전역 실행 맥락까지 스택 영역에서 팝이 된 경우를 말한다.

 

3. 웹 API (Web API)

Ajax 요청, setTimeout(), 이벤트 핸들러의 등록과 같이 웹 브라우저에서 제공하는 기능들을 말한다. 그런데 중요한 것은 이러한 요청들의 처리가 JavaScript 엔진의 쓰레드와는 다른 쓰레드들에서 이뤄진다는 점이다. JavaScript 엔진의 스택에서 실행된 비동기 함수가 요청하는 비동기 작업에 대한 정보와 콜백 함수를 웹 API를 통해 브라우저에게 넘기면, 브라우저는 이러한 요청들을 별도의 쓰레드에 위임하게 되는 것이다. 그러면 그 쓰레드는 해당 요청이 완료되는 순간 전달받았던 콜백 함수를 JavaScript 엔진의 태스크 큐라는 곳에 집어넣는다. (태스크 큐에 전달된 콜백 함수가 처리되는 방식은 바로 뒤이어서 설명한다.)

 

예를 들어, setTimeout() 함수가 실행되면 JavaScript 엔진은 웹 API를 통해 브라우저에게 setTimeout() 작업을 요청하면서 콜백 함수를 전달하고, 브라우저는 이러한 타이머 작업을 별도의 쓰레드에게 위임한다. 그러고 나면 JavaScript 엔진의 스택에서는 setTimeout() 함수의 스택 프레임이 즉시 팝 된다. 그리고 인자로 명시한 시간이 흐르고 나면 해당 타이머 작업을 처리하고 있던 쓰레드는 전달받았던 콜백 함수를 JavaScript 엔진의 태스크 큐에 집어넣게 된다.

 

4. 태스크 큐 (Task Queue)

태스크 큐는 웹 API를 처리하고 있던 쓰레드로부터 전달받은 콜백 함수들을 FIFO(First-In First-Out) 구조로 저장하고 있는 일종의 큐(Queue)로, JavaScript 엔진 자체에 포함되어 있는 부분이다. 여기서 말하는 태스크란 곧 콜백 함수를 의미하기 때문에 콜백 큐(Callback Queue)라고 부르기도 한다. 여기에 저장된 콜백 함수들은 스택이 비는 순간 스택에 순서대로 푸시된다. 이러한 원리로 비동기 작업이 완료된 이후 콜백 함수가 실행되는 것이다. 그런데 비동기 작업이 완료되어 태스크 큐에 콜백 함수가 들어가 있더라도 스택이 비어있지 않다면 해당 콜백 함수가 바로 실행되지 못한다는 특징이 있다. 그래서 setTimeout() 함수도 인자로 명시한 시간은 '잠드는 최소 시간'일 뿐, 그것보다 더 오래 잠들 수 있다.

 

5. 이벤트 루프 (Event Loop)

한편, 위와 같은 동작이 가능하려면 매 순간 스택이 비어있는지 여부와 태스크 큐에 콜백 함수가 기다리고 있는지 여부를 확인해야만 한다. 이러한 역할을 수행하는 것이 바로 이벤트 루프이다. 이벤트 루프는 JavaScript 엔진 자체에 포함되어 있는 부분이다. 즉, 이벤트 루프는 매 순간 스택이 비어있는지 확인을 해서 스택이 비어있다면 태스크 큐에 콜백 함수가 들어올 때까지 기다렸다가 첫 번째로 들어오는 콜백 함수를 스택에 쌓는 역할을 한다. 만약 스택이 비어있지 않다면 일반적인 방식으로 스택에서 함수의 호출을 처리하게 된다. 결국 이벤트 루프는 이름 그대로 같은 작업을 무한히 반복하는 무한 루프라고 생각하면 이해하기 쉬울 것이다. 참고로 이렇게 매 순간 태스크 큐와 스택을 확인하는 작업을 틱(Tick)이라고 부른다. 이벤트 루프의 동작 방식을 가상의 코드로 설명한다면 다음과 같다고 한다(출처 : MDN).

while (queue.waitForMessage()) {
    queue.processNextMessage();
}
The event loop facilitates this process; it constantly checks whether or not the call stack is empty. If it is empty, new functions are added from the event queue. If it is not, then the current function call is processed. (발췌)

List of Articles
번호 제목 날짜 조회 수
» 비동기 작업의 원리 (JavaScript 엔진, Web API, Task Queue, Event Loop) file 2023.01.20 138
246 Node.js와 npm(+ npx)의 개념 2023.01.20 134
245 바닐라 JS, ECMAScript 개념 file 2023.01.20 109
244 마우스 오른쪽버튼 , 드래그 선택 차단 2023.01.12 118
243 개발자도구 F12키 막기 file 2023.01.12 167
242 패스워드, 확인패스워드가 맞는지 체크 2023.01.12 129
241 문자열 안에 쌍따옴표, 홑따옴표 표시하는 3가지 방법 file 2021.08.20 1768
240 대소문자 변경 (대문자를 소문자로, 소문자를 대문자로) 2021.08.20 259
239 시간 계산하기 (시/분/초/ 더하기, 빼기) 2021.08.20 1699
238 날짜 계산 (Date) 마지막 날짜(말일)/윤달/윤년 처리하기 2021.08.20 1518
237 월의 마지막 날짜 계산하기 2021.08.20 242
236 Date 객체로 원하는 날짜, 시간 표현하기 2021.08.20 221
235 날짜 계산하기 (년/월/일 더하기, 빼기) 2021.08.20 2351
234 3자리 마다 쉼표만 찍어주는 number_format 함수 2021.03.26 224
233 팝업창 맨위로 올라오게 하기 2021.03.26 848
232 폼안에 태그명, 함수명 같을때 오류 2021.03.26 214
231 ajax 사용시 Internal Sever Error 뜨는 경우 2021.03.26 953
230 자주쓰는 것들 2021.03.26 203
229 utf-8일때 alert 한글 깨짐 해결 2021.03.26 3525
228 split, join, replace, replace_all 2021.03.26 204
Board Pagination Prev 1 2 3 4 5 6 7 8 9 10 ... 13 Next
/ 13

하단 정보를 입력할 수 있습니다

© k2s0o1d4e0s2i1g5n. All Rights Reserved