oris9

JavaScript의 동작원리에 대해 본문

JavaScript

JavaScript의 동작원리에 대해

oris9 2024. 1. 12. 21:41

 

 

JavaScript의 동작원리에 대해 (콜스택, 큐, 이벤트루프)

ES6 기준으로 작성되었습니다

 

내부에서 자바스크립트가 동작하는 구조(자바스크립트의 런타임모델)

 

*Heap : 변수나 객체 같은 참조 타입들이 이곳에 할당됨

 

JavaScript는 콜스택방식으로 코드를 처리하고, 한 번에 코드를 한 줄만 수행할 수 있습니다.(=싱글스레드처리방식)

 

콜스택(Call Stack) 방식이란,

호출된(call)된 코드들이 push되어 콜스택에 쌓이면서(stack) 코드 처리가 진행됩니다. 이때, 코드는 팬케이크처럼 아래에서 위로 쌓이게 됩니다. 이렇게 콜스택에 쌓인 코드는 위에서부터 처리되며, 값이 반환되면(실행 완료되면) 빠져나갈 수(pop)될 수 있게 됩니다. (나중에 들어온 코드가 먼저 나갈 수 있는 구조임, 선입후출)

 

만약 함수1이 매개변수로 다른 함수(함수2)에서 반환된 값이 필요하다면

함수1이 실행될때 함수1은 콜스택에 쌓이고 함수 처리에 필요한 값을 받기 위해서 함수2가 콜스택에 또 쌓이게 됩니다. 함수2에서 값이 반환되면, 함수2는 콜스택에서 빠지면서 함수1에 값을 전달해줍니다. 그때까지 다음 줄의 코드는 실행되지 않습니다. (콜스택에 계속 함수1이 쌓여있었기 때문에)

 

그런데 코드가 만약 시간이 걸리는 작업(setTimeout, 비동기함수 등)이라면

그 일을 브라우저(Web API)한테 넘기고 콜스택에서 제거되면서 다음 줄의 코드를 실행하게됩니다. 그 시간이 걸리는 작업은 콜백큐(Callback Queue)에 들어가게 됩니다. 브라우저는 작업이 끝나면 자바스크립트가 곧바로 그 값을 받아 수행합니다. 

 

실행컨텍스트(Execution Context)

이때 사실 콜스택에 쌓이는 하나하나들은 실행컨텍스트(Execution Context)입니다.

실행컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아 놓은 객체를 의미합니다.

이 객체는 변수, 함수, this, arguments 등에 대한 정보들로 이루어져있습니다.

 

이 Execution Context라는 코드 블록은 딱 3가지 경우에만 생성됩니다

1. 맨 처음 코드가 실행되었을 때 (Global Execution Context).
2. 함수가 호출되었을 때.
3. eval()이 사용되었을 때. (eval() 사용의 경우는 eval() 자체를 XSS(Cross Site Script) 공격에 사용되는 등 악의적인 공격에 사용될 수 있다 보니 보안상 문제 때문에 사실상 안 쓰는 케이스) - 임우찬님 블로그

 

 

-코드가 아니라 객체다. 

코드가 실행되는 과정을 살펴보면, 가장 먼저 전역 컨텍스트가 콜스택에 담기게 됩니다.

이말인 즉슨, global(웹에서는 window)객체가 콜스택 가장 아래 쪽에 담기게 되면서 코드가 실행된다는 말입니다.

다들 아시다시피 코드를 실행할 때는 그 전역환경이 존재하고 최상위 전역환경을 확인해보면 객체로서 존재함을 확인할 수 있습니다.

그 객체안에서 Math.floor와 같은 각종 메서드들을 불러오고 실행할 수 있게 되는 것이죠.

이렇게 외부환경 정보를 구성하게 되면, 그 다음에 작성된 코드를 하나씩 읽어가기 시작합니다.

단순한 함수이면, 그 코드 한줄만으로도 실행이 끝나겠지만(이때도 전역 객체는 필요할 경우가 대부분)

대부분의 함수들은 추가적인 코드실행을 필요로 하게 됩니다.

이런 함수가 실행되면, 이 특정함수실행컨텍스트를 생성해서 콜스택에 담은 후, 바깥의 코드를 읽는 걸 중지하고 함수 내부의 코드(혹은 코드실행에 필요로 하는 값을 찾기위해)를 실행하기 시작합니다. 또한 전역에서 선언한 변수나 함수등 여러 값들이 필요할 수 있습니다.

