본문 바로가기
> 개발/Javascript

[Javascript] 변수의 스코프, 호이스팅, TDZ

by @일리 2023. 3. 14.

지난 번에 변수의 정의와 선언, 할당에 대한 글을 정리했었다. 이번에는 모던 자바스크립트 딥 다이브 책을 참고해서 변수 스코프를 공부해보고 설명할 예정이다.

 

[이전글]

 

[Javascript] 변수의 정의와 선언, 할당

오늘부터 틈틈이 모던 Deep Dive를 정독하면서 정리한 개념을 블로그에 올릴 예정이다. 개발 공부를 막 시작했을 땐 이 책을 보고 좌절했었는데, 개발 공부를 어느 정도하고서 책을 다시 보니 내용

dalsong-00.tistory.com


변수의 스코프

스코프(Scope)란 '유효 범위'라는 의미이다. 그러니 변수의 스코프는 변수의 유효한 범위를 의미할 것이다. 스코프를 이해하기 위해 다음 예제를 살펴보자.

 

모던 자바스크립트 Deep Dive 예제 13-02

console.log()의 결과 var1, var2, var3은 정상적으로 1, 2, 3 이라는 값이 나오고, var4와 var5는 ReferenceError가 나온다. 참조 에러는 변수가 함수 내에 선언되었을 경우 그 함수 내에서만 유효한 범위를 갖기 때문에 발생한다. 변수 외에 함수, 클래스 등 모든 식별자는 식별자가 선언된 위치에 따라 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정된다. 

 

스코프 예제2
모던 자바스크립트 Deep Dive 예제 13-03

 

이를 고려해서 예제를 보자. 함수 바깥과 안에 x라는 변수가 있다. 함수 foo를 호출했을 때 x는 local 값을 가지고, 함수 밖에서 x는 global이라는 값을 가진다. 같은 변수이지만 다른 결과가 나온 이유는 두 변수가 서로 다른 스코프를 갖기 때문이다. 이처럼 스코프가 다를 때에는 고유해야 하는 식별자가 충돌할 일이 없기 때문에 같은 이름의 식별자를 사용할 수 있다.

 

예제처럼 같은 이름의 변수가 있는 상황에서 자바스크립트 엔진이 어떤 변수를 참조할 것인지를 결정하는 것을 식별자 결정(identifier resolution)이라고 한다. 스코프를 통해 결정 과정이 이뤄지기 때문에 스코프를 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙이라고 하기도 한다.

 

'x'라는 같은 변수를 출력하려 할 때 console.log 의 위치에 따라 다른 결과가 나오는 것을 보면 알 수 있듯이 자바스크립트 엔진은 코드의 실행 위치와 주변에 어떤 코드가 있는지를 문맥(context)을 고려한다. 코드가 작성되고 선언된 환경을 렉시컬 환경(lexical environment)라고 부르고, 코드의 평가와 실행은 실행 컨텍스트(Execution Context)에 의해 이뤄진다.

스코프의 종류

스코프의 종류는 크게 전역 범위지역 범위로 나뉜다. 그리고 지역 범위는 함수 범위블록 범위로 또 나눌 수 있다. 변수는 자신이 선언된 위치가 전역인지 또는 지역인지에 따라 스코프가 정해진다. 전역에서 선언되었다면 전역 스코프를 갖고, 지역에서 선언되었다면 지역 스코프를 갖는다.

1. 전역 스코프

전역은 코드의 가장 바깥 영역이다. 가장 바깥 영역에 선언된 변수는 전역 스코프를 가지는 전역 변수가 된다. 이 변수는 어디서든 참조가 가능하다. 위의 예제에서 var1과 var x = 'global'이 전역변수이다.

2. 지역 스코프

지역은 함수의 몸체 내부를 의미한다. 함수 내부와 함수 안의 함수(중첩 함수)에 선언된 변수는 그 안에서만 참조될 수 있다. 위의 예제에서 var2와 var3와 var4, var5, 그리고 var x = 'local'은 함수 스코프를 가진다. 그런데 왜 var2와 var3은 블록 안에 갇혀있는데 전역 변수 var1처럼 제대로 2와 3을 출력할까? 그것은 var의 특성 때문인데 이것은 조금 뒤에 설명할 예정이다.

