코어 자바스크립트 1
On this page
여는 글
퍼블리셔 재직 시절부터 프론트엔드 취준 ~ 지금까지 자바스크립트의 바이블처럼 끼고 살던 책들이 있다. 프론트엔드 개발자라면 모두들 찍먹은 해봤을 이웅모님의 모던 자바스크립트 Deep Dive, 정재남님의 코어 자바스크립트. 특히 코어 자바스크립트는 프로토타입, 클로저 등 아무리 MDN을 읽고 다른 사람들이 작성한 글을 봐도 이해가 안되던 어려운 개념들을 이해하게 해준 한줄기 빛같은 책이었다.
물론 코어 자바스크립트도 ES6를 중심으로 자바스크립트의 핵심 이론을 설명하고 있기 때문에 ECMAScript 2025 까지 공식 발행된 지금, 최신의 자바스크립트 작동 원리와 세부적으로는 다른 부분이 있을 수 있다. 그렇지만 전반적인 구동 원리를 다룬다는 점에서 이 책만한게 없다. 다시 코어 자바스크립트를 복기하면서 메모할 부분들, 잊고 있던 개념들, 추가적으로 새로 알게된 것들을 블로그로 정리해보려고 한다.
데이터 타입
자바스크립트 데이터 타입에는 크게 두가지 종류가 있다. 흔히 말하는 원시타입과 참조타입.
- Primitive Type
- Number
- String
- Boolean
- null
- undefined
- Symbol
- Reference Type
- Object
- Array
- Function
- RegExp
- Set / WeakSet
- Map / WeakMap
- …
- Object
자바스크립트 메모리 구조는 스택 메모리, 힙 메모리 영역으로 나뉘어 있다. 스택 영역에는 변수와 함께 원시형 데이터가 저장되고, 힙 영역에는 참조형 데이터가 저장된다.
우리가 브라우저로 웹사이트에 접속하면, 자바스크립트 메모리는 실제로 어디에 어떻게 저장될까?
- 브라우저가 렌더러 프로세스를 만들거나 재사용한다. 이 프로세스는 운영체제로부터 가상 메모리를 받고, 실제 접근된 부분만 RAM에 매핑된다.
- 프로세스 안에는 메인 스레드(필요 시 워커 스레드)가 있고, 각 스레드마다 스택(콜스택)이 있다. 스택에는 호출 프레임·리턴 주소·일부 로컬 값이 잠시 올라왔다가 바로 내려간다(pop/push).
- 같은 프로세스 안에서 JS 엔진(V8) 이 Isolate + JS 힙을 준비한다. 데이터(객체·배열·함수)는 힙, 실행 중 필요한 프레임/참조는 스택을 쓴다.
<script>
/type="module"
또는 이벤트 루프에 의해 실행 트리거가 오면, 엔진이 파싱→컴파일→실행하고, 이후 GC가 힙을 주기적으로 회수·압축한다.- 전체 메모리가 JS 힙만 있는 것은 아니다. DOM/스타일 등 네이티브 메모리,
ArrayBuffer
/WASM 같은 외부 메모리, 텍스처/버퍼 등 GPU 메모리도 별도로 잡힌다. - 탭을 닫거나 다른 사이트로 완전히 이동하면 해당 Isolate/힙/스레드가 정리되고, 필요에 따라 프로세스도 종료되거나 재사용된다.
스택은 스레드 로컬 실행 메모리, 힙은 엔진이 GC로 관리하는 데이터 메모리이며, 둘 다 렌더러 프로세스의 메모리(결국 RAM) 위에서 동작한다.
우리가 데이터를 어떤 변수에 할당할 때, 기본형 데이터는 값 자체가 변수 슬롯에 들어간다고 이해하면 편하다. 참조형 데이터는 힙 메모리의 어떤 주소에 객체가 놓이고 변수 슬롯엔 해당 주소가 저장된다.
let a = { n: 1 }; // 힙에 객체 생성, a에는 그 참조값이 저장됨
let b = a; // 참조값(주소 같은 것) 복사 → a와 b가 같은 객체를 가리킴
b.n = 2; // 같은 객체 내부를 수정
a = { n: 3 }; // a에 '새 객체'의 참조를 대입 (b는 여전히 이전 객체를 가리킴)
console.log(b.n); // 2
그래서 기본형은 값이 바로 바뀌는 반면에, 참조형은 값이 바뀌지 않는다.
그렇다면 모두 값을 직접 저장하면 안되는걸까?
- 값을 직접 저장하면 데이터 할당시에는 빠를 수 있으나 값의 비교에는 비용이 많이 든다. 메모리 낭비가 심하다는 문제가 있다.
- 값의 주소를 저장하면 데이터 할당시에는 해당 값이 있는지 없는지 확인(비교)해야하므로 느릴 수 있으나, 할당 후에는 같은 주소라면 항상 같은 값을 가르키므로 비용이 들지 않는다. 또한 메모리 낭비를 최소화할 수 있다.
GC에 대하여
JS 엔진은 도달성(reachability) 을 기준으로 힙의 객체를 지운다. 전역/콜스택/클로저 등 루트에서 더 이상 경로가 없는 객체 그래프는 회수 대상이 된다.
이 말은 즉 불필요한 참조를 적절히 끊어주어야 GC가 알아서 회수할 수 있고, 메모리 낭비를 줄일 수 있다는 의미이다.
참조 끊기의 예)
-
변수/프로퍼티/컬렉션: 재할당/
null
대입,splice
/map.delete()
로 엔트리 제거 -
클로저/리스너/타이머:
removeEventListener
/clearInterval
등으로 해제. 콜백이 큰 객체를 쥐고 있지 않게 구조를 분리
실행 컨텍스트
실행 컨텍스트란?
동일한 환경의 코드를 실행하는 데에 필요한 배경이 되는 환경정보
동일한 환경을 지닐 수 있는 조건은 딱 4가지
- 전역공간
- 함수
eval- module
결국 자바스크립트의 독립된 코드 뭉치라고 하는 것은 곧 함수이다. 자바스크립트는 함수에 의해서만 컨텍스트를 구분할 수 있다. 블록스코프(if/for/switch/while)은 별개의 실행컨텍스트가 존재하지 않는다. 정리하면 실행 컨텍스트란 “함수를 실행할 때 필요한 환경정보를 담은 객체”이다.
아래의 코드를 보고 어떤 순서로 콘솔 로그가 실행될지 예상해보자.
var a = 1;
function outer() {
console.log(a); // 1️⃣
function inner() {
console.log(a); // 2️⃣
var a = 3;
}
inner();
console.log(a); // 3️⃣
}
outer();
console.log(a); // 4️⃣
// 실행컨텍스트의 순서에 의해 주석의 숫자 순으로 실행될 것이다.
자바스크립트는 이러한 실행컨텍스트를 콜스택에 담아 실행 순서를 결정한다. 콜스택이란 현재 어떤 함수가 동작중인지, 다음에 어떤 함수가 호출될 예정인지 등을 제어하는 자료구조이다. 실행컨텍스트에는 세 가지 환경 정보가 담긴다.
- VariableEnvironment : 식별자 정보 수집 (변화 반영 X)
- LexicalEnvirionment : 각 식별자의 데이터 추적, 컨텍스트 내부 코드들을 실행하는 동안에 변수의 값들의 변화가 생기면 그 값이 저장된다. (변화 반영 O)
- envirionmentRecord (현재 문맥의 식별자 정보)
- outerEnvironmentReference (외부 환경 참조)
- ThisBinding
식별자 정보의 변화를 반영하는 Lexical Envirionment를 집중해서 살펴보면, Lexical Envirionment는 실행 컨텍스트를 구성하는 환경 정보들을 모아 사전처럼 구성한 객체라고 할 수 있다.
실행 컨텍스트 A
- 내부 식별자 a: 현재 값은 undefined
- 내부 식별자 b: 현재 값은 20
- 외부 정보: D를 참조
실행 컨텍스트가 최초 실행될 때, 현재 컨텍스트의 식별자 정보들을 수집해서 envirionmentRecord 에 담는다. 이 과정을 바로 **호이스팅(Hoisting)**이라고 한다. 호이스팅은 결국, 실행 컨텍스트의 맨 위로 식별자 정보를 끌어올리는 과정이다.
outerEnvironmentReference는 외부 환경에 대한 참조인데, 즉 Lexical Environment에 대한 참조를 의미한다. 이 outerEnvironmentReference가 관여하는 것은 결국 우리가 흔히 알던 **스코프 체인(Scope Chain)**이라는 것을 알 수 있다.
아래의 그림은 위의 자바스크립트 코드를 실행했을 때, 콜스택에 실행컨텍스트가 담기는 순서와 실행 컨텍스트 내부 구조를 그린 그림이다.
그래서 스코프 체인이 뭔데?
코드와 그림을 함께 엮어 설명하면, 우선 inner함수의 실행 컨텍스트에서 선언한 변수는 envirionmentRecord에 의해서 접근을 할 수 있다. 그리고 outerEnvironmentReference를 통해서 outer함수의 Lexical Environment 정보에도 접근을 할 수가 있다. 한편, outer함수의 실행 컨텍스트 입장에서는 outer 내부에서 선언한 식별자들과 전역공간에서 선언한 변수에도 접근이 가능하다. 하지만 inner에서 선언한 변수를 outer 함수에서 접근하는 것은 불가능하다. 한편 전역공간에서 선언한 변수는 inner, outer 모두 접근이 가능하다. 이것이 스코프이다. 외부로는 나갈 수 있지만 자기보다 안쪽으로는 들어갈 수 없다. 변수의 유효범위가 정해지는 것이다.
다시 코드로 돌아와 스코프 체인에 의해서 어떤 값이 출력될지 확인해보자.
var a = 1; // 전역 스코프에 a 바인딩
function outer() { // outer의 렉시컬 스코프 형성
console.log(a); // 1 ← 스코프 체인: outer→전역. outer에 a가 없으니 전역 a를 찾음
function inner() {
// 호이스팅: 'var a' 선언이 inner 지역 스코프에 먼저 올라가며 초기값은 undefined
console.log(a); // undefined ← 현재 스코프에 a가 있으므로 전역으로 올라가지 않음
var a = 3; // 여기에서야 inner의 a에 3을 대입
}
inner();
console.log(a); // 1 ← 여전히 전역 a를 본다
}
outer();
console.log(a); // 1 ← 전역 a
정리하면, 실행 컨텍스트는 다음과 같이 구성된다.
Execution Context
- Variable Envirionment
- Lexical Envirionment
- environmentRecord: 현재 문맥의 식별자 (hoisting)
- outerEnvironmentReference : 외부 식별자 (scope chain)
- this
정리하다 보니 글이 길어져 나머지 개념 (this, closer, prototype, class 내가 헷갈렸던 것들 위주)에 대한 정리는 나누어 작성해야할 것 같다.