자바스크립트에서 변수 선언과 실행 순서에 대한 직관적인 이해는 실제 동작 방식과 차이가 있습니다. 이를 이해하려면 호이스팅(Hoisting)과 스코프(Scope) 개념을 명확히 알아야 합니다.
호이스팅이란?
호이스팅은 변수 선언이 코드 실행 전에 해당 스코프의 최상단으로 끌어올려지는 동작을 말합니다. 예를 들어, 다음과 같은 코드에서 undefined가 출력되는 이유를 설명할 수 있습니다.
console.log('b', b); // undefined
var b = 1;
여기서 var b = 1은 실제로 두 단계로 처리됩니다:
var b(선언) - 호이스팅되어 실행 컨텍스트에 등록b = 1(할당) - 원래 위치에서 실행
자바스크립트 엔진은 코드를 실행하기 전에 실행 컨텍스트(Execution Context)를 생성합니다. 이 컨텍스트는 변수 환경(Variable Environment)과 렉시컬 환경(Lexical Environment)으로 나뉩니다. 변수 선언은 실행 코드와 분리되어 먼저 처리됩니다.
실제 처리 순서는 다음과 같습니다:
// 실행 컨텍스트 생성 단계
var b; // 선언 호이스팅
b = undefined; // 기본값 할당
// 실행 단계
console.log('b', b); // undefined
b = 1;
같은 이름의 변수 선언이 여러 번 있어도 첫 번째 선언만 유효합니다. 하지만 할당문은 실행 단계에서 처리되므로 호이스팅되지 않습니다.
더 복잡한 예제를 보겠습니다:
console.log('a', a); // undefined
var b = 1;
var b = function() {
var a = 2;
console.log('ab', a, b); // 2, [Function]
};
var a = 3;
b();
console.log('a', a, b); // 3, [Function]
위 코드는 내부적으로 다음과 같이 처리됩니다:
// 변수 선언 호이스팅
var b;
var b; // 중복 선언은 무시
var a;
console.log('a', a); // undefined
b = 1; // 첫 번째 할당
b = function() { ... }; // 두 번째 할당으로 덮어쓰기
a = 3;
b();
console.log('a', a, b); // 3, [Function]
주의: 암시적 선언(값만 할당하고 선언하지 않는 경우)은 호이스팅되지 않습니다.
console.log('a', a); // ReferenceError: a is not defined
a = 1;
블록 레벨 스코프
let과 const는 블록 레벨 스코프를 제공합니다. var는 함수 레벨 스코프만 가지지만, let과 const는 중괄호 {}로 생성되는 모든 블록을 스코프로 인식합니다.
블록 레벨 스코프의 주요 특징
- 변수 이름 중복 불가
- 선언 전 사용 불가 (일시적 사각지대, TDZ)
- 호이스팅이 발생하지 않음 (실제로는 호이스팅되지만 TDZ로 인해 접근 불가)
다음 코드는 변수 이름 중복으로 인해 오류가 발생합니다:
var a = 1;
let b = 3;
console.log('a', a);
let a = 2; // SyntaxError: Identifier 'a' has already been declared
let b = 4; // SyntaxError: Identifier 'b' has already been declared
let과 const는 선언 전에 이미 해당 이름이 존재하는지 확인합니다.
호이스팅되지 않는 예제:
console.log('a', a); // undefined (var로 선언됨)
console.log('b', b); // ReferenceError: Cannot access 'b' before initialization
var a = 1;
let b = 2;
일시적 사각지대 (Temporal Dead Zone, TDZ)
블록 내에서 let이나 const로 변수를 선언하면, 해당 블록 내에서는 전역 변수와 동일한 이름이어도 전역 변수보다 지역 변수가 우선합니다. 선언 전에는 접근할 수 없습니다.
var a = 1;
var c = 2;
var b = function() {
console.log('c', c); // 2 (전역 변수 접근 가능)
console.log('a', a); // ReferenceError: Cannot access 'a' before initialization
let a = 2;
};
b();
함수 내부에 let a가 선언되어 있으면, 함수 내에서는 전역 변수 a가 가려집니다. 따라서 a를 선언 전에 사용하려고 하면 오류가 발생합니다.
블록 레벨 스코프의 세 가지 특징을 정리하면:
1. 변수 이름 중복 불가
2. 변수 호이스팅 없음 (TDZ 존재)
3. 일시적 사각지대
const 추가 설명
const는 let과 거의 동일하지만, 한 가지 차이가 있습니다: const로 선언된 변수는 재할당이 불가능합니다.
let a = { name: '홍길동' };
const b = { name: '홍길동' };
a = { name: '김철수' }; // 가능
b = { name: '박영희' }; // TypeError: Assignment to constant variable
하지만 재할당이 불가능할 뿐, 객체나 배열의 내부 속성은 변경할 수 있습니다.
const str = "123";
console.log(str.slice(0, 1)); // '1'
const obj = { name: '123' };
obj.name = '345';
console.log(obj); // { name: '345' }
const arr = ['1'];
arr.push('2');
console.log(arr); // ['1', '2']
const는 변수 자체에 대한 재할당을 막을 뿐, 참조하는 객체의 변경을 막지 않습니다.