2-1. 함수 레벨 스코프

함수 레벨 스코프는 var 키워드로 선언된 변수만 해당된다. 오직 함수의 코드 블록만이 지역 스코프를 만든다.

2-2. 블록 레벨 스코프

블록 레벨 스코프는 함수를 포함한 모든 코드 블록(if, for, while, try~catch 등)에 의해 지역 스코프를 만든다.

 

처음에 함수 레벨 스코프와 블록 레벨 스코프의 차이가 이해가 가지 않았다. 지역 스코프를 만든다는 게 대체 무슨 말일까 고민하면서 한참 여러 글을 보다가 chat GPT에게 myFunction() 예제를 받고 이해했다. myFunction2는 myFunction 함수에서 키워드를 let으로만 바꾼 것이다.

스코프 예제4
블록 스코프를 이해해보자.

var 키워드를 사용했을 때는 if (true)라는 코드 블럭 속에 var y가 있는데도 코드 블럭 밖에서 y를 참조할 수 있다. 반면 let 키워드를 사용했을 때는 if (true)라는 코드 블럭 밖에서는 y를 참조할 수 없다. var 키워드를 사용할 때는 함수 코드 블럭 내에서 유효한 범위를 가진다. 반면 let과 const는 모든 코드 블럭에 의해 지역 범위가 생긴다. 

스코프 예제 5
블록 스코프를 이해해보자2.

var 키워드 예제를 새로 만들어봤다. 위에서 if (true)를 함수 블록으로 바꾸자 ReferenceError가 나타났다. var 키워드는 함수라는 울타리안에만 갇힌다고 이해하면 되겠다! 그 외의 코드 블럭은 var를 가둘 수 없다. 이제 변수의 스코프 예제에서 var2와 var3의 출력값으로 왜 2와 3이 나왔는지 이해가 간다. if (true)라는 코드 블럭만으로는 var를 가둘 수 없어서 var가 전역변수처럼 울타리 바깥으로 꺼내져 할당된 값을 제대로 출력할 수 있는 것이다! 굿굿!!

스코프 체인

하나의 함수가 있을 때 그 함수의 내부에서 함수가 정의되는 것을 '함수의 중첩'이라고 한다. 안에 있는 함수는 '중첩 함수(그림에서 inner 함수)', 그 함수를 감싸고 있는 함수는 '외부 함수(그림에서 outer함수)'이다. 함수가 중첩됨에 따라 함수의 지역 스코프도 중첩이 될 수 있다. 

스코프 예제 3
모던 자바스크립트 Deep Dive 그림 13-2

 

inner 스코프 < outer 스코프 < 전역 스코프로 스코프는 하나의 계층적 구조로 연결이 된다. 이러한 계층적 연결이 스코프 체인(Scope Chain)이다. 자바스크립트 엔진이 변수를 참조할 때, 변수를 참조하는 코드의 스코프에서 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다. 전역변수 x와 전역변수 y처럼 상위 스코프에서 선언한 변수는 outer()와 inner()와 같은 하위 스코프에서 참조할 수 있다. 반대로 하위 스코프에서 상위 스코프로는 참조할 수 없다.

스코프 체인

스코프 체인은 물리적으로 실재한다. 자바스크립트 엔진이 코드를 실행하기 전에 위쪽 그림과 유사한 자료구조인 렉시컬 환경을 실제로 생성한다. 변수 선언을 실행할 때 변수 식별자는 key로 등록이 되고, 변수 할당이 일어날 때 변수 식별자에 해당하는 값이 변경된다. 렉시컬 환경도 스코프처럼 전역 렉시컬 환경과 함수 렉시컬 환경으로 구분된다. 전역 렉시컬 환경은 코드가 로드되면 곧바로 생성이 되고, 함수의 렉시컬 환경은 함수가 호출될 때 생성된다. 

렉시컬 스코프 또는 정적 스코프

스코프 예제 6
모던 자바스크립트 Deep Dive 예제 13-09

