본문으로 건너뛰기

var·let·const, 스코프, 실행 컨텍스트

1. var, let, const​

1.1 선언, 초기화, 할당​

변수를 만드는 일은 코드로는 선언과 할당 두 단계로 보이지만, 엔진은 그 사이에 초기화를 한 단계 더 둡니다.

let count; // 선언
count = 1; // 할당
단계하는 일
선언이름을 그 범위에 등록합니다
초기화등록한 이름에 메모리 자리를 주고 undefined를 넣습니다
할당그 자리에 실제 값을 넣습니다

세 키워드의 차이는 대부분 이 세 단계가 언제 일어나느냐에서 갈립니다.

1.2 var​

함수 스코프입니다. var로 선언한 변수는 그 변수를 감싼 함수 안이면 어디서나 읽힙니다. if나 for의 중괄호는 경계가 되지 않습니다.

function run() {
if (true) {
var a = 1;
}

console.log(a); // 1
}

선언 전에 읽어도 에러가 나지 않습니다. 엔진이 코드를 실행하기 전에 그 범위의 선언을 먼저 훑어 이름을 등록하고 undefined까지 넣어 두기 때문입니다.

console.log(a); // undefined
var a = 1;

1.1의 세 단계 중 선언과 초기화가 실행 전에 끝나고 할당만 제자리에 남는 셈입니다. 이름이 미리 등록되는 이 현상을 호이스팅이라고 합니다. 코드가 위로 옮겨지는 것이 아니라 이름이 먼저 등록되는 것입니다.

그 밖의 특징

  • 같은 이름을 다시 선언해도 에러가 없습니다.
  • 전역에 선언하면 window의 프로퍼티가 되어 다른 스크립트와 이름이 부딪힐 수 있습니다.

1.3 let과 const​

ES6에서 도입된 let과 const는 var의 특징을 모두 뒤집습니다.

블록 스코프입니다. 감싼 중괄호 안에서만 읽힙니다.

function run() {
if (true) {
let b = 2;
}

console.log(b); // ReferenceError: b is not defined
}

var는 블록을 지나 함수 스코프까지 읽히고, let은 블록에서 끊긴다

선언 전에 읽으면 에러가 납니다. 실행 전에 이름이 등록되는 것은 var와 같습니다. 다른 점은 초기화입니다. var는 등록하면서 undefined를 넣지만, let과 const는 등록만 하고 선언문이 실행될 때 초기화합니다.

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 1;

이름은 있지만 아직 읽을 수 없는 이 구간을 TDZ(Temporal Dead Zone)라고 합니다.

var는 생성 단계에서 undefined로 초기화되어 바로 읽을 수 있고, let은 선언문이 실행되기 전까지 TDZ에 있다

재선언은 SyntaxError입니다. 컴파일 단계에서 걸리므로 그 파일의 코드는 한 줄도 실행되지 않습니다.

let a = 1;
let a = 2; // SyntaxError: Identifier 'a' has already been declared

const는 재할당을 막습니다. 변수가 들고 있는 것은 값 자체가 아니라 값이 있는 자리를 가리키는 주소이고, const는 이 주소를 바꾸지 못하게 합니다. 주소가 가리키는 객체의 내부는 바뀝니다.

const list = [];

list.push(1); // 됩니다. 주소는 그대로고 배열 안만 바뀝니다
list = [1]; // TypeError: Assignment to constant variable.

주소를 나중에 바꿀 수 없으므로 선언과 동시에 값을 줘야 합니다.

반복문에서는 반복마다 새 바인딩이 생깁니다.

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}
// 3 3 3

for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}
// 0 1 2

var i는 함수 스코프라 i 칸이 하나뿐이고, 콜백이 실행될 때는 반복이 끝나 3이 들어 있습니다. let i는 반복마다 새 칸이 생겨 콜백이 각자 자기 차례의 값을 붙잡습니다.

1.4 비교​

varletconst
재선언됩니다SyntaxErrorSyntaxError
재할당됩니다됩니다TypeError
스코프함수블록블록
호이스팅이름 등록 + undefined 초기화이름 등록만이름 등록만
TDZ없습니다선언문 실행 전까지선언문 실행 전까지
선언만 하고 두기됩니다됩니다SyntaxError
전역에 선언하면window의 프로퍼티아닙니다아닙니다

호이스팅과 TDZ는 별개가 아니라 한 이야기의 앞뒤입니다. 셋 다 이름은 미리 등록됩니다. var만 그때 undefined까지 들어가서 읽어도 에러가 안 날 뿐입니다.

1.5 무엇을 쓰는가​

기본은 const, 재할당이 필요한 자리만 let입니다. const를 먼저 쓰면 이 이름은 끝까지 이 값이라는 사실이 코드에 드러나 읽는 사람이 추적할 대상이 줄어듭니다.

