자바스크립트 코드의 실행 과정 - 실행 컨텍스트에서 변수를 선언할 때의 3가지 단계(선언, 초기화, 할당)를 실행 컨텍스트와 렉시컬 환경을 통해서 설명했습니다. 이번 글에서는 이러한 변수 선언 과정을 거치면서 JS 내부에서는 어떤 일이 발생하는 지, 호이스팅이 발생하는 이유는 무엇인지에 대해 더욱 자세하게 알아보겠습니다.
변수를 선언했을 때, 변수 이름은 어디에 등록되는가?
변수 이름을 비롯한 모든 식별자는 실행 컨텍스트에 등록됩니다. 실행 컨텍스트란, 간단히 말하면 프로그램이 실행되는 동안 변수와 함수가 존재하는 장소입니다. 변수 이름과 변수 값은 실행 컨텍스트 내에 키/값 형식인 객체로 등록되어 관리됩니다.
JS 엔진은 소스코드를 한 줄씩 순차적으로 실행하기에 앞서 먼저 소스코드의 평가 과정을 거칩니다. 이 때 변수 선언을 포함한 모든 선언문을 소스코드에서 찾아내 먼저 실행합니다. 그리고 평가 과정이 끝나면 비로소 선언문을 제외한 소스 코드를 한 줄씩 순차적으로 실행합니다.
코드의 실행 과정 : 평가 => 실행
=> 이로 인해, 변수 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 JS 고유의 특징을 변수 호이스팅이라고 합니다.
호이스팅이 발생하는 이유 ?
=> 코드를 순차적으로 한줄 한줄 실행하기 전에 소스 코드를 평가하면서 변수 선언문이 먼저 실행되기 때문입니다.
사실 변수 뿐만 아니라 함수, 클래스 등의 모든 식별자가 호이스팅 됩니다. 모든 선언문은 런타임 이전 단계에서 먼저 실행되기 때문입니다.
var score = 80;
다음과 같은 코드는 변수의 선언과 값의 할당을 하나의 문으로 단축 표현한 것이며, 이는 실제로 아래와 같이 2개의 문으로 나누어 각각 실행됩니다.
var score; // 변수의 선언
score = 80; // 값의 할당
이 때 주의 할 점은 변수 선언과 값의 할당의 실행 시점이 다르다는 것이다.
값의 할당이 일어나면 undefined
값이 있던 메모리에 새로운 값인 80
을 덮어 쓰는 것이 아니라, 80을 위한 새로운 메모리를 할당하고, score는 그 메모리를 가리키게 됩니다.
이는 undefined
값은 이제 더 이상 필요하지 않다는 것을 의미합니다. 이러한 불필요한 값들은 garbage collector
에 의해 메모리에서 자동 해제됩니다. 단, 메모리에서 언제 해제될지는 예측할 수 없습니다.
console.log(score); // undefined
score = 80;
var score;
console.log(score); // 80
var variable1 = "변수";
let variable2 = "123";
위와 같이 변수를 선언하고 값을 할당하는 코드가 실행되면 다음 3가지 단계로 나누어 실행됩니다.
이를 실제 실행 컨텍스트 관점에서 다시 표현해보면 다음과 같이 표현할 수 있습니다.
선언 단계
변수를 Execution Context의 Lexical Environment에 등록합니다.
초기화 단계
Lexical Environment에 등록되어 있는 변수를 위하여 메모리를 할당하는 단계로, 여기서 변수는 undefined로 초기화됩니다.
할당 단계
변수에 실제로 값이 할당되는 단계입니다. (undefined → 특정한 값)
우리가 선언한 변수나 상수는 값이 아닌 메모리 주소를 바라보고 있습니다.
let variable = 126;
let variable2 = variable;
이렇게 새로운 변수에 기존 변수를 대입한다면, variable2
가 variable
의 메모리 주소를 참조하게 됩니다.
여기서 만약 다음과 같이 기존 변수의 값을 변경한다면 어떻게 될까요 ?
variable = variable + 1
=> 기존 variable
이 가리키던 메모리 주소가 아닌, 새로운 메모리 주소를 할당 받고, 그곳에 값을 넣게 됩니다.
=> 이는 JS에서 원시 타입은 값의 변경이 불가능하기 때문입니다. 특정 메모리에 값이 할당이 되면, 그 메모리에 다른 값은 할당이 불가능합니다. Garbage collector가 이를 해제한 후에야 가능합니다.
=> 즉, 원시 타입 변수의 값이 변경될 때는 항상 메모리가 새로 할당됩니다.
변수 및 함수 선언문이 스코프 내의 최상단으로 끌어올려지는 것처럼 동작하는 현상을 말합니다.
호이스팅이 발생하는 이유는 JS 엔진은 소스코드를 순차적으로 한줄 한줄 실행하기 전에 소스 코드를 평가하면서 변수와 함수 선언문을 먼저 실행하고, 이를 렉시컬 환경에 등록한 다음에 코드를 실행하기 때문입니다.
실제로 선언문이 코드 상에서 최상단으로 끌어올려지는 것은 아닙니다. 렉시컬 환경에 변수 및 함수가 등록되어 있기 때문에 코드 상에서 변수 선언문 이전에 변수를 참조하는 코드가 나와도, 해당 변수를 참조할 수 있으므로 최상단으로 끌어올려지는 것처럼 느끼는 것 뿐입니다.
let, const와 var을 통해 변수를 선언할 때 변수 선언의 3가지 단계를 거치는 부분에 차이가 발생하는데, var의 경우 1번과 2번이 동시에 진행되는데 let, const는 1번만 먼저 진행됩니다.
Execution Context의 생성 과정 중 첫 번째 페이즈인 Creation Phase에서
이를 좀 더 자세히 정리하면,
var 키워드로 선언한 변수는 선언 단계와 초기화 단계가 한번에 이뤄집니다. 즉, 스코프에 변수를 등록하고 메모리에 변수를 위한 공간을 확보한 후, undefined로 초기화합니다. 따라서 변수 선언문 이전에 변수에 접근하여도 스코프에 변수가 존재하기 때문에 참조 에러가 발생하지 않습니다. 다만 undefined를 반환한다. 이후 변수 할당문에 도달하면 비로소 값이 할당됩니다.
let 키워드로 선언된 변수는 선언 단계와 초기화 단계가 분리되어 진행됩니다. 즉, 스코프에 변수를 등록하지만 초기화 단계는 변수 선언문에 도달했을 때(코드 실행 후) 이뤄집니다.따라서 초기화 이전에 변수에 접근하려고 하면 참조 에러가 발생합니다. 이는 아직 변수가 초기화되지 않았기 때문입니다. 즉, 변수를 위한 메모리 공간이 아직 확보되지 않았기 때문입니다.
따라서 스코프의 시작 지점부터 초기화 시작 지점까지는 변수를 참조할 수 없습니다. 스코프의 시작 지점부터 초기화 시작 지점까지의 구간을 ‘일시적 사각지대(Temporal Dead Zone; TDZ)’라고 부릅니다.
=> 즉, let은 호이스팅 시 선언 단계만 진행됩니다.
렉시컬 환경이란 식별자-변수 맵핑 데이터 구조입니다. 여기서 식별자는 변수/함수의 이름을 참조하고, 변수는 실제 객체 [포함된 함수 객체] 또는 원시값을 참조합니다.
렉시컬 환경의 컨셉은 다음과 같습니다.
LexicalEnvironment = {
Identifier: <value>,
Identifier: <function object>
}
다시 말해, 렉시컬 환경은 프로그램이 실행되는 동안 변수와 함수가 존재하는 장소입니다.
undefined VS "초기화되지 않은 상태"
undefined
lexicalEnvironment = { a: undefined }
초기화되지 않은 상태
lexicalEnvironment = { a: <uninitialized> }
호이스팅 함수 선언 시 렉시컬 환경의 변화
helloWorld(); // 'Hello World!'가 콘솔에 찍힌다
function helloWorld(){
console.log('Hello World!');
}
컴파일 단계에서 함수 선언이 메모리에 추가된다는 것을 이미 알고 있기 때문에 실제 함수 선언 전에 우리 코드로 그 선언에 접근할 수 있습니다.
lexicalEnvironment = {
helloWorld: < func >
}
자바스크립트 엔진이 helloWorld()
호출을 접하게 되면, 렉시컬 환경 내부를 살펴보고 함수를 찾은 후에 실행 시킬 수 있습니다.
=> 하지만 함수 표현식은 var, let, const로 선언하기 때문에 호이스팅 되어도 undefiend
또는 uninitialized
상태로 렉시컬 환경에 저장되기 때문에 선언문 이전에 접근 시 에러가 발생합니다.
모던 자바스크립트 Deep Dive