위 예제의 실행 결과를 예상해보았다. 나는 10과 1일 줄 알았는데 정답이 1, 1이었다. 왜 틀렸을까? 함수의 상위 스코프를 함수를 호출한 곳 기준으로 생각했기 때문이다. foo() 함수를 호출했을 때 bar() 함수는 foo() 함수 내부에 있으니 var x = 10;일 거라고 생각했다. 이렇게 함수를 호출한 곳을 기준으로 상위 스코프를 결정하는 것을 '동적 스코프'라고 하고, 함수가 정의된 곳을 기준으로 상위 스코프를 결정하는 것을 '렉시컬 스코프' 또는 '정적 스코프'라고 한다.

 

전자가 동적 스코프로 불리는 이유는 함수가 정의된 곳은 정적으로 정해져있지만, 호출하는 곳은 여기저기에 있을 수 있기 때문이다. 예제의 결과가 1, 1이 나온 이유는 자바스크립트에서는 렉시컬 스코프를 따르기 때문이다. bar가 정의된 곳을 기준으로 상위 스코프는 전역 스코프이고, 그렇기에 var x = 1이라는 값이 나오는 것이다.

 

let, const 키워드

앞에서 var 키워드와 스코프를 집중적으로 학습해보았으니 이번에는 let과 const 키워드도 알아보자. 그 전에 var 키워드를 사용할 때 어떤 단점이 있기에 ES6에서 let과 const 키워드를 만들었는지 간략하게 살펴보자.

var 키워드의 문제점

1. 변수의 중복 선언이 가능하다.

2. 함수 외의 코드 블록에서 var 키워드로 선언한 변수는 전역 변수가 된다.

3. 변수 호이스팅 문제가 발생한다.

 

그렇다면 let과 const는 어떨까? 

let 키워드

1. 변수 중복 선언 불가

var 키워드를 사용해 변수를 선언할 때는 같은 이름의 변수를 여러 번 선언할 수 있었다. 하지만 let 키워드를 사용할 때는 불가능하다. 같은 이름의 변수를 선언하면 문법 에러(Syntax Error)가 발생한다.

 

let 키워드 예시
let 키워드는 재선언이 불가능하다.

2. 블록 레벨 스코프

let 예시 2
let 키워드는 블록 레벨 스코프를 갖는다.

 

let 키워드는 var와 달리 블록 레벨 스코프를 갖는다. 함수가 아닌 {블록}에도 갇힌다. 앞서 말했듯 if문, for문, while문, try~catch문을 지역 스코프로 인정한다.

3. 호이스팅

호이스팅은 선언부가 해당 스코프에서 맨 위로 이동되는 것처럼 보이는 자바스크립트의 동작을 의미한다. 호이스팅은 var 키워드 외에 let과 const에서도 일어나지만 let과 const 키워드에서는 마치 호이스팅이 발생하지 않는 것처럼 동작한다.

 

var 키워드로 선언한 변수는 런타임 이전에 자바스크립트 엔진에 의해 선언 단계와 초기화 단계가 한 번에 진행된다. 선언 단계에서 스코프(실행 컨텍스트의 렉시컬 환경)에 변수 식별자를 등록해서 자바스크립트 엔진에 변수의 존재를 알리고 즉시 초기화 단계가 실행되어 변수가 undefined로 초기화된다. 이미 스코프에 변수가 등록이 되어 있기 때문에 변수 선언문 이전에 변수에 접근을 해도 에러 없이 undefined가 나온다. 변수 할당문이 실행될 때 비로소 변수에 값이 할당된다.

 

아직 실행 컨텍스트가 무엇인지 몰라서 '스코프(실행 컨텍스트의 렉시컬 환경)에 변수 식별자를 등록' 이 부분이 이해가 안간다. 나중에 공부할 예정이니 일단 쭉쭉 진행해보자.

 

