탐색으로 돌아가기
V8 Engine18 / 24 단계

Call Stack

함수 실행 스택

자바스크립트는 싱글 스레드라, 함수 호출이 콜스택에 쌓이고(push) 끝나면 빠진다(pop). 한 번에 하나의 작업만 처리한다.

콜스택(call stack)은 인터프리터가 '지금 어느 함수를 실행 중이고, 그 함수를 어디서 호출했는가'를 추적하는 LIFO(Last-In-First-Out) 자료구조다. 함수를 호출하면 그 함수의 실행 컨텍스트(execution context)를 담은 스택 프레임(stack frame)이 스택 맨 위에 push되고, 함수가 return하면 그 프레임이 pop되어 호출한 위치로 제어가 돌아간다. JavaScript는 단일 스레드(single-threaded)라 콜스택이 하나뿐이며, 한 순간에 오직 하나의 프레임만 실행한다 — 이것이 동기 코드가 순차적으로 실행되는 이유다. 각 실행 컨텍스트는 그 함수의 지역 변수 환경(variable environment), 스코프 체인(scope chain, 렉시컬 환경 참조), this 바인딩(this binding)을 함께 보관한다. 재귀가 종료 조건 없이 계속되거나 너무 깊어지면 스택 프레임이 한계를 넘어 RangeError: Maximum call stack size exceeded(스택 오버플로우, stack overflow)가 발생한다.

내부 구성

스택 프레임 (Stack Frame / 실행 컨텍스트)
한 함수 호출의 실행 상태를 담는 단위. 스택의 최소 실행 단위이며 호출 시 push, 반환 시 pop
전역 실행 컨텍스트 (Global Execution Context)
스크립트 최초 실행 시 스택 바닥에 생성되는 최상위 컨텍스트. 전역 객체(window/globalThis)와 전역 this를 설정
함수 실행 컨텍스트 (Function Execution Context)
함수 호출마다 새로 생성되어 스택에 쌓이는 컨텍스트. 지역 변수·인자·내부 상태를 격리
변수 환경 (Variable Environment) / 렉시컬 환경
해당 컨텍스트의 변수·함수 바인딩을 저장하는 레코드. 클로저는 이 바인딩을 컨텍스트 종료 후에도 살려둔다
스코프 체인 (Scope Chain)
현재 렉시컬 환경에서 바깥(outer) 환경으로 이어지는 참조 사슬. 식별자 해석 시 안에서 밖으로 탐색
this 바인딩 (this Binding)
컨텍스트 생성 시 호출 방식에 따라 결정되는 this 값. 화살표 함수는 자체 this 없이 상위 스코프 것을 사용
반환 주소 / 명령 포인터 (Return Address)
함수 종료 후 제어가 돌아갈 호출 지점(다음 실행할 코드 위치)을 기록
스택 오버플로우 (Stack Overflow)
프레임이 스택 최대 크기를 초과할 때 발생하는 오류 상태(RangeError). 무한/과도 재귀가 대표 원인

핵심 포인트

  • 콜스택은 LIFO 구조: 함수 호출 시 프레임 push, return 시 pop
  • JavaScript는 단일 스레드 → 콜스택 1개, 한 번에 한 프레임만 실행(동기 실행의 근거)
  • 스택 프레임 = 실행 컨텍스트: 변수 환경, 스코프 체인, this 바인딩을 담는다
  • 스크립트 로드 시 전역 실행 컨텍스트(Global Execution Context)가 스택 바닥에 먼저 놓인다
  • this 바인딩은 호출 방식(메서드/일반/화살표/call·apply·bind/new)에 따라 컨텍스트 생성 시점에 결정된다
  • 스코프 체인은 렉시컬(정적) 구조라 '어디서 선언됐는가'로 결정되지, 어디서 호출됐는가가 아니다
  • 종료 없는/과도한 재귀는 스택 오버플로우(RangeError)로 이어진다

심화

면접에서 콜스택과 실행 컨텍스트를 물으면 '생성 단계(creation phase)와 실행 단계(execution phase)'를 구분해 답하면 깊이가 산다. 컨텍스트가 스택에 올라갈 때 먼저 생성 단계에서 var는 undefined로, 함수 선언은 통째로 메모리에 올려두는 호이스팅(hoisting)이 일어나고, let/const는 선언은 되지만 초기화 전이라 접근 시 TDZ(Temporal Dead Zone) 에러가 난다. this 바인딩과 outer 환경 참조(스코프 체인의 뿌리)도 이 생성 단계에서 확정된다. 그다음 실행 단계에서 코드를 한 줄씩 돌리며 값을 대입한다. 또 하나 중요한 구분은 '스코프 체인은 렉시컬(정적)이고 콜스택은 동적(런타임)'이라는 점이다. 스코프 체인은 코드를 어디에 작성했느냐로 컴파일 타임에 정해지므로, 함수를 어디서 호출하든 변수 탐색 경로는 동일하다 — 이것이 클로저가 성립하는 원리다. 반대로 콜스택은 실제 호출 순서에 따라 런타임에 쌓인다. 그래서 에러 스택 트레이스(콜스택 스냅샷)와 스코프 체인은 별개다. 마지막으로 'JS는 왜 재귀 깊이에 한계가 있나'는 각 프레임이 실제 메모리(스택 메모리)를 차지하기 때문이며, ES6의 꼬리 호출 최적화(TCO)는 명세엔 있지만 대부분 엔진이 구현하지 않아 실무에선 재귀 대신 반복문이나 명시적 스택으로 푸는 게 안전하다는 점을 덧붙이면 좋다.

쉽게 말하면 접시를 위로 쌓고(호출) 위에서부터 치우는(반환) 스택. 맨 위 접시만 다룰 수 있음.

면접 예상 질문

#콜스택#싱글 스레드#LIFO#블로킹