자바스크립트 메모리 구조와 실행 컨텍스트
Frontend/Javascript
· 2025-01-31
들어가며
Javascript는 논블로킹 싱글스레드 언어라고 표현한다. JS의 구조와 기능에 대한 특성을 한 마디로 축약해주는 표현이기 때문에 Javascript Series를 시작하기 가장 적합한 말이다. 이번 게시글에서는 JS의 싱글스레드 특성을 구조를 통해 살펴보고 이전 게시글의 스코프와 호이스팅이 '실행 컨텍스트'와 어떤 관계를 가지는지 알아본다.
자바스크립트는 싱글-스레드 언어
JS가 싱글 스레드 언어라는 것은 JS 엔진이 한 번에 하나의 작업만 처리할 수 있다는 것을 의미한다. 여러 작업이 동시에 실행되지 않고, 순차적으로 실행(동기)된다는 뜻이다. 그럼에도 비동기 프로그래밍을 통해 복잡한 작업을 효율적으로 처리할 수 있는 이유는 JS의 강력한 구조 덕분이다. 비동기 동작에 대해서는 (3)편에서 다루기 때문에 이번 게시글에서는 구조에 대해서만 다루며, 이 원리를 이해하기 위해서는 먼저 JS의 메모리 구조와 실행 컨텍스트 개념을 하나씩 살펴본다!
Synchronous Execution by Single Thread/싱글 스레드의 동기적 실행
메모리 구조
싱글 스레드 방식으로 동작하는 JS는 실행하는 동안 두 가지 메모리 구조를 사용한다. 딱 두 가지만 알자. 바로 콜 스택(Call Stack)과 힙(Heap)이다. 한 번에 하나의 작업만 처리한다는 싱글 스레드 특성은 하나의 콜 스택을 가졌다는 의미와 가다.
콜 스택(Call Stack)
-
JS에서 보이지 않는 곳에서 함수 호출을 관리하는 일종의 데이터 구조다.
-
가장 마지막에 들어온 컨텍스트부터 먼저 내보내는 후입선출(Last In, First Out: LIFO) 원칙에 따라 데이터를 저장하며, 실행이 끝난 함수는 Stack에서 제거된다.
-
이 안에 담기는 대상은 실행 컨텍스트와 원시타입 데이터이다.
-
실행 컨텍스트: 변수 식별자, 스코프 체인, this 등을 저장하고 필요시 메모리 힙을 참고하여 코드 실행에 필요한 변수, 함수 등의 위치를 참조하여 실행한다.
LIFO 원칙을 따르는 Call Stack 동작
스택에서 컨텍스트가 추가되는 것을 Push(삽입)된다고 하고, 맨 위의 컨텍스트가 삭제되는 것을 Pop(삭제)된다고 한다.
힙(Heap)
-
동적으로 데이터를 할당할 수 있는 메모리 공간이다.
-
그렇기 때문에 크기를 예측할 수 없는 참조 타입(객체, 배열, 함수 ..)을 저장하기에 적합하다.
-
참조 타입 데이터 : 객체, 배열, 함수는 Heap에 저장하고 각 힙의 주소는 Call Stack에 저장한다.
-
이전에 살펴보았던 JS의 가비지 컬렉터(Garbage Collector)에 의해 관리되어 더 이상 참조되지 않는 객체들은 자동으로 제거된다.
- 정확히는 변수를 재할당하는 과정에서 더 이상 참조되지 않는 값이 생기면 JS의 가비지 컬랙터가 해당 데이터를 메모리에서 해제한다.

✔️ 참조 타입인 배열과 객체를 let이 아닌 const로 선언을 권장하는 이유

