호이스팅과 실행 컨텍스트

Sheryl Yun·2022년 1월 7일
1

Javascript 정복

목록 보기
4/9
post-thumbnail

출처: [10분 테코톡 - 하루의 실행 컨텍스트] https://www.youtube.com/watch?v=EWfujNzSUmw&list=RDCMUC-mOekGSesms0agFntnQang&index=6

실행 컨텍스트

자바스크립트 코드를 실행하는 데 필요한 환경 정보,
변수 객체, 스코프 체인, this에 관한 내용 등을 담고 있는 객체이다.

실행 컨텍스트는 크게 전역 컨텍스트와 함수 컨텍스트 2가지로 나뉘며,
자바스크립트 코드 실행이 시작되면 무조건 콜 스택에
전역 컨텍스트가 먼저 쌓이고, 그 위로 함수를 호출할 때마다 함수 컨텍스트가 생성된다.
각 컨텍스트가 생성될 때마다 실행에 필요한 환경 정보,
즉 변수 객체, 스코프 체인, this가 생성된다.
코드의 함수가 실행되면, 함수 안의 변수들이 함수 컨텍스트 안의 변수 객체에서 값을 찾고, 없다면 스코프 체인을 따라 올라가며 찾게 된다.
그리고 함수의 실행이 마무리되면 해당 함수 컨텍스트는 콜 스택에서 제거된다.
함수의 실행이 모두 끝나서 페이지가 종료되면 맨 아래 있던 전역 컨텍스트도 사라진다.


자바스크립트 엔진의 기본 작동 방식

코드가 실행되면 자바스크립트 엔진은 콜 스택(호출 스택)가장 먼저
전역 실행 컨텍스트(Global context)
를 담는다.

그리고 전역 공간에 있는 각 함수들이 호출되면
호출되는 순서대로 각 함수의 실행 컨텍스트들을 콜 스택에 추가한다.

아래는 전역 실행 컨텍스트(Global Execution Context) 위에
함수 A의 실행 컨텍스트가 쌓인 그림이다.
각 실행 컨텍스트마다 Record와 Outer가 있다.

콜 스택은 자료구조 '스택'과 구조가 동일하여
가장 나중에 추가된 맨 위의 실행 컨텍스트만 '활성화'되고
그 실행 컨텍스트가 종료되어 스택에서 제거되고 나면
다음 실행 컨텍스트를 활성화시키는 순서로 실행된다.

위의 예시 그림에서는
함수 A가 실행되면 실행 컨텍스트가 콜 스택에서 사라지고,
전역의 코드들이 실행되고 나면
전역 실행 컨텍스트가 콜 스택에서 사라진다.

1. Record로 JS 호이스팅 이해하기

Record를 가리키는 ECMA script의 정식 명칭은
'환경 레코드(Environment Record)'이며,

key: value 형태처럼
식별자와, 식별자에 바인딩된 값을 기록해두는 객체이다.
(식별자 = '변수' / 바인딩 = '할당'으로 읽어도 무방)

호이스팅

선언문이 최상단으로 끌어올려져서
선언 라인 이전에도 에러 없이 변수를 참조할 수 있는 현상이다.

주의: 실제로 코드가 끌어올려지는 것은 아니며, 자바스크립트 파서가 내부적으로 끌어올려서 처리한다.

호이스팅 발생 과정

호이스팅이 발생하는 과정은 두 단계가 있다.

1) 생성 단계 (Creation Phase)

  • 자바스크립트 엔진은 코드를 실행하면
    우선 전역 실행 컨텍스트를 가장 먼저 생성해서 콜 스택에 넣는다.

  • 그 후 전체 코드를 스캔한다.
    이 과정에서 선언할 식별자 변수가 있다면
    전역 실행 컨텍스트의 환경 레코드에 미리 선언해둔다.
    식별자 변수가 var 키워드로 선언되었다면 'undefined'로 초기화해두고,
    let/const라면 따로 초기화를 진행하지 않는다.

2) 실행 단계 (Execution Phase)

위에서 기록해둔 선언문 라인을 제외한 나머지 코드를 순차적으로 실행한다.
필요한 경우, 생성 단계에서 환경 레코드에 기록해 둔 식별자를 참조하거나
또는 값을 업데이트(재할당)한다.

호이스팅 종류

호이스팅의 종류에는 변수 호이스팅과 함수 호이스팅이 있다.

1. 변수 호이스팅 (var, let/const)

변수를 var로 선언한 경우

자바스크립트 엔진이 전체 코드를 스캔할 때
undefined라는 값을 할당하여 환경 레코드에 기록한다.

