오늘 배운 내용들은 자바스크립트의 중요한 내용이기도 하고 면접에서도 많이 나오는 주제이기 때문에 평소보다 자세히 정리해보도록 하겠다!
꼼꼼히 정리하다보면 내가 완벽하게 이해했는지, 놓친 부분은 없는지 체크할 수 있을 것이다! 아자아자
🔎 호이스팅이란?
변수나 함수의 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트 고유의 특징
print();
function print() {
console.log("a");
}
놀랍게도 문제없이 잘 실행된다.
어라라? 함수가 선언되기도 전에 호출을 했는데 에러가 발생하지 않는다고? 자바스크립트에서는 그렇다..!
그렇다면 변수는 어떨까?
console.log(name);
var name = "minha";
출력 결과가 undefined가 나오기는 하지만 역시나 에러가 발생하지 않는다.
바로 자바스크립트의 특징인 호이스팅 때문이다. 위에 정의를 작성한 것처럼 선언문이 코드의 선두로 끌어올려진 것처럼 동작하는 자바스크립트의 특징을 바로 호이스팅이라 한다.
선언문이 코드의 선두로 올라왔으니, 변수를 참조하거나 함수를 호출할 때 에러가 발생하지 않는 것이다.
(변수 참조 시 undefined가 나온 이유는 이후에 설명하겠다.)
자바스크립트 엔진은 소스코드를 한 줄식 순차적으로 실행하기에 앞서 먼저 소스코드의 평가 과정을 거치면서 모든 선언문을 찾아내 먼저 실행하기 때문이다.
이 원리를 더 자세히 이해하기 위해서는 실행 컨텍스트를 알아야 한다.
🔎 실행 컨텍스트란?
자바스크립트 코드가 실행되는 공간(환경), 정확히는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체
자바스크립트 엔진은 자바스크립트 코드를 실행할 때 독립적인 공간에서 실행시키며, 그 공간을 실행 컨텍스트라고 이해하면 된다.
실행 컨텍스트는 코드를 실행하는 공간이라고 했으니, 생성되는 시점은 바로 코드가 실행되는 시점이다.
자바스립트 엔진은 자바스크립트 코드를 실행하기 위해서 실행 컨텍스트 하나를 만들게 된다.
그리고 생성된 실행 컨텍스트는 콜 스택이라 불리는 스택에 넣어두어 코드를 실행하게 된다.
🔎 콜 스택(call stack)이란?
자바스크립트 코드가 실행되는 동안 생성되는 실행 컨텍스트를 저장하는 자료구조
자바스크립트 엔진은 함수 호출이 발생할 때마다 콜 스택에 실행 컨텍스트를 쌓아두고, 끝나면 제거하는 방식으로 동작하며, 콜 스택이 빌 때까지 실행한다.
이때 제일 처음에 스택에 들어가는 실행 컨텍스트가 바로 전역 실행 컨텍스트이다.
즉, 모든 자바스크립트 프로그램은 항상 하나의 전역 실행 컨텍스트를 가지고 있는 것이다.
🔎 전역 실행 컨텍스트란?
자바스크립트에서 코드가 실행될 때 생성되는 기본적인 실행 컨텍스트로,
프로그램의 최상위 수준에서 실행되는 코드를 위한 컨텍스트
아래에서 더 자세히 설명할 예정이니, 간단히 실행 컨텍스트의 구성 요소만 보고 넘어가자.
record
: 변수 선언문을 기록해두는 객체 (Environment Record)outer
: 상위 컨텍스트의 연결 통로의 역할 (바깥 렉시컬 스코프를 참조하는 객체)🔎 스코프란?
식별자를 참조할 수 있는 범위 혹은 실행 컨텍스트가 영향을 미치는 범위
실행 컨텍스트 하나 당 하나의 스코프를 가지게 되며, 이를 렉시컬 스코프라 부른다.
그리고 전역 실행 컨텍스트에서의 유효한 범위를 전역 스코프라고 한다.
앞서 언급했듯이 record는 변수 선언문을 기록해두는 객체이다.
record는 2가지의 단계를 거쳐서 실행된다.
1. 생성 단계
현재 컨텍스트의 선언문을 환경 레코드에 기록하는 단계
코드를 실행하기 전, 코드 내에 있는 변수나 함수 등 선언하고 정의한 부분 중 선언문들만 따로 모아 기록하는 것이다.
컨텍스트 생성 단계에서 선언문을 기록한다는 것을 좀 더 자세히 설명해보겠다.
var name;
name = "minha";
위의 코드에서 선언문은 바로 var name;
이다.
선언문만을 찾고 있던 record는 이걸 보고 name이라는 이름의 변수 선언을 기록한다.
그렇다면 이렇게 변수가 선언됨과 동시에 할당되는 경우는 어떨까?
var name = "minha";
let age = 26;
const gender = "female";
이런 경우 변수의 선언과 할당을 분리한다.
var name;
name = "minha";
let age;
age = 26;
const gender;
gender = "female";
이렇게 보면 이해가 쉬울 것이다.
참고로 const는 이렇게 나누는 것이 불가능하지만 이해를 위해서 나누었다.
record는 이렇게 변수 선언문만 뽑아 기록한다.
미리 기록하는 이유는 메모리 공간을 확보하기 위해서라고 한다!
자 근데! 여기서 하나 더 짚고 넘어갈 것이 있다.
바로 변수 선언 키워드에 따라 기록하는 과정이 조금 다르다는 것이다.
< var
로 선언된 변수의 경우 >
record는 변수명을 기록하면서 해당 변수에 undefined
로 값을 초기화해주는 과정까지 진행한다.
그래서 아래의 코드에서 선언문 이전의 변수를 출력하면 undefined가 나온다.
console.log(name); // undefined
var name = "minha";
< let
과 const
로 선언된 변수의 경우 >
record는 변수명을 기록하면서 해당 변수를 초기화해주지 않는다.
초기화를 하지 않았으니, 아래의 코드에서는 선언문 이전의 변수를 출력할 때 에러가 발생한다.
console.log(name); // 에러 발생
console.log(age); // 에러 발생
let name = "minha";
const age = 26;
발생하는 에러 내용은 변수 is not defined
이 아닌, Cannot access 변수 before initialization
라는 점을 확인하자.
자바스크립트 엔진이 변수가 선언되었다는 것을 알고 있다는 증거이니까!
아무래도 호이스팅은 오류를 발생시킬 가능성이 높기 때문에 지양하는 것이 좋다고 한다.
이러한 이유 때문이라도 var
보다는 let
과 const
를 사용하는 것이 더 권장되는 방식이라고 한다.
함수를 정의하는 방식은 여러 가지가 있는데, 함수 정의를 기록하는 방식에서는 차이가 없을까?
하나씩 살펴보도록 하자.
< 함수 선언문 >
print(); // hello
// 함수 선언문
function print() {
console.log("hello");
}
함수 선언문은 말 그대로 선언문이다.
따라서 선언문을 열심히 찾아다니는 record는 함수 선언문 전체를 기록한다.
변수에서는 undefined로 초기화를 한다 안한다 이러지만,
함수 선언문에서는 그런거 없다! 그냥 저 내용 전부를 기록한다.
실행할 때 당연히 에러가 발생하지 않는다.
이미 실행 전부터 자바스크립트 엔진이 print라는 함수를 기록해두어 알고 있기 때문이다.
이런 현상이 바로 함수 호이스팅이다.
< 함수 표현식 >
print();
// 함수 표현식
const print = function() {
console.log("hello");
}
함수 표현식은 조금 다르다.
여기서 print
변수는 함수 정의를 참조하는 변수이기 때문에 record는 print
라는 변수 선언문을 기록한다.
참고로 여기서 발생하는 호이스팅은 함수 호이스팅이 아닌 변수 호이스팅이라는 점을 알아두면 좋을 듯 하다.
여기서 좀 더 자세히 살펴보면,
함수 표현식을 정의할 때 변수를 let
이나 const
로 선언했느냐, 아니면 var
로 선언했느냐에 따라 에러 메세지가 다르다는 것을 확인할 수 있다.
let
이나 const
의 경우, print가 초기화 되지 않았다는 에러 메세지가 출력된다.
이유는 위에 적었으니 넘기겠다.
var
의 경우, print는 함수가 아니라는 에러 메세지가 출력된다.
print는 undefined로 초기화 되었고, undefined는 함수가 아니기 때문에 ()
연산자를 사용할 수 없으므로 에러가 발생하는 것이다.
추가적으로 내가 헷갈렸던 부분이 과연 저 print 변수가 참조할 익명 함수는 record에 기록되느냐였다.
알아본 결과!! 함수 리터럴이기 때문에 값으로 취급하여 record에 기록하지 않는다고 한다.
아래의 화살표 함수도 동일하다.
< 화살표 함수 >
print();
// 화살표 함수
const print = () => {
console.log("hello");
}
화살표 함수도 함수 표현식과 동일하기 때문에 자세한 설명은 생략하도록 하겠다.
2. 실행 단계
생성 단계에서 기록된 환경 레코드를 참조하여 코드를 실행하거나 업데이트하는 단계
record가 더이상 기록할 게 없으면 실행 단계로 넘어간다.
실행할 때는 record에 기록된 것을 참조하면서 실행을 하게 된다.
🔎 일시적 사각지대란,
record가 변수를 기록하는 시점부터 초기화 단계에 접어들기 전까지의 구간으로,let
과const
에서만 발생한다.
실행하는 과정에서 let
과 const
는 할당문 혹은 선언문을 만나 그제서야 undefined로 초기화되고, 할당문은 값의 할당까지 진행된다.
let name = "minha" // 이 시점에 name 변수를 undefined로 초기화 -> minha 할당
let age; // 이 시점에 age 변수를 undefined로 초기화
이렇게 undefined로 초기화되기 직전까지를 일시적 사각지대라고 한다.
일시적 사각지대 구간에서 변수를 참조하게 되면 ReferenceError가 발생한다는 점을 알아두면 되겠다.
outer는 상위 컨텍스트의 연결 통로의 역할을 하는 객체이다.
다음과 같은 코드가 있다.
function print() {
console.log("a");
}
print();
console.log("b");
이 코드를 보고 실행 컨텍스트가 어떻게 구성될지 정리해보겠다.
- 코드 실행을 위해 전역 실행 컨텍스트가 생성된다.
- 전역 실행 컨텍스트의 생성 단계에서 record에 의해 print라는 함수 선언문이 기록된다.
- 더 이상 기록할 선언문이 없어 실행 단계로 넘어간다.
print();
로 적힌 함수 호출문을 만난다.- print 함수를 실행하기 위해 print 함수의 실행 컨텍스트가 생성된다.
자 여기까지만 보겠다.
위의 과정까지 진행했을 때 콜 스택은 아래와 같이 실행 콘텍스트가 쌓여 있을 것이다.
여기서 print 함수를 호출한 전역 실행 컨텍스트가 상위 컨텍스트로,
print 함수 전용 실행 컨텍스트가 하위 컨텍스트가 되겠다.
그리고 이 상위 컨텍스트와 하위 컨텍스트를 연결해주는 것이 바로 outer이다.
실행 컨텍스트는 실행 단계에서 필요한 변수나 함수가 해당 실행 컨텍스트의 record에 기록되어 있지 않을 때 연결 통로를 사용한다.
연결 통로를 통해 상위 컨텍스트로 건너가 상위 컨텍스트의 record에 필요한 내용이 있는지 확인하는 것이다.
만약 필요한 내용이 없다면 더 상위로, 더더 상위로 이동하며 필요한 내용을 발견할 때까지 혹은 상위 컨텍스트가 없는 전역 실행 컨텍스트에 도달할 때까지 찾는다.
단, outer는 하위에서 상위로만 이동할 수 있는 단방향 연결 통로이기 때문에 상위 컨텍스트는 하위 컨텍스트에 있는 record 기록을 확인할 수 없다.
🔎 스코프 체인이란?
바깥 렉시컬 스코프를 참조할 수 있는 현상
🔎 스코프 체이닝이란?
바깥 렉시컬 스코프를 참조해 나가는 것
위의 설명처럼 연결 통로를 통해 상위 컨텍스트로 이동해 원하는 변수나 함수를 찾아 참조하는 현상을 스코프 체인. 그리고 그 과정을 스코프 체이닝이라 한다.
아래의 코드를 기반으로 오늘 배운 내용을 흐름에 따라 정리해보겠다.
var name = "minha"
function print() {
console.log(name);
}
console.log(name);
print();
아까와 같이 단계별로 설명해보겠다.
- 코드 실행을 위해 전역 실행 컨텍스트가 생성된다.
- 전역 실행 컨텍스트의 생성 단계에서 record에 의해 name이라는 변수가 기록되며, undefined로 초기화가 된다.
- 전역 실행 컨텍스트의 생성 단계에서 record에 의해 print라는 함수 선언문이 기록된다.
- 더 이상 기록할 선언문이 없어 실행 단계로 넘어간다.
var name = "minha";
을 실행함으로써 name이라는 변수에 minha라는 문자가 할당된다.console.log(name);
을 실행한다.
여기서 잠깐! name이라는 변수를 찾아 출력해야 한다.
전역 실행 컨텍스트는 유효한 범위. 즉 스코프 내에서 name이라는 변수를 찾아보게 된다.
아까 2번에서 name이라는 변수를 record에 기록해두었기 때문에 이를 참조하여 문제없이 실행한다.
이어서 진행해보자.
print();
을 실행하기 위해 print 함수의 실행 컨텍스트가 생성된다.- print 함수 실행 컨텍스트의 생성 단계에서 record가 기록할 것이 없으므로 실행 단계로 넘어간다.
console.log(name);
을 실행한다.
아까와 같이 print 함수도 name이라는 변수를 찾아 출력해야 한다.
그러나 스코프 내에 name이라는 변수를 찾을 수가 없다.
print 함수의 실행 컨텍스트는 name이라는 변수를 찾기 위해 outer를 통해 상위 컨텍스트까지 범위를 넓히고 상위 컨텍스트인 전역 실행 컨텍스트의 record에 기록된 name을 참조하여 console.log를 보여주게 된다.
이후 단계는 이렇다.
- 코드 실행이 끝난 print 함수 실행 컨텍스트는 콜 스택에서 제거된다.
- 전역 실행 컨텍스트 역시 코드 실행이 종료되어 콜 스택에서 제거되며, 프로그램이 종료된다.