본문 바로가기
개발

자바스크립트 호출 스택(Call Stack) 이해하기

by 마스터누누 2019. 1. 7.
728x90
반응형

시작하기에 앞서

자바스크립트를 심도 있게 이해하기 위해 "모든 자바스크립트 개발자가 알아야할 33가지 개념(https://github.com/yjs03057/33-js-concepts)"의 항목들과 링크되어있는 블로그 글들을 정리하여 포스팅하기로 했습니다. 


호출 스택의 경우 How JavaScript works: an overview of the engine, the runtime, and the call stack 이라는 글이 완성도가 높고 깔끔하게 설명했기 때문에, 본문의 거의 대부분을 차지 하게 되었습니다. 통번역이라기 보다는, 필요한 내용을 추가, 통합, 삭제하는 과정이 많아서, 본래 내용과는 다소 차이가 발생할 수 있습니다. 원문을 보기 원하시는 분들은 최하단의 링크를 참조해 주시기 바랍니다. 


마지막으로, 포스트를 작성하는 과정에서 생긴 오역 및 잘못된 내용들은 댓글에 적어주시면 수정하도록 하겠습니다.



개요


자바스크립트는 점점 대중화, 확장되고 있으며 그 범위는 프론트엔드 뿐만 아니라 백엔드, 임베디드 프로그래밍까지 영향을 미치고 있습니다.


그러나 실제로 자바스크립트의 내부 동작을 이해하는 사람은 그렇게 많지 않다고 생각합니다. 예를 들면, 함수의 호출이나 이벤트 루프의 동작 원리와 같은 부분입니다. 이 글은 자바스크립트를 더 깊게 이해함으로써, 더 나은 코드와 앱을 작성할 수 있게 되는 것이 목표입니다.


만약 입문자라면, 자바스크립트와 다른 언어의 차이점이 무엇인지 이해 할 수 있을 것 입니다. 기존 개발자라면, 자바스크립트에서 제공되는 API의 동작 원리를 더 잘 이해하게 될 것입니다.


자바스크립트 엔진

가장 대중적인 자바스크립트의 엔진은 구글의 V8 엔진입니다. V8 엔진은 크롬과 노드 안에서 동작합니다.

자바스크립트 엔진은 다음과 같은 두 가지 주요 구성 요소로 이루어져 있습니다.



  • 메모리 힙(Memory Heap) — 객체는 힙, 대부분 구조화되지 않은 메모리 영역에 할당된다. 변수와 객체에 대한 모든 메모리 할당은 여기서 발생한다.
  • 호출 스택(Call Stack) — 코드가 실행될 때 호출 스택이 쌓인다.


실행 환경(Runtime)

브라우저에는 자바스크립트 개발자가 사용하는 거의 모든  API가 있습니다(예: setTimeout ). 그러나 이런 API 들은 엔진에서 제공해주지 않습니다. 그렇다면 이 API들은 어디서 오는 걸까요?



사실 브라우저는 단순히 엔진 하나만으로 구성되어 있지 않습니다. DOM, AJAX, setTimeout 등의 브라우저에서 제공하는 Web API라고 하는 것들이 있습니다. 또한 이러한 Web API의 호출을 통제하기 위한 Event Queue와 Event Loop도 존재합니다.


호출 스택(Call Stack)

자바스크립트는 단일 스레드 프로그래밍 언어이므로, 단일 호출 스택이 있습니다. 단일 호출 스택이 있다는 뜻은 한 번에 하나의 일(Task)만 처리할 수 있다는 뜻입니다.


호출 스택이란 프로그램에서 우리가 어디에 있는지를 기본적으로 기록하는 데이터 구조입니다. 동작 방식은 다음과 같습니다. 함수를 실행하면 해당 함수의 기록을 스택 맨 위에 추가(Push) 합니다. 우리가 함수를 결과 값을 반환하면 스택에 쌓여있던 함수는 제거(Pop) 됩니다. 예제를 살펴보도록 하죠.


function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);


엔진이 이 코드를 실행하기 전에는 호출 스택이 비어있습니다. 가장 아랫줄에 printSquare 함수가 실행되면 이후 단계는 다음과 같습니다



호출 스택의 각 항목을 스택 프레임이라고 합니다.


예외 처리 시 스택의 동작


아래의 코드는 예외가 던져질 때 스택의 호출이 어떤 순서로 일어나는지 알려줍니다. 다음의 코드를 살펴봅시다.


function foo() {
    throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
    foo();
}
function start() {
    bar();
}
start();


만약 위의 코드가 크롬에서 실행된다면 아래와 같은 순서로 에러가 발생할 것입니다



스택 오버플로우