(위의 이미지: TVChannel 변수를 var 키워드로 선언하여 코드 스캔 시
undefined 값을 할당받아 전역 컨텍스트의 환경 레코드에 기록된 상태)

예시 코드 실행

  1. 첫 번째 줄: 스캔이 끝난 코드를 바로 출력하여
    전역 컨텍스트의 환경 레코드에 기록된 undefined를 그대로 출력한다.

    🙋‍♀️ 참고: console.log()도 log() 함수이기 때문에
    자바스크립트 엔진에 의해 호출되면
    console.log 함수에 대한 실행 컨텍스트가 생성된다.

  1. 두 번째 줄: 같은 변수(식별자)에 'Netflix'라는 값이 재할당되었다.

  2. 마지막 3번째 줄: 재할당된 값('Netflix')을 출력한다.


변수를 let 또는 const로 선언한 경우

자바스크립트 엔진이 환경 레코드에 식별자를 기록만 해두고
값을 초기화하지는 않는다.
(= undefined로 미리 특정 값을 할당하지 않는다)

때문에, 선언하기 이전에 식별자를 '참조'(= 출력 또는 사용)하려고 하면
Reference Error가 발생한다.

일시적 사각지대 (Temporal Dead Zone)
let이나 const로 선언하여
선언 라인 이전에 식별자를 참조할 수 없게 되는 구역

var와 let/const의 차이 추가 정리

var 키워드로 변수를 선언하는 경우에는
선언과 동시에 초기화가 이루어진다.

선언 단계에서는 메모리 공간을 확보한 뒤 메모리 주소에 식별자를 연결하고,
초기화 단계에서는 식별자를 암묵적으로 undefined 값으로 초기화한다.

변수를 let/const 키워드로 선언하면
선언 단계에서 메모리에 식별자는 연결해두지만,
값은 undefined로 초기화하지 않는다.

따라서 재할당 전까지는 변수에 아무 값이 담기지 않아서
선언만 했을 때는 값을 읽어올 수가 없다. (일시적 사각지대 발생)

추가 정보

let/const는 비교적 최근에 추가되었다.
이는 '선언문 이전에는 변수를 참조할 수 없다'는
일반적인 프로그래밍 방식을 자바스크립트도 추구하도록
'언어적 차원에서 보완된 것'이라 할 수 있다.

2. 함수 호이스팅

추가 출처: https://velog.io/@hyun-jii/%ED%95%A8%EC%88%98%EC%84%A0%EC%96%B8%EB%AC%B8-%ED%95%A8%EC%88%98%ED%91%9C%ED%98%84%EC%8B%9D%EA%B3%BC-%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85

자바스크립트에서는 함수를 변수에 담을 수 있다는 특징이 있다.
이를 함수 표현식이라고 한다.

함수 표현식(Function Expression)

함수를 변수에 담고 있기 때문에
기본적으로 변수 호이스팅과 똑같이 동작하지만
var의 경우에는 차이가 있다.

var 변수에 함수를 담아 선언문 이전에 실행하려고 하면
undefined가 뜨는 게 아니라 Type error가 뜬다.

(아래 예제 해설에서 추가 설명)

let/const 키워드의 함수 표현식은
그냥 변수 호이스팅의 경우와 똑같다.
(=> 아직 환경 레코드에서 기록된 값이 없어서 Reference error 발생)


함수 선언문 (Function Declaration)

var, let/const와 같은 키워드가 아닌
그냥 function 키워드로 함수를 선언하는 방식이다.

함수 선언문은 자바스크립트 엔진이 코드를 스캔할 때
완성된 함수 전체를 한 번에 환경 레코드에 기록해둔다.

그래서 코드 상의 어디에서 호출해도
이미 key 역할을 하는 식별자 함수가
그 함수의 value인 함수로 초기화되어 있기 때문에

에러가 발생하지 않는다.

이러한 특징 때문에 사용을 지양하려는 목소리도 있다.


함수 표현식 Quiz

다음 예제들의 결과를 생각해보자.

1번째 예제

count();

var count = function() {
    console.log('count는 1이다.');
}

2번째 예제

var count = function() {
    console.log('count는 1이다.');
}

count();

3번째 예제

count();

let count = function() {
    console.log('count는 1이다.');
}

.
.
.

첫번째 결과는 TypeError 이다.
var 는 호이스팅의 영향을 받으므로 위로 끌어올려진다.
그러므로 var count; 가 가장 먼저 실행되는데 undefined이다.
그 후로 count를 바로 '호출'하는데 호출은 함수 타입에만 가능하다.
하지만 undefined 데이터 타입은 함수가 아니므로 Type error를 발생시킨다.

