[JavaScript/Node.js] 자바스크립트 작동 원리

JuseungL·2024년 1월 9일
0

Node.js

목록 보기
1/8
post-thumbnail

알아보게 된 계기

2023년 창업 동아리에 참여하게 되었고 어플 개발에서 백엔드 개발을 맡은 나는 처음으로 해보는 백엔드이기에 어떤 프레임워크를 사용할지 고민을 많이 했었다. 그 당시 지금보다 더 아는게 없었기에 스프링 부트는 많이 어렵다하고 구글 레퍼런스도 어느 정도 있고 스프링 부트 보다는 쉽다고 알려진 Node.js의 Express를 사용해보기로 했다. 개발을 하는 과정에서 비동기 관련 키워드를 사용할때마다 의문이 들었고 알아보니 자바스크립트 언어 자체는 싱글 스레드(Single Thread)라고 한다. 그리고 거기에 항상 따라 나오는 단어들인 논 블로킹(Non-Blocking) 그리고 비동기(Asynchronous).
도대체 싱글 스레드인데 어떻게 비동기, 논 블로킹이 존재할 수 있는지 이해가 되지 않았다. 그래서 이번 기회에 JavaScript부터 시작해서 Node.js의 작동 원리에 대해 깊이 있게 이해해 보고자 공부하게 되었다.


JavaScript 작동 원리

아래 그림은 자바스크립트 작동원리하면 흔하게 볼 수 있는 사진이 있다.
그림을 보면 회색 테두리 안에 "JS"가 적힌 노란색 네모 박스, 메모리 힙(Memory Heap) 그리고 콜 스택(Call Stack)이 보인다.

  • 자바스크립트 엔진(JavaScript Engine)
    노란색 네모 박스 부분이 자바스크립트 엔진을 나타낸다.
    우리가자바스크립트를 쓸때 .js 파일을 쓰게되면 퓨터는 실제로 알아들을 수 없다. 이때 자바스크립트 엔진이 자바스크립트 코드를 해석(인터프리팅)하여 기계어로 변환하고 실행시켜주는 역할을 한다. 이때, 실행 환경에 따라 해당 엔진의 종류는 달라지는데 대표적으로 크롬이나 Node.js는 V8 엔진을 사용한다.
    크롬이나 Node.js와 같이 자바스크립트 코드가 실행되는 환경을 런타임이라고 한다.

  • 메모리 힙(Memory Heap)
    동적으로 할당된 메모리가 저장되는 곳으로 객체, 배열 등의 데이터 구조는 메모리 힙에 할당된다. 메모리 힙은 크기가 동적으로 변하며, 수동으로 해제되지 않는 한 계속 사용 가능한 상태로 남아있다. JavaScript에서 가비지 컬렉션은 더 이상 필요하지 않은 메모리를 자동으로 해제하여 메모리 누수를 방지한다.

  • 콜 스택(Call Stack)
    함수 호출의 실행 컨텍스트를 기록하는 곳으로, 현재 실행 중인 함수의 위치 및 변수 등을 추적하는 Data Structure이다. 즉, Call을 쌓는 곳으로 호출된 Function을 해당 Stack에 쌓게 된다.
    콜 스택은 후입선출(Last-In-First-Out, LIFO) 구조를 가지며, 함수가 호출되면 해당 함수의 실행 컨텍스트가 스택의 맨 위에 푸시(push)되고, 함수가 반환될 때 팝(pop)됩니다. 이를 통해 현재 실행 중인 함수가 어디에 있는지 추적할 수 있다.

간단하게 논블로킹에 대해서 얘기해보자면 여기서 각각의 작업을 하나의 Function이라고 했을 때 논블로킹일 경우 함수 하나가 실행되는 동안 분명 어딘가에서 또 다른 함수가 동시에 실행 중인 것을 알 수 있다. 그 다른 곳이 Web APIs이며 실행되고 난 콜백을 콜백 큐(Callback Queue)에서 받고 이벤트 루프(Event Loop)가 콜 스택으로 던져 주는 것이다.