var는 쓰지 않습니다. 블록을 무시하고, 재선언이 조용히 통과하고, 선언 전에 읽어도 undefined를 돌려주고, 전역을 오염시킵니다. 모두 실수를 드러내지 않고 덮는 방향입니다.


2. 스코프(Scope)​

2.1 스코프란​

스코프는 이름이 통하는 범위입니다. 선언한 이름이 어디까지 보이는지를 정합니다.

const user = 'global';

function run() {
const temp = 'local';
console.log(user); // global
}

run();
console.log(temp); // ReferenceError: temp is not defined

가장 바깥이 전역 스코프, 함수나 블록 안이 지역 스코프입니다. 지역에서는 전역이 보이지만 반대는 아닙니다.

범위가 나뉘어 있어서 같은 이름을 다른 곳에 두고도 부딪히지 않습니다. 반대로 전역은 어디서나 보이는 만큼 어느 코드에서든 바꿀 수 있으므로, 변수는 쓰이는 자리에서 가장 가까운 범위에 두는 것이 원칙입니다.

2.2 함수 스코프와 블록 스코프​

지역 스코프를 만드는 단위가 둘입니다.

단위경계키워드
함수 스코프함수 몸통의 중괄호만var
블록 스코프모든 중괄호 (if, for, while, try, 단독 블록)let, const

객체 리터럴의 중괄호는 블록이 아니라 값이므로 스코프를 만들지 않습니다.

2.3 스코프 체인(Scope Chain)​

스코프는 중첩됩니다. 안쪽에서 이름을 못 찾으면 한 단계 바깥을 보고, 전역까지 올라가도 없으면 ReferenceError입니다. 이 탐색 경로가 스코프 체인입니다.

const a = 'global';

function outer() {
const b = 'outer';

function inner() {
const c = 'inner';
console.log(a, b, c); // global outer inner
}

inner();
}

안쪽에서 바깥으로만 올라가고, 전역까지 없으면 ReferenceError로 끝난다

탐색은 찾는 순간 멈춥니다. 그래서 안쪽에 같은 이름이 있으면 바깥 이름은 가려집니다. 이것을 섀도잉이라고 합니다.

const name = 'global';

function print() {
const name = 'local';
console.log(name); // local
}

2.4 렉시컬 스코프(Lexical Scope)​

스코프 체인이 어떻게 이어질지는 함수를 어디서 호출했는지가 아니라 어디에 썼는지로 정해집니다.

const name = 'global';

const printName = () => console.log(name);

const callIt = () => {
const name = 'local';
printName();
};

callIt(); // global

printName은 callIt 안에서 불렸지만 바로 옆에 있는 'local'을 보지 못합니다. 쓰인 자리가 전역이기 때문입니다.

코드에 적힌 위치로 범위가 정해지는 이 방식을 렉시컬 스코프라고 합니다. 실행하지 않아도 코드만 읽으면 범위가 확정되므로 정적 스코프라고도 부릅니다.


3. 실행 컨텍스트(Execution Context)​

3.1 실행 컨텍스트란​

실행 컨텍스트는 코드 한 덩어리를 실행하는 데 필요한 정보를 모아 둔 것입니다. 이 안에서 어떤 이름이 어떤 값인지, 여기 없으면 어디로 가서 찾을지, this는 무엇인지가 들어 있습니다.

스코프와 나란히 놓으면 역할이 분명해집니다.

스코프실행 컨텍스트
무엇인가이름이 어디까지 보이는지에 대한 규칙그 규칙대로 실제 값을 담아 둔 그릇
언제 정해지는가코드를 쓸 때 한 번실행할 때마다 새로

함수 호출 한 번을 작업 하나라고 보면, 실행 컨텍스트는 그 작업을 위해 편 작업대입니다. 같은 함수를 두 번 부르면 작업대도 둘이라 변수 값이 섞이지 않습니다.

3.2 언제 만들어지는가​

언제무엇이
스크립트를 시작할 때전역 실행 컨텍스트 하나. 끝날 때까지 남습니다
함수를 부를 때호출마다 하나. 함수가 끝나면 사라집니다

만들어질 때 두 단계를 거칩니다.

단계하는 일
생성 단계그 범위의 선언을 먼저 훑어 이름을 등록합니다. 코드는 아직 실행하지 않습니다
실행 단계코드를 한 줄씩 실행하며 값을 넣습니다

1장의 호이스팅이 바로 생성 단계입니다. var는 이때 undefined까지 들어가고, let과 const는 이름만 등록됩니다. 실행 단계에서 그 선언문을 만날 때까지가 TDZ입니다.

3.3 안에 무엇이 들어 있는가​

