콜 스택 — 지금 실행 중인 함수를 쌓아 두는 곳

자바스크립트가 "지금 어디를 실행 중인지" 기억하는 방법. 에러 메시지의 스택 트레이스가 바로 이것이다.

Seobway · · 6분

함수가 끝난 뒤 어디로 돌아가는지 누가 기억하는가

c() 가 끝나면 b() 로, b() 가 끝나면 a() 로 돌아가야 한다. 이 "돌아갈 자리"를
기억하는 것이 콜 스택이다.

function a() { b(); console.log("a 끝"); }
function b() { c(); console.log("b 끝"); }
function c() { console.log("c 실행"); }

a();
// c 실행
// b 끝
// a 끝     ← 들어간 순서의 반대로 끝난다

들어간 순서와 끝나는 순서가 반대다. 이게 스택의 성질이다.


콜 스택은 어떻게 동작하는가

콜 스택은 실행 중인 함수 호출을 쌓아 두는 후입선출(LIFO) 자료구조다.
함수를 호출하면 위에 쌓이고(push), 반환하면 위에서 빠진다(pop).

가장 위에 있는 것이 지금 실행 중인 함수다. 자바스크립트는 스레드가 하나라
콜 스택도 하나뿐이고, 따라서 한 번에 한 함수만 실행한다.

인터랙티브 다이어그램: a() → b() → c() 가 쌓였다 빠지는 과정 (단계별로 재생할 수 있다)

  1. a() 를 호출한다. 스택 맨 아래에 쌓인다.
  2. a 안에서 b() 를 호출한다. a 위에 쌓인다.
  3. b 안에서 c() 를 호출한다. 맨 위가 c 다 — 지금 실행 중인 함수.
  4. c 가 출력하고 반환한다. 맨 위에서 빠진다.
  5. 이제 맨 위는 다시 b 다. b 가 남은 줄을 실행하고 반환한다.
  6. a 도 남은 줄을 실행하고 반환한다. 스택이 비었다.
  7. 스택이 비면 자바스크립트는 다음 할 일을 찾는다.


에러 메시지의 스택 트레이스가 곧 콜 스택이다

에러가 났을 때 보이는 그 목록은 에러가 난 순간의 콜 스택을 위에서부터 찍은 것이다.

Uncaught TypeError: Cannot read properties of undefined
    at c (app.js:9)      ← 여기서 터졌다
    at b (app.js:5)      ← c 를 부른 곳
    at a (app.js:1)      ← b 를 부른 곳

위에서 아래로 읽으면 터진 지점 → 그것을 부른 곳 순서다. 원인을 찾을 때는
맨 위부터 보고, 내 코드가 아닌 라이브러리 프레임은 건너뛴다.


직접 확인

스택은 무한하지 않다. 끝나지 않는 재귀를 넣으면 한계를 눈으로 볼 수 있다.

function depth(n = 1) {
  return depth(n + 1);   // 반환하지 않으니 계속 쌓인다
}

try {
  depth();
} catch (e) {
  console.log(e.constructor.name + ":", e.message);
}

// 얼마나 깊이 쌓이는지 세어 보자
let count = 0;
function measure() {
  count++;
  measure();
}
try { measure(); } catch { console.log("최대 깊이 약", count);
}

깊이는 브라우저와 상황에 따라 다르다. 값 자체보다 스택에 한계가 있다는 사실이 중요하다.


흔한 실수


한 줄 정리

콜 스택은 실행 중인 함수 호출을 쌓아 두는 후입선출 구조이고, 자바스크립트에는
하나뿐이다. 그래서 한 번에 한 함수만 실행되고, 스택이 비어야 비동기 작업이 시작된다.

참고

  1. [1] Call stack — MDN Web Docs 용어 사전
  2. [2] RangeError: Maximum call stack size exceeded — MDN Web Docs
  3. [3] Executable Code and Execution Contexts — ECMAScript 사양

관련 글