콜백(Callback)이란?
Function을 인자로 넘겨줄때 그때 그 Function을 콜백이라고 한다. 대게는 특정 작업이나 이벤트가 완료된 후 나중에 실행되도록한다. 즉, 비동기 프로그래밍에서 사용되며 특정 작업이 완료된 후 어떤 일이 발생해야 하는지 지정할 수 있다.

지금부터 싱글 쓰레드이면서 어떻게 저런 논 블로킹, 비동기 방식을 가능하도록 했는지에 대해서 이해해보자

console.log('Start');
setTimeout(function timer1() {
  console.lg('callback');
}, 5000);
console.log('End');

간단한 코드로 확인해보면 해당 코드의 결과는
Start -> callback -> End가 아닌 Start -> End -> callback의 순서로 출력된다.

위의 코드를 차근차근 뜯어 보면
1. console.log('Start');가 처음에 콜 스택에 쌓인다. 이때 아직 해당 console.log()가 실행된 것은 아니고 단지 호출된 후 콜 스택에 쌓여있는것 뿐이다.
2. 콜 스택에 쌓인 후 console.log('Start');가 실행되면 그때서야 브라우저 콘솔에 "Start"가 출력된다.
3. 실행된 함수는 콜 스택에서 빠져나가게 되고 브라우저 콘솔에만 "Start"가 남는다. 그 결과 다시 콜 스택은 빈 상태가 된다.
4. 이번에는 setTimeout()timer1이라는 콜백 함수를 가지고 콜 스택에 쌓이게 된다. 이때 setTimeout은 Web API를 호출하게 되는 함수이다.
해당 함수가 실행되게 되면 setTimeout의 두번째 인자인 5000ms이 지난 후에 첫번째 인자인 콜백함수 timer1()을 실행하게 된다.
5. 이제 실제로 어떻게 동작하는지 보면 콜 스택에서 setTimeout이 실행되게 되면 Web API의 timer가 timer1이라는 콜백을 가지고 등록되고 지정해준 시간 만큼 기다린다. 이때 실행된것이므로 콜 스택에서 setTimeout()은 제거된다.
6. 이때 콜 스택은 다시 비어있으므로 console.log('End');가 호출되고 실행 되어 브라우저 콘솔에 "End"가 출력되고 콜 스택이 다시 비워진다.
7. timer에 등록된 시간이 다 지난 후에 콜백인 timer1()콜백 큐 안으로 들어오게 되고 이벤트 루프는 콜 스택을 보고있다가 콜 스택이 비워지게 되면 콜백 큐의 가장 앞에 있는 콜백을 콜 스택으로 넘겨주게 된다.
8. 이제 콜 스택의 timer1()이 쌓인 상태이므로 해당 함수를 실행하게 된다. 그 안에는 console.log('callback'); 메소드가 있는데 콜 스택에 timer1()는 종료가 되기 전인 상태이므로 그 위에 console.log()가 쌓이게 된다.
9. console.log('callback');이 실행되고 나서야 브라우저 콘솔에 "callback"이 출력되고 콜 스택에서 사라지고 timer1() 또한 종료됐으므로 콜 스택에 마지막으로 없어진다.
10. 이제 작업이 종료되었다.

이것을 바탕으로 콜백 큐(Callback Queue)와 이벤트 루프(Event Loop)에 대해 정리해보면

  • 콜백 큐(Callback Queue)
    콜백 큐는 Web API에서 처리된 후 실행을 기다리는 콜백 함수를 보유하는 Data Structure이다. 콜백 대기열은 FIFO(선입선출) 방식으로 작동합니다. 즉, 첫 번째로 추가된 콜백이 가장 먼저 처리된다.

  • 이벤트 루프(Event Loop)
    이벤트 루프는 콜 스택과 콜백 큐를 지속적으로 모니터링하고 호출 스택이 비어 있으면 이벤트 루프는 콜백 큐에 콜백이 있는지 확인한다. 대기열에 콜백이 있는 경우 이벤트 루프는 첫 번째 콜백을 가져와 실행을 위해 호출 스택에 푸시한다.

profile
기록

0개의 댓글