이렇게 코드 한 줄이 실행될 때는 여러 값들이 필요하고 컴퓨터는 이를 일종의 하나의 정보저장공간인 실행컨텍스트 객체를 만들어 동작하는 것입니다.

 

 

-실행컨텍스트에 담기는 환경정보는 위의 그림과 같이 3가지로 나눠서 볼 수 있습니다.

 

1. Lexical Environment

  1-1. Environment Records : 기본적으로 변수, 함수 이름과 관련된 값들을 추적한다.(이로 인해 호이스팅이 발생함)

더보기

호이스팅

 

호이스팅은 코드 해석을 좀 더 수월하게 하기 위해 environmentRecord의 수집 과정을 추상화한 개념으로, 실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 '끌어올린다'고 해석하는 것입니다. 

코드 작성시 변수는 const let var 등의 키워드로 [선언]되고 정보가 [할당] 될 수 있습니다.

 

이때 const let var 키워드에 대해 살펴보도록 하겠습니다.

우선 이런 변수생성코드를 작성하면, 

컴퓨터에서는 변수의 생성에 대한 코드의 실행과는 상관없이 우선적으로 코드 최상단으로 정보들을 끌어올려(hoisting)해와 변수생성과정을 거치게됩니다.

 

1. Declaration phase(선언 단계) : 변수를 Execution Context의 Lexical Environment에 등록.
2. Initialization phase(초기화 단계) : Lexical Environment에 등록되어 있는 변수를 위하여 메모리를 할당하는 단계. 여기서 변수는 undefined로 초기화된다.
3. Assignment phase(할당 단계) : 변수에 실제로 값이 할당되는 단계이다. (undefined → 특정한 값)

 

var의 경우 1번과 2번이 동시에 진행되는데 let, const는 1번만 먼저 진행되게됩니다. 

 

따라서 let, const는 초기화와 할당이 나중에 이루어지고 선언단계와 초기화단계 사이인, TDZ(Temporal Dead Zone)에 머물러 있기 때문에 초기화 코드(=작성한 변수생성코드)를 만나기 전까지는 그 변수를 찾게되면 레퍼런스 에러가 일어나게 되게 되는 것입니다. 

스코프 또한 var의 경우는 functional scope, let과 const는 block scope이기때문에 스코프의 차이도 존재하죠 

(값이 할당되기 전까지는 TDZ에 의해 가려져있다고 생각!)

 

ES6의 class선택자나, class 선택자 안에서 쓰는 constructor()안의 super()같은 경우 또한 TDZ의 영향을 받는다.

 

 

    1-1-1. Declarative Environment Records (선언적 환경 레코드) : 함수, 변수, this 등 식별자 바인딩이 저장됨(대부분의 경우가 이에 해당)

    1-1-2. Object Environment Records (개체 환경 레코드) : with문 같은 코드를 쓸 때, 일부 경우

  1-2. Outer Reference Environment (scope) : 외부 스코프의 주소참고, 상위스코프의 렉시컬환경을 가리킵니다. (이로 인해 scope와 scope chain이 형성됨), 단방향 링크드 리스트로 하위 스코프에서 상위스코프를 참조하는 것만 가능합니다.

(이렇게 Lexical EnvironmentOuter Reference Environment 가 상위 Lexical Environment와 연결되어 있는 것을 ES5버전  스코프는 변수의 유효범위를 말한다. outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironment를 참조한다. 코드 상에서 어떤 변수에 접근하려고 하면 현재 컨텍스트의 LexicalEnvironment를 탐색해서 발견되면 그 값을 반환하고, 발견하지 못할 경우 다시 outerEnvironmentRecord에 담긴 LexicalEnvironment를 탐색하는 과정을 거친다. 전역 컨텍스트의 LexicalEnvironment까지 탐색해도 해당 변수를 찾지 못하면 undefined를 반환한다.이후의 Execution Context에서는 "Lexical Nesting Structure"라고 한다.)

더보기

 

스코프는 변수의 유효범위를 말한다. outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironment를 참조한다. 코드 상에서 어떤 변수에 접근하려고 하면 현재 컨텍스트의 LexicalEnvironment를 탐색해서 발견되면 그 값을 반환하고, 발견하지 못할 경우 다시 outerEnvironmentRecord에 담긴 LexicalEnvironment를 탐색하는 과정을 거친다. 전역 컨텍스트의 LexicalEnvironment까지 탐색해도 해당 변수를 찾지 못하면 undefined를 반환한다.

 