칸담는 것
Lexical Environmentlet과 const 선언, 그리고 바깥 환경으로 가는 링크
Variable Environmentvar 선언과 함수 선언문
This Binding이 컨텍스트에서 this가 가리키는 값

Lexical Environment가 핵심입니다. 변수 보관함이라고 보면 되고, 두 부분으로 되어 있습니다.

부분역할
환경 레코드이름과 값의 표. 변수를 찾을 때 먼저 보는 곳
바깥 환경 참조환경 레코드에 없을 때 가서 찾을 바깥 Lexical Environment의 주소

환경 레코드와 바깥 환경 참조로 이루어진 상자가 사슬처럼 이어지고, 전역의 참조는 null이다

2장에서 본 두 가지가 여기서 실물이 됩니다.

  • 바깥 환경 참조를 따라 올라가는 것이 스코프 체인입니다. 전역의 바깥 환경 참조는 null이라 거기까지 없으면 ReferenceError입니다.
  • 바깥 환경 참조는 함수를 부를 때가 아니라 함수를 만들 때 정해집니다. 이것이 렉시컬 스코프입니다.

Variable Environment가 따로 있는 이유는 블록에 들어갈 때 새로 만드는 쪽이 Lexical Environment뿐이기 때문입니다. let과 const는 블록마다 새 보관함에 들어가서 블록을 나오면 사라지고, var는 함수 단위로 한 번만 만드는 보관함에 들어가서 블록을 무시하는 것처럼 보입니다.

This Binding은 this가 정해지는 규칙이 그 자체로 한 편이라 여기서는 다루지 않습니다.

3.4 콜스택(Call Stack)​

실행 컨텍스트는 쌓입니다. 맨 위가 지금 실행 중인 것이고, 함수가 끝나면 위에서부터 빠집니다.

function outer() {
inner();
}

function inner() {
console.log('안쪽');
}

outer();

전역 위에 outer, 그 위에 inner가 쌓였다가 차례로 빠진다

빠지지 않고 계속 쌓이면 RangeError: Maximum call stack size exceeded가 납니다. 끝나지 않는 재귀에서 보는 에러입니다.

3.5 한 번에 따라가기​

const greeting = 'hi';

function greet(name) {
const msg = greeting + ' ' + name;
return msg;
}

greet('Kim');
  1. 스크립트가 시작되면 전역 실행 컨텍스트가 만들어집니다. 생성 단계에서 환경 레코드에 greeting과 greet이 등록됩니다. greet은 함수 선언문이라 함수 전체가 바로 들어가고, greeting은 const라 아직 비어 있습니다. 바깥 환경 참조는 null입니다.
  2. 실행 단계에서 greeting에 'hi'가 들어갑니다.
  3. greet('Kim')을 부르면 새 실행 컨텍스트가 만들어져 전역 위에 쌓입니다. 생성 단계에서 name은 'Kim', msg는 비어 있음으로 등록됩니다. 바깥 환경 참조는 전역입니다. greet이 전역에 쓰였기 때문입니다.
  4. 실행 단계에서 greeting을 찾습니다. greet의 환경 레코드에 없으니 바깥 환경 참조를 따라 전역으로 가서 'hi'를 찾습니다. msg에 'hi Kim'이 들어갑니다.
  5. return이 끝나면 greet의 실행 컨텍스트는 스택에서 빠지고 전역만 남습니다.

1장의 호이스팅과 TDZ는 1번과 3번의 "이름부터 등록하고 값은 비워 두기"이고, 2장의 스코프 체인은 4번의 "바깥 환경 참조 따라가기"입니다.


정리​

  • 변수는 선언, 초기화, 할당 세 단계를 거치고, 세 키워드의 차이는 이 단계가 언제 일어나느냐입니다.
  • 셋 다 이름은 실행 전에 등록됩니다. var만 그때 undefined까지 들어가고, let과 const는 선언문이 실행될 때까지 TDZ에 있습니다.
  • var는 함수 스코프, let과 const는 블록 스코프입니다. const는 재할당만 막고 객체 내부는 바뀝니다.
  • 스코프 체인은 안에서 밖으로만 올라가고, 어떻게 이어질지는 호출 위치가 아니라 코드에 쓴 위치로 정해집니다.
  • 스코프가 규칙이라면 실행 컨텍스트는 그 규칙대로 실제 값을 담은 그릇입니다. 함수를 부를 때마다 새로 만들어져 콜스택에 쌓이고, 끝나면 빠집니다.
  • 만들 때 생성 단계에서 이름부터 등록하고 실행 단계에서 값을 넣습니다. 호이스팅과 TDZ가 여기서 나옵니다.
  • 변수는 Lexical Environment의 환경 레코드에서 먼저 찾고, 없으면 바깥 환경 참조를 따라갑니다. 이것이 스코프 체인이고, 그 참조는 함수를 만들 때 정해집니다.