EtoC

자바스크립트 내부 동작 과정 본문

Language/JavaScript

자바스크립트 내부 동작 과정

게리드 2024. 1. 4. 07:30

첫 면접을 봤는데 2차부터 4차 면접까지 꾸준하게 받은 질문이 eventloop와 garbage collection이였다.

책에서 스윽 보고지나가면서 대충 이런거구나하고 넘겼는데 계속 질문이 들어오는게  많이 부족하게 대답한 듯..🥲
그래서 다시 책을 읽으면서 자세히 정리해보았다.

0.   비동기처리의 환경구조와  용어정리

  •  자바스크립트 엔진(런타임)은 memory heap과 call stack으로 이루어져있다.
  •  heap은 구조화되지않는 큰 메모리 영역으로 말한다.  객체는 힙에 할당된다.
  • memory heap은 자바스크립트의 객체(함수,변수 등)에 메모리 할당이 일어나는곳으로 크기가 변하는 값의 참조값을 가지고 있다.
  • wep API는 브라우저에서 구현된 API로, node에서는 background라고 하며, 비동기 처리를 담당한다.
  • call stack은 호출스택으로 실행할 코드를 한줄 단위로 할당되어 실행되는 자료구조이다.
  • callback queue는 비동기처리가 끝나고 실행되어야할 콜백 함수가 차례로 할당된다.
  • event loop는 callstack이 비어있는지 확인하고, queue에 할당된 함수를 순서에 맞춰 callstack에 할당해준다.
    이벤트루프는 하나 이상의 테스크 큐를 가지며, 테스크 큐에서 실행가능한 첫 번째 태스크를 가지고 온다.

 자바스크립트이 런타임모델은 코드의 실행과 이벤트 수집 및 처리, 큐에 대기중인 하위작업을 처리하는 이벤트루프에 기반한다.

이는 비동기처리를 위해 별도의 라이브러리가 필요한 C언어나 Java가 가진 모델과는 다르다.

Evevnt Loop

Event Loop는 JavaScript의 런타임 환경, 즉 브라우저의 JavaScript 엔진 또는 Node.js와 같은 환경에서 발생하는 개념으로,

이벤트기반 모델에서 여러 이벤트가 동시에 발생했을때 어떤 순서로 콜백 함수를 호출할지를 판단하는 역할을 한다.
노드가 종료될때까지 이벤트 처리를 위한 작업을 반복하기때문에 loop(루프)라고 한다.

 

브라우저에서의 Event Loop
브라우저 환경에서 JavaScript 코드가 실행될 때, 이벤트 루프가 발생한다.
이벤트루프는 웹 페이지에서 발생하는 이벤트들을 처리하고, 콜백 함수들을 실행하는 역할을 한다.
브라우저에서는 사용자의 상호작용, 타이머, XMLHttpRequest 등 다양한 이벤트들이 발생하며,
이러한 이벤트들이 이벤트 큐에 쌓이고 이벤트 루프를 통해 순차적으로 실행된다.

Node.js에서의 Event Loop
Node.js는 JavaScript를 서버 환경에서 실행하기 위한 런타임으로,
Node.js 역시 이벤트루프를 통해 비동기적인 작업들을 처리한다.
파일 시스템 접근, 네트워크 요청, 데이터베이스 쿼리 등이 비동기적으로 이벤트 루프를 통해 처리된다.

 

 


1. Nodejs에서의 이벤트 처리 메커니즘

 

nodejs는 이벤트기반으로 발생하는데, 이벤트기반 시스템에서는 특정 이벤트가 발생할때 어떤 작업을 할지 미리 등록해두어야한다.
이걸 이벤트 리스너에 콜백함수를 등록한다고 표현한다.
그리고 이벤트가 발생하면 이벤트리스너에 등록해둔 콜백함수를 호출한다.
발생한 이벤트가 없거나 발생했던 이벤트를 다 처리하면 노드는 다음 이벤트가 발생할때까지 대기한다.


2. 동기적인 코드 처리

노드는 자바스크립트의 코드를 맨 위에서부터 한줄씩 실행하다가 함수 호출부분을 발견하면 호출한 함수를 call stack(호출 스택)에 넣는다고 했다.

그럼 아래와 같은 코드가 있다고할때 실행결과는 어떻게 출력될까?

function first() {
  second();
  console.log('1')
}

function second(){
  third();
  console.log('2')
}

function third(){
  console.log('3')
}
first()

출력결과는 왼쪽과 같다.

자바스크립트 코드를 위에서부터 읽어서 만난 first함수가 제일 먼저 호출되고,

first 함수 내부의 second가 호출되고, second내부의 third가 호출된다.

run js라는 프로그램으로 돌려보면 1,2,3이 출력되는데 왜 node는 3,2,1 순으로 출력될까? (undefined는 반환하는 값이 없어서이다.)