var count;    // undefined

count();      // count는 함수가 아닌데 왜 함수를 호출하니?

var count = function() {
    console.log('count는 1이다.');
}

두번째 결과는 정상적으로 console.log가 동작한다.
var count가 호이스팅으로 인해 위로 끌어올려지지만,
count() 호출 전에 undefined 대신 함수를 재할당하므로
count를 '호출'하였을 때 함수임을 인식하여 정상 작동한다.

var count;    // undefined

var count = function() {  // count에 함수 재할당
    console.log('count는 1이다.');
}

count(); // 함수 호출 (정상 작동)

세번째 결과는 Reference Error 이다.
키워드가 let이어서 count 식별자의 값이 아직 정의되지 않은 상태이므로
호출이든 출력이든 어떤 형태로든 이 식별자를 '사용'하려고 하면
Reference Error가 발생하는 것이다.

count(); // 값이 없어...

let count = function() {
    console.log('count는 1이다.');
}

함수 선언문 Quiz

다음 예제들의 결과를 생각해보자.

count();

function count() {
    console.log('count는 2이다.');
}
function count() {
    console.log('count는 2이다.');
}

count();

.
.
.

첫번째 두번째 모두 정상 작동한다.

처음에 코드를 스캔할 때 함수 선언문은
완성된 상태로 환경 레코드에 기록
되기 때문에
호출이 함수 선언문의 위에 있든 아래쪽에 있든 상관없이 잘 작동하게 된다.

즉, 선언과 동시에 작성된 함수로 값도 초기화하여,
선언하기 이전에도 함수를 사용할 수 있는 것이다.

2. Outer로 JS 스코프체이닝 이해하기

Outer의 정식 명칭은 '외부 환경 참조(Outer Environment Reference)'이다.

실행 컨텍스트의 내부에
Lexical Environment(렉시컬 환경, 정적 환경)가 존재하고
그 안에 Record(환경 레코드)와 Outer(외부 환경 참조)가 들어 있다.

Outer는 내부 함수의 실행 컨텍스트를 실행할 때 참조할 값을
외부 함수의 실행 컨텍스트로 범위를 한 단계씩 넓혀 찾는 것이다.

위의 그림에서는 3층(가장 안쪽에 선언된 함수)에서 필요한 값을
2층(3층의 외부 함수)에서 찾아보고,
없으면 1층(전역 컨텍스트)까지 내려가면서 찾는다.

Outer를 통해 스코프 체이닝(Scope Chaining)이 일어나는 것이다.

스코프 체이닝 (Scope Chaining)
식별자를 찾기 위해 점점 범위(스코프)를 넓혀가며 찾는 과정
(cf. 스코프 체인: 이 과정에서 스코프들이 연결된 리스트)

식별자 결정 (Identifier Resolution)

코드 실행 시 특정 변수나 함수의 값을 결정하는 것이다.

처음에 전역 변수를 선언한 뒤 내부 함수에서 값을 재할당하면
코드 상에서 동일한 식별자가 여러 개가 된다.
이러한 상황에서 자바스크립트 엔진이 어느 값을 참조(사용)할지 결정하는 것이다.

3층 함수에서 lamp 변수의 값을 출력하는 코드가 있지만
3층 함수 내부에는 lamp 식별자가 없다.
그래서 lamp 식별자의 값을 찾기 위해
사다리를 하나 타고 내려간다. (바로 바깥의 외부 함수로 이동)
2층에는 lamp가 있고 값은 on이다.
그런데 1층에도 lamp가 있고, 다른 값이 할당되어 있다. (예: off)

이러한 경우 자바스크립트 엔진은 lamp의 값을 무엇으로 결정할까?

답은 '2층에서 값을 찾으면 1층까지 안 가고 바로 출력해버린다.'

자바스크립트 엔진은 lamp라는 변수의 값을 찾기 위해
한 칸씩 바깥 스코프로 이동하다가
lamp라는 식별자를 발견하면 바로 그 값을 출력한다.
더 아래로 내려가서 또다른 lamp가 있어도
어차피 같은 식별자이기 때문에 그 값은 인식하지 않는다.

변수 섀도잉 (Variable Shadowing)
동일한 식별자 선언으로 인해
더 상위 스코프(=더 아래층)에서 선언된 식별자의 값이 가려지는 현상

profile
데이터 분석가 준비 중입니다 (티스토리에 기록: https://cherylog.tistory.com/)

0개의 댓글