이름 그대로 스택의 사이즈를 초과 했을 때 발생하는 오류입니다. 스택 오버플로우는 생각보다 쉽게 일어날 수 있습니다. 특히 재귀를 호출했을 때 말이죠.


function foo() {
    foo();
}
foo();


마지막 줄에서 foo() 함수가 실행되는데, foo() 함수의 내부를 살펴보면 종료 조건 없이 자신을 계속해서 호출하게 됩니다. 따라서 함수의 스택 프레임이 계속해서 호출 스택에 쌓이게 됩니다.



그러다가 어떠한 시점에서 호출 스택의 함수 호출 수가 호출 스택의 실제 크기를 초과하게 되고, 브라우저는 다음과 같은 오류를 발생시키는 것으로 함수를 종료 시킵니다.



단일 호출 스택의 문제점

단일 스레드에서 코드를 실행하는 것은 멀티 스레드 환경에서 발생하는 복잡한 시나리오(예: deadlocks)를 고려할 필요가 없으므로 매우 쉽습니다. 그러나 단일 스레드에서 실행하는 것도 상당히 제한적입니다. 자바스크립트에서는 하나의 호출 스택만 있기 때문에, 하나의 함수 처리가 엄청 느려서 다른 함수 실행에 지장을 줄 때는 어떻게 해야 할까요?


예를 들어, 브라우저에서 복잡한 이미지 처리를 한다고 생각해봅시다. 앞서 배운 호출 스택의 동작 방식을 생각 해볼 때, 이미지 처리 작업 스택을 차지하고 있으면 자바스크립트는 후속 작업들을 처리할 수 없습니다. 단일 스레드, 단일 호출 스택이기 때문입니다.


문제는 이것뿐만이 아닙니다. 브라우저가 호출 스택에서 많은 작업을 처리하기 시작하면 꽤 오랜 시간 동안 응답을 멈출 수 있습니다. 대부분의 브라우저는 이 상황에서 웹 페이지를 종료할지 여부를 묻는 오류 메시지를 표시합니다. 그렇다면 해결 방법은 무엇일까요?



비동기 콜백(Asynchronous callbacks)

가장 쉬운 해결책은 비동기 콜백을 사용하는 것입니다. 즉, 우리의 코드 일부를 실행하고 나중에 실행될 콜백(함수)를 제공합니다. 비동기 콜백은 즉시가 아닌, 특수한 시점에 실행되므로 console.log와 같은 동기 함수와는 다르게 스택 안에 바로 push 될 필요가 없습니다. 그런데 스택이 아니라면 이 콜백 함수들은 누가 관리하는 걸까요?


이벤트 큐(Event Queue)와 비동기 콜백의 처리 과정

자바스크립트 실행환경(Runtime)은 이벤트 큐(Event Queue)를 가지고 있습니다. 이는 처리할 메시지 목록과 실행할 콜백 함수 들의 리스트입니다.



그럼 비동기가 처리되는 과정을 살펴봅시다. 우선 버튼 클릭과 같은 이벤트가 발생하면 DOM 이벤트, http 요청, setTimeout 등과 같은 비동기 함수는 C++로 구현된 web API를 호출하며, web API는 콜백 함수를 이벤트 큐(콜백 큐)에 밀어 넣습니다. 그럼 이벤트 큐는 대기하다가 스택이 텅 비는 시점에 이벤트 루프를 돌리게 됩니다(스택에 넣음). 이벤트 루프의 기본 역할은 큐와 스택, 두 부분을 지켜보다가 스택이 비는 시점에 콜백을 실행시켜 주는 것. 각 메시지와 콜백은 다른 메시지가 처리되기 전에 완전히 처리됩니다.


웹 브라우저에서는 이벤트가 발생할 때마다 메시지가 추가되고 이벤트 리스너가 첨부됩니다. 따라서 리스너가 없으면 이벤트가 손실됩니다. 콜백 함수의 호출은 호출 스택의 초기 프레임으로 사용되며, 자바스크립트가 싱글 스레드이므로 스택에 대한 모든 호출이 반환될 때까지 메세지 폴링(polling) 및 처리가 중지됩니다. 동기식 함수 호출은 이와 반대로 새 호출 프레임을 스택에 추가합니다.


추가로, 비동기 콜백 및 호출 스택, 이벤트 루프의 동작 순서는 다음 포스팅을 참고해 주시기 바랍니다

자바스크립트 호출 스택(Call Stack) 동작 예제 - devnunu


참고 및 출처

Understanding Javascript Call Stack, Event Loops — Gaurav Pandvia

How JavaScript Works: An Overview of the Engine, the Runtime, and the Call Stack — Alexander Zlatkov







반응형

댓글