let 키워드는 선언과 초기화가 동시에 일어나는 var 키워드와 달리 선언 단계와 초기화 단계가 분리되어 진행된다. 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 선언 단계가 실행되는 것은 같지만, 변수 선언문에 도달했을 때에야 초기화 단계가 실행된다. 그래서 초기화 단계가 실행되기 전 변수에 접근을 하면 Reference Error가 발생한다. 여기서 변수 선언 단계와 초기화 단계 사이에 있는 변수를 참조할 수 없는 구간을 일시적 사각지대(Temporal Dead Zone : TDZ) 라고 부른다. 

let 과 호이스팅
let은 선언과 초기화가 분리되어 일어난다.
var와 호이스팅
var는 선언과 초기화가 동시에 진행된다.

 

위의 예시를 보면 var 키워드와 let 키워드의 선언과 초기화가 다르게 발생한다는 것을 알 수 있다. let 키워드에서는 선언이 호이스팅되었지만 초기화는 되지 않았기 때문에 초기화를 하기 전에는 변수 x 에 접근할 수 없다는 참조 에러가 발생하지만, var 키워드에서는 선언과 초기화가 동시에 일어났기 때문에 undefined가 출력된다.

const 키워드

const 키워드는 let과 비슷한 점이 많다. 그러니 let과 다른 점을 위주로 알아보자.

1. 재할당 금지

const 키워드는 재할당이 불가능하다. 값을 재할당하려고 할 때에는 Type Error가 발생한다.

const 재할당 금지!
const 키워드는 재할당이 불가능하다.

2. 선언과 초기화

 

const 키워드는 반드시 변수의 선언과 동시에 초기화를 해야 한다. var와 let에서는 변수 선언만 해주면 자바스크립트 엔진이 알아서 초기화를 해줬지만, const는 내가 직접 값까지 할당해줘야 한다.

const 오류
const는 선언과 동시에 초기화까지 직접 해줘야 한다.

정리

1. 오늘은 변수의 스코프, 즉 변수의 유효한 범위에 대해 알아보았다. 변수의 스코프는 전역 스코프와 지역 스코프로 나뉘고, 지역 스코프는 함수 스코프와 블록 스코프로 나뉜다. var 키워드는 함수 스코프를, let과 const는 블록 스코프를 기준으로 지역을 갖는다.

 

2. 같은 이름의 변수가 여러 개 있을 때 어떤 변수를 참조할 것인지 자바스크립트는 결정을 해야 한다. 이러한 결정을 식별자 결정이라고 부른다. 식별자 결정은 스코프 체인을 바탕으로 이뤄진다. 상위 스코프는 하위 스코프에서 참조될 수 있고, 하위 스코프는 상위 스코프에서 참조될 수 없다.

 

3. 자바스크립트에서는 렉시컬 스코프 또는 정적 스코프에 따른다. 그래서 함수가 정의된 곳을 기준으로 스코프를 참조한다. 

 

4. var, let, const 키워드를 비교했다. var 키워드에서는 변수의 선언과 동시에 초기화가 일어난다. let 키워드에서는 변수의 선언과 초기화가 분리되어 일어나며, 선언과 초기화 사이를 TDZ라고 부른다. const 키워드에서는 변수의 선언과 동시에 초기화를 해줘야 한다. undefined로 초기화되는 var 키워드와 달리 const 키워드에서 초기화는 선언 시점에 값을 할당하는 것으로 이해하면 된다. 


책에 쓰여진 글이 무슨 말일까를 고민해보고 글로 정리하는 데 시간이 꽤 오래 걸렸다. '뭔 소리지..' 싶은 구간이 좀 있었지만 계속 자료를 찾아보고 곱씹어보면서 이해할 수 있었다. 이전 글을 작성할 때는 let 키워드에서도 호이스팅이 일어난다는 말에 물음표를 띄웠지만 TDZ 개념을 배우고 변수의 선언과 초기화 단계가 어떻게 일어나는지 학습하면서 왜 let 키워드에서는 호이스팅이 일어나지 않는 것처럼 보이는지 알 수 있었다. 역시 작동 원리를 알아야 개념들이 더 잘 이해되는 법이다. 다음 글은 오늘 이해하지 못한 실행 컨텍스트를 주제로 작성해보도록 하겠당~!!

댓글