이유는 노드의 call stack(호출 스택)은 호출된 순서와 반대로 실행이 완료되기때문이다.

 

콜스택의 실행 구조를보면 아래와 같다.


1 ) 노드의 Call Stack(호출 스택)

📌 stack(쌓아 올린다)
- stack은 순서가 보존되는 선형 자료구조의 일종으로, LIFO(Last In First Out) 메커니즘을 갖고 있다.
데이터를 받은 순서대로 정렬하며, 가장 마지막으로 입력된 것부터 순차적으로 가져온다.
- 이유는 스택이 top을 통해서만 접근할 수 있기때문에 가장 마지막에 들어온 자료가 가장 상단에 있기에 마지막에 들어온 자료부터 top을 통해 삭제된다.
- 스택의 사용 예시로는 웹 브라우저의 방문기록(뒤로가기), 실행 취소(undo) 등이 있다.

 

call stack은 js의 인터프리터가 읽은 함수를 한줄씩 쌓아 올린뒤 위에서부터 실행한다.

함수가 실행되는 동안에는 콜 스택에 머물러있다가 실행이 완료되면 호출 스택에서 지워진다.

 

anonymous 함수는 처음 실행시의 전역 컨텍스(global context)이다.
컨텍스트는 함수가 호출되었을때 생성되는 환경으로, 자바스크립트 코드는 실행시 기본적으로 전역 컨텍스트 안에 들어간다.

 

 

내부 동작 과정
third()→ console.log(3) → third가 콜스택에서 나감 → second() → console.log(2) → second 나감 → first() →
console.log(1) → first 나감 →anonymous 순으로 실행되고, anonymous까지 실행이 완료되면 호출스택은 비어있게된다.

 

그래서 출력된 결과는 3 → 2 → 1 이 된다.

Uncaught RangeError: Maximum call stack size exceeded
Call Stack을 사용하다보면 만날 수 있는 에러이다.
callstack마다 담을수있는 한계가 존재하는데 이한계를 넘어가면 위의 에러를  띄우고 종료하게된다. 
보통 1만개까지 담을 수 있고 크롬의경우 12만개정도 담을수있다고한다.

3.  비동기적인 코드 처리

그럼 자바스크립트에서 비동기 작업이나 setTimeout같이 백그라운드로 보내지는 이벤트가 발생하면 어떻게될까?

setTimeout은 비동기코드이기때문에 처리과정이 조금 더 복잡해진다.

비동기 코드 처리가 어떻게 일어나는지 알기위해서는 아래의 개념들을 알아야한다.

Event Loop
이벤트 발생시 호출할 콜백함수들을 관리하고, 호출된 콜백함수의 실행 순서를 결정한다.
call stack이 비어있는지 항시 확인하고 비어있을경우, 태스크 큐에 있는 콜백 함수를 처리한다.
Queue
순서가 보존되는 선형 자료구조의 일종으로, FIFO(First In First Out) 메커니즘을 갖고 있다.
데이터를 받는 순서대로 정렬하며, stack과는 다르게 가장 먼저 입력된 것을 순차적으로 가져온다.
큐의 경우 삽입 연산이 수행되는 리어(rear)삭제연산이 수행되는 front를 통해 양방향에서 삽입과 삭제 작업이 이루어진다.
rear에서 이루어지는 삽입연산을 Enqueue(인큐), front에서 이루어지는 삭제연산을 Dequeue(디큐)라고 한다.
큐의 사용 예시로는 프로세스 관리 ,프린터의 인쇄 대기, 서비스센터 고객 대기 시간 등이 있다.
테스크 큐(Callback Queue, Event Queue)
이벤트 발생 후, 백그라운드에서 테스크 큐로 비동기함수나 이벤트 리스너의 콜백 함수를 받는다.
콜백들은 대개 완료된순서대로 줄을 서있지만 특정한 경우에는 순서가 바뀌기도한다.
백그라운드(Web APIs)
setTimeout같은 비동기함수나 이벤트 리스너들이 대기하는곳으로 자바스크립트가 아닌 다른언어로 작성된 프로그램이다.
주로 비동기작업들을 처리하며 여러작업이 동시에 실행될 수 있다.

 

이번 코드의 실행결과는 어떻게 될까?

function run() {
  console.log('3초 후 실행');
}

 console.log('시작');
  setTimeout(run, 3000);
  console.log('끝');

 

결과를 알기위해서는 run이 호출스택에 언제들어가는지를 알아야한다.

실행 결과는 이미지와 같다. 

 

출력 결과: 시작 → 끝 → 3초 후 실행