2. Variable Environment : 담기는 정보는 렉시컬 환경과 동일하지만, 최초 실행시 스냅샷을 찍어 그 환경을 유지해 저장하고 있다. 스냅샷을 먼저 찍어 이에 담은 다음 여기에 담긴 정보를 복사해서 렉시컬 환경을 만들게 된다. 스냅샷 유지를 목적으로 사용한다.

3. thisBinding : this로 지정된 객체가 저장

 

 

 

 

 

여기서 Callback Queue란,

런타임에서 작업을 순차적으로 처리하기 위한 대기실입니다. Queue는 선입선출(First-In-First-Out)의 선형자료구조를 뜻합니다.

 

큐는 내부적으로 Task Queue, Microtask Queue로 나눌 수 있습니다. 여기서 주의할 점이 큐는 선입선출구조이지만, Task Queue 보다 Microtask Queue의 task가 먼저 out되게 됩니다.

 

API의 종류에 따라 어느 큐에 들어가는지 나뉘게 됩니다.

Task Queue로 콜백을 보내주는 대표적인 API들 : setTimeout, setInterval, setImmediate, requestAnimationFrame 등
Microtask Queue로 콜백을 보내는 대표적인 API들: Promise, process.nextTick, Object.observe, MutationObserver

 

 

이벤트루프(Event Loop)란?

Event loop은 Queue를 주시하고 있습니다.

call stack 안에 task가 모두 실행되어 비어있을 때 Queue에 대기하고 있던 함수들이 순차적으로 push 합니다.

 

//노드 배운다음에 다시작성하자 

이벤트 루프는 Node.js에서 이벤트 기반(non-blocking) I/O 모델을 구현하기 위한 핵심 개념 중 하나입니다.Node.js는 이벤트 기반으로 비동기 처리를 하고,이벤트 루프는 이벤트 큐에서 이벤트를 가져와 처리하는 역할을 담당합니다.

이벤트 루프는 노드의 내부적인 동작 원리 중 하나로, Call Stack, Task Queue, Event Queue 3가지 개념으로 구성됩니다.

Call Stack은 코드 실행 중 현재 실행중인 함수를 기록하는 스택 자료구조입니다.Task Queue는 비동기 처리가 끝난 후 실행할 콜백 함수를 보관하는 큐 자료구조입니다. Event Queue는 이벤트 발생 시 처리할 콜백 함수를 보관하는 큐 자료구조입니다.

이벤트 루프의 동작 방식은 다음과 같습니다.

Call Stack이 비어있으면, Event Queue에서 이벤트를 가져와 Call Stack에 넣습니다.Call Stack에서 이벤트를 처리합니다.비동기 처리 함수(setTimeout, setInterval 등)가 호출되면, Call Stack에서 해당 함수를 제거하고, Task Queue에 콜백 함수를 보냅니다.Call Stack이 다시 비어있으면, Task Queue에서 콜백 함수를 가져와 Call Stack에 넣습니다.Call Stack에서 콜백 함수를 처리합니다.이렇게 Event Queue와 Task Queue를 활용하여 비동기 처리와 이벤트 처리를 구현합니다.

 

 

 

 

이렇게 콜스택방식으로 함수를 처리하기 때문에 동기함수인 자바스크립트가 비동기적으로 작동할 수 있게 됩니다.

 

 

 

const first = () => {
	second()
	Promise.resolve().then(() => console.log("first"))
//Promise.resolve()는 즉시 이행된 프로미스를 반환하므로, .then 콜백은 다음 마이크로태스크 큐로 이동합니다.
//마이크로태스크 큐의 작업은 콜스택이 비어있을 때 실행되므로, console.log("first")가 출력됩니다.
}
const second = () => {
	third()
	setTimeout(() => console.log("second"), 0)
}
const third = () => {
	console.log("third")
}

first()				// third 다음 first 다음 second 출력됨

 

 

 

 

 

 

 

참고하면 좋은 사이트) 콜스택 시각적으로 보여주는 사이트. http://latentflip.com/loupe

 

http://latentflip.com/loupe/

 

latentflip.com

 

 

이미지,참고 출처:

https://m.blog.naver.com/dlaxodud2388/222655214381

https://heycoding.tistory.com/86

https://www.codetetris.com/posts/how-to-work-js/