(중요하다 생각해서 소주제 폰트로 따로 적어보았다)
불필요한 재할당을 방지한다. const로 변수를 선언하면, 해당 변수의 참조값을 변경할 수 없다(재할당이 불가하다). 반면, let은 메모리 주소 변경이 가능하여 참조값을 재할당할 수 있다. const로 선언된 데이터를 다루는 힙의 속성은 코드의 무결성을 보장하고, 의도치 않은 참조 주소 변경도 방지한다.
Push, Pop이 벌어지더라도 참조 타입이 저장된 힙의 주소를 변하지 않는다. 만일 let으로 선언을 하면, 참조값은 새로운 객체나 배열로 재할당될 수 있고 이는 불필요한 메모리 할당 및 해제를 발생시켜 성능에 부정적인 영향을 미친다.
혹시 콜 스택과 힙이 어디에 위치하는지 궁금한가? 필자처럼 머릿속으로 그려나가며 공부하는게 이해하기를 선호하는 편이라면 참고하자. 콜 스택과 힙은 JS 엔진 내부의 메모리 영역에 위치한다. 이 때 JS 엔진은 브라우저 일부에 포함되어있기 때문에 브라우저에 내장되어있다 볼 수 있지만, 브라우저가 콜 스택과 힙을 관리하거나 하진 않는다.
실행 컨텍스트
갑자기 등장하는 이 녀석은 뭘까? 정확한 정의는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체라고 하지만, 처음 접하는 이에게는 다소 낯선 이름과 정의다. 일단은 콜 스택에 들어오는 데이터 한 덩어리 정도라고 생각해보자. 페이지가 마운트될 때마다 실행 컨텍스트들을 하나씩 쌓이면서 Push되고, 실행이 완료된 컨텍스트는 Pop되면서 다시 콜 스택을 비우는 과정을 반복한다.
Call Stack에 Push, Pop되는 실행 컨텍스트
- Step1: 자바스크립트 코드가 실행되면 가장 먼저 전역 실행 컨텍스트(global)가 생성된다.
- Step2: 첫 번째 함수가 실행되어 실행 컨텍스트가 생성되고 콜 스택 최상단에 위치한다.
- Step3 ~ Step6: 실행되는 함수 순서대로 실행 컨텍스트가 생성되고, 종료되는 즉시 실행 컨텍스트도 제거된다.
- Step7: 모든 함수가 실행된 상태로 콜 스택에는 전역 컨텍스트만 남겨져있다.
콜 스택 안에서 실행 컨텍스트는 위와 같이 실행에 따라 생성되고 종료됨에 따라 제거된다. 이는 자바스크립트 전체 코드의 환경과 순서를 보장하기 위함이라는 것이 핵심이다.
실행 컨텍스트 종류
-
전역 실행 컨텍스트 : 전역에서 선언된 변수와 함수들로 구성되며, 자바스크립트 앱이 종료될 때까지 유지된다. 그렇기 때문에 우리는
this를 통해서 전역 객체인window객체에 접근이 가능한 것이다. -
함수 실행 컨텍스트 : 함수가 호출될 때마다 생성되는 실행 컨텍스트로 함수 내에서 사용되는 변수, 매개변수,
this값, 스코프 체인 등이 포함되어 함수가 도는 동안은 콜 스택의 최상단에 위치한다.
실행 컨텍스트 생성 과정
-
생성 단계 : 변수와 함수 선언이 메모리에 저장되지만, 변수는 아직 초기화되지 않은 상태로 호이스팅된다.
this가 바인딩되고, 스코프 체인이 설정된다. -
실행 단계 : 코드가 실행되고 변수들이 초기화되어 함수가 실행된다.
실행 컨텍스트 구성요소
다시 돌아와서 실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체라고 했다. 실행 컨텍스트는 Variable Environment와 Lexcial Environment, 그리고 this Binding 세 가지 구성요소로 이루어져있다. Variable Environment와 Lexcial Environment는 각각 environmentRecord와 outerEnvironmentReference를 포함한 동일한 구조로 되어있고, Lexical Environment의 역할에 따라 외부 환경을 참조하면서 스코프 체인을 만들어낸다.
전 게시글에서 var, let, const의 차이점 중에 스코프에 대해 언급하였다. 스코프가 만들어지기 위한 스코프 체인이 어떻게 외부 참조를 가능하게 하길래 함수 외부의 변수까지 접근이 가능한지를 고민해보며 살펴보자.
Variable Environment + Lexical Environment
- 둘 다 변수와 함수 선언을 관리한다. 함수가 호출될 때 그 함수 내부에서 선언된 변수들과 함수 선언을 모두 포함하고, 실행 컨텍스트가 생성될 때 호이스팅되는 대상들이다.
실행 컨텍스트 구성
-
실행 컨텍스트는 Variable Environment라는 구조에 식별자 정보를 먼저 저장한 뒤, 이를 Lexical Environment로 복사하여 코드 실행 중에 이를 활용한다. 즉, Variable Environment에 함수 실행 중 변화된 값을 그 하나씩 차곡차곡 기록하는 것이다. 즉, Variable Environment는 최초 시점 이후로 변경사항이 없고, Lexical Environment는 변경 사항이 실시간으로 반영되는 셈이다.
-
environmentRecord: 현재 실행 컨텍스트와 관련된 변수 및 함수 선언을 기록하는 객체다. 해당 컨텍스트에서 정의된 모든 식별자와 그 값이 저장된다.
-
outerEnvironmentReference: 현재 실행 중인 함수나 코드가 외부 스코프에서 정의된 변수를 참조할 때 사용되며, 해당 실행 컨텍스트가 참조할 수 있는 상위 스코프를 가리킨다. 즉, 현재 호출된 함수가 선언될 당시의 Lexical Environment를 참조하여 스코프 체이닝을 이룬다.
Lexical Environment 지속 참조
- 위와 같은 그림처럼 a함수가 내부 함수 b를 호출한다면, b함수는 a함수의 외부 환경을 참조하고 있기 때문에 접근이 가능하다.
스코프 체인(Scope Chain)
-
Javascript 코드에서 변수를 참조하는 과정을 정의하는 역할을 한다.(쉬운 말로는 그냥 변수를 찾는 과정 정도로 생각하자) 함수 내부에서 변수를 찾을 때, JS는 해당 함수의 스코프에서 변수를 찾아보고, 없을시 외부 스코프(outerEnvironmentReference)로 이동하여 변수를 찾을 때까지 이 과정을 반복하여 스코프 체인을 만들어낸다.
-
실행 컨텍스트는 outerEnvironmentReference를 통하여 자신이 선언된 시점의 외부 환경을 기억하여 상위 스코프의 변수까지 접근할 수 있는 것이다.
내부 함수 호출에 따른 참조와 스코프 체인 생성
- 위 사진에서
var로 선언된 변수b가second()에서console.log()를 통해 호출이 가능한 것도first()를 참조하여 스코프 체인을 통하여 가능한 것이다.
this 바인딩
-
실행 컨텍스트가 실행되는 환경에서
this가 참조하는 객체를 가리킨다. 즉, 전역 컨텍스트에서this는 전역 객체window를 가리키고, 실행 컨텍스트 생성 시점에 따라 어느 객체를 참조하는지를 가리키는 역할을 한다. -
한마디로
this식별자가 현재 바라봐야할 대상 객체이다. 업데이트되는 스코프에 따라 다르기 때문! 따라서 초기에는 Variable Environment와 동일하지만 변경 사항이 실시간으로 적용되면서 바뀌게 된다.
실행 컨텍스트와 호이스팅의 관계
JS에서 변수가 선언되기 전에 참조될 수 있게 하는 호이스팅은 실행 컨텍스트가 생성되는 과정에서 발생한다. 실행 컨텍스트의 environmentRecord 안에 식별자와 식별자에 바인딩된 값을 기록해두는 객체 덕분에 JS는 호이스팅을 위한 코드 실행 전에 변수와 함수 선언을 미리 메모리에 등록할 수 있다. 변수와 함수 선언을 미리 메모리에 등록하여 변수 초기화 전에 호출될 수 있게 함으로써 JS는 동적이고 유연한 실행 환경을 기반으로 복잡한 코드 흐름을 처리할 수 있는 것!이다.