내부 동작 과정
1.  콜스택에 anonymous 가 먼저 들어가고 console.log('시작') 이 들어가고 실행되어 출력된다.
2.  setTimeout이 콜 스택에 들어간다. setTimeout은 비동기 함수이기때문에 실행되면 백그라운드로 이동한다.
3. 백그라운드에서 3초가 지나기전에 콜스텍에 console.log('끝')이 들어오고 실행되어 끝이 출력된다.
4.  anonymous가 실행된다.
5. 백그라운드에서 3초가 지나면 백그라운드에있던 콜백함수를 콜백큐로 넘겨준다.
6.  event loop는 콜스텍이 비어있는지 확인하고 비어있으면 콜백큐의 콜백함수를 콜스텍에 넘겨준다.
7. 콜스텍에 들어간 콜백함수가 실행되고 3초후 실행을 출력한뒤 콜스텍에서 빠져나간다.

만약 setTimeout이 0초이더라도 로직자체는 같기때문에 출력결과는 시작 → 끝 → 3초 후 실행으로 같다.

3) Promise

⛔️ Promise는 비동기가아니라 동기이다. 그런데 .then을 만나면 비동기로 동작한다.

console.log("시작");

setTimeout(function() {
    console.log("타이머 실행");
}, 0);

Promise.resolve().then(function() {
    console.log("프로미스 실행");
});

console.log("끝");

 

실행결과는 시작 → 끝 → 프로미스 실행 → 타이머 실행 순으로 출력된다.

내부 동작 과정
1. anonymous가 콜스택에 들어오고 console.log('시작')이 들어온뒤 실행되어'시작'이 출력된다.
2. setTimeout이 콜스텍에 들어오고 백그라운드로 이동한다.
3. Promise가 콜스텍에 들어오고 .then이 있어 비동기로 인식하여 백그라운드로 이동한다.
4. console.log('끝')이 콜스텍에 들어오고 실행되어 '끝'이 출력된다.
❗️timeout이 먼저 완료되고 promise가 완료되었을때 무엇이 먼저 콜백으로 실행될까?
5. 콜백큐에 timeout 콜백이 들어오고  then 콜백이 들어온다.
6. anonymous가 나가고 이벤트 루프는 콜스텍이 비었는지 확인하고 콜백큐에서 하나씩 꺼내 콜스택으로 옮긴다.
7. 콜백 then이 콜스택에 들어가고 console.log('프로미스 실행')을 실행하고 출력한뒤 빠져나온다.
8. 콜스택이 비고 이벤트루프가 timeout 콜백을 콜스택에 삽입하고 실행하고 출력한뒤 빠져나온다.

 

queue이기때문에 선입선출로 timeout이 먼저 콜스택에들어갈거 같지만 then이 우선순위가 더 높아서 then이 먼저 들어간다.

콜백큐의 구조와 우선순위는 아래와 같다.


2) event loop의 callback Queue 처리의 우선순위  

콜백큐 안에는 Task Queuemicrotask Queue, animation frames가 있다.

이벤트루프가 콜백큐에서 먼저 탐색하는 우선순위Microtask Queue > Animation Frames > Task Que 순이다.

promise는 microtask queue에 담긴다. 

그래서 promise가 먼저 실행되었고 마이크로테스크 큐가 비게되니 다음으로 테스크 큐에있는 timeout을 콜스택으로 보내준것이다. 

정리하는데 콜백큐의 용어가 너무 다양한거같다 

면접볼때 뭐라고 말해야하는지 빙글빙글~😵‍💫

 

📝  콜백큐 추가정리

  1. Callback Queue는 Task Queue라고 많이 불린다
    하지만 callback queue안에 task queue가 포함되어있는 형태이다.
  2. Task Queue는 MacroTask Queue라고도한다.
Task Queue(MacroTask Queue, Event Queue)
태스크큐에는 일반적인 작업들, setTimeout, setInterval같은 비동기적인 작업들이 들어간다.

MicroTask Queue
마이크로 테스크큐에는 주로 Promise와 관련된 작업들이 들어간다.
Promise의 then이나 catch같은 콜백들이 여기에 속한다.

차이점
MicroTask실행 중에 새로운 마이크로태스크를 큐에 추가할 수 있으며, 이 작업은 큐가 비어질 때까지 반복한다.
반면에 MacroTask는 이벤트 루프가 실행되는 동안 추가된 매크로태스크는 다음 이벤트 루프에서 실행되어야한다.
즉, MacroTask는 현재 이벤트 루프에서 실행이 시작되었더라도 해당 이벤트 루프에서 큐에 추가된 매크로태스크는 그 이후의 이벤트 루프에서 실행된다.

책읽고 다른 자료들도 찾아보면서 정리하다보니 너무 재밌어서 벌써 잘시간이다.

글이 너무 길어졌지만 동작원리를 보니 신기하고 재밌다

지금생각하면 이걸 대충보고 자바스크립트 개발자입니다하며 면접을 봤으니..부끄럽다.