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)가 발생한다.
내부 구성
핵심 포인트
- 콜스택은 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)는 명세엔 있지만 대부분 엔진이 구현하지 않아 실무에선 재귀 대신 반복문이나 명시적 스택으로 푸는 게 안전하다는 점을 덧붙이면 좋다.
쉽게 말하면 접시를 위로 쌓고(호출) 위에서부터 치우는(반환) 스택. 맨 위 접시만 다룰 수 있음.