JS_7. 스코프

Seoyong Lee·2021년 5월 4일
0

JavaScript / TypeScript

목록 보기
8/25
post-thumbnail
post-custom-banner

스코프의 정의

스코프(scope)는 변수에 접근할 수 있는 '범위'로, 크게 전역 스코프(Global scope)지역 스코프(Local scope)로 나누어진다. 또한 전역 스코프에서 선언된 변수를 전역 변수라고 하며, 지역 스코프에서 선언된 변수를 지역 변수라고 한다. 이러한 스코프가 중요한 이유는 바로 스코프의 적절한 설정을 통해 변수의 '생존 기간'을 제어할 수 있기 때문이다.

중첩 규칙

다음 예시를 통해 스코프의 작동 원리를 알아보자.

let user = 'lee';
if (user) {
 let message = `hello, ${user}!`;
 console.log(message); // 'hello, lee!'
}

console.log(message); // ReferenceError

if 문의 바디는 중괄호({})로 둘러싸여 있으며, 이러한 중괄호 안에 let을 이용해 message라는 변수가 선언되었다. 이렇게 if 문 내부에서 선언된 변수는 내부에선 문제없이 동작하지만, if 문 밖에서 호출하면 ReferenceError를 반환하는 것을 볼 수 있다. 그 이유가 무엇일까?

바로 변수의 접근 가능 범위가 다르기 때문이다. user 변수를 자세히 보면 if 문의 밖에서 선언된 것을 볼 수 있다. 이렇게 if 문 내부에서 let으로 선언된 message는 '블록 스코프'를 가지며, 외부 변수 user에 접근할 수 있다. 그러나 if 문 외부에서 '블록 스코프'를 가지는 내부 변수 message에 접근하려 하면 ReferenceError가 난다. 스코프는 이렇게 접근 가능한 범위를 정하는 기준이 된다.

블록 스코프와 함수 스코프

ES6에서 블록 스코프(block scope)let, const가 새롭게 등장하기 전까지 자바스크립트의 모든 스코프는 함수 스코프(function scope)였다. 이러한 구분은 스코프의 범위를 블록(중괄호{})을 기준으로 할 것인지, 함수를 기준으로 할지에 따라 정해진다.

함수 스코프와 var

var로 대표되는 전통적인 함수 스코프에선 함수 내부에서 선언된 변수는 함수 안에서만 유효하다. 이러한 함수 밖에서 선언된 변수는 모두 전역 스코프를 부여하는 것이 함수 스코프의 특징이다. 만약 동일한 이름의 변수가 함수의 내부와 외부에 동시에 선언된 경우, 내부에 선언된 변수를 먼저 참조한다. 또한 암묵적 전역(Implicit global) 원칙에 따라 선언되지 않은 변수를 사용해도 ReferenceError가 발생하지 않으며, 전역 객체인 window에 등록된다(window.message와 같은 형태로). 그렇다면 함수 스코프와 이를 사용하는 var는 어떠한 단점을 가지고 있었기에 새롭게 블록 스코프 방식이 추가된 것일까?

  • 전역 변수의 남발: 함수만을 스코프로 인정한다는 뜻은 반대로 생각하면 함수가 아닌 if, for 등의 중괄호 블록 내부에서 선언된 변수는 모두 전역 변수가 되어 블록 외부를 자유롭게 넘나든다는 뜻이다. 이러한 상황은 의도하지 않은 문제를 만들 수 있다.
  • 변수의 중복 선언 가능: var는 실수로 같은 이름의 변수를 중복 선언해도 오류를 내지 않기 때문에 다른 변수의 값을 의도치 않게 변경시킬 수 있다.
  • 호이스팅: 변수를 선언하기 이전에 참조할 수 있는 호이스팅은 이러한 변수의 통제를 더욱 어렵게 만든다.

이러한 문제들은 ES6에서 블록 스코프가 추가되는 계기가 되었다.

블록 스코프와 let, const

블록 스코프는 블록을 스코프의 기준으로 한다. 이러한 방식은 let, const의 등장과 함께 가능해졌으며, 변수를 사용하려는 사용처에 최대한 가까이 두고, 최소한의 유효범위를 가지는 것을 목적으로 한다. 기존에 변수 선언을 위해 사용되었던 var는 변수를 중복 선언할 수 있고, 선언된 변수가 호이스팅된다는 문제가 있었다. 그러나 letconst는 중복 선언이 불가능하고, 심지어 const는 선언과 동시에 값을 '상수화'하여 중복 할당도 불가능하다(그렇기에 const는 반드시 선언과 함께 값을 할당해 주어야 한다). 또한 호이스팅 과정이 다르다.

여기서 중요한 점은 let, const로 선언된 변수도 호이스팅 자체는 일어난다는 사실이다. 그러나 중요한 차이점이 있는데, 바로 변수 생성 과정이다. 일반적인 변수의 생성은 다음과 같이 3단계로 이루어진다.

  • 선언 단계(Declaration phase)
    변수를 실행 컨텍스트의 변수 객체(Variable Object)에 등록한다. 이 변수 객체는 스코프가 참조하는 대상이 된다.
  • 초기화 단계(Initialization phase)
    변수 객체(Variable Object)에 등록된 변수를 위한 공간을 메모리에 확보한다. 이 단계에서 변수는 undefined로 초기화된다.
  • 할당 단계(Assignment phase)
    undefined로 초기화된 변수에 실제 값을 할당한다.

var의 경우 선언과 초기화가 동시에 이루어지며, 따라서 선언 이전에 접근하여도 undefined라는 초기화된 값을 반환한다. 그러나 let, const의 경우 선언과 초기화가 분리되어 진행된다. 즉, 선언까지는 호이스팅 되지만, 실제 초기화는 변수 선언문에 도달해야 이루어진다. 따라서 초기화 이전에 변수에 접근하려 하면 레퍼런스 에러를 반환한다. 변수를 위한 메모리 공간이 확보되지도 않았기 때문이다. 따라서 letconst를 사용한 변수는 스코프의 시작점부터 초기화 지점까지 변수 참조가 불가능하다. 이러한 지점을 '일시적 사각지대(Temporal Dead Zone; TDZ)'라고 부른다.

그러나 이러한 방식이 호이스팅이 발생하지 않는 것과는 어떻게 다른 것일까? 다음 예제를 보면 letconst에서도 적어도 호이스팅 자체는 일어나고 있다는 것을 알 수 있다.

let foo = 1; // 전역 변수

{
  console.log(foo); // ReferenceError: foo is not defined
  let foo = 2; // 지역 변수
}

위 예제에서 foo는 전역 변수 1이 출력될 것 같이 보인다. 그러나 let으로 선언된 변수는 블록 스코프를 따르며, console.log의 foo는 지역 변수 foo 이기 때문에, 그리고 그 foo는 아직 초기화 지점을 만나지 못해 일시적 사각지대에 있으므로 레퍼런스 에러를 반환하고 있다. 만약 호이스팅이 아예 일어나지 않았다면, 뒤에 선언된 지역 변수는 고려되지 않은 채로 먼저 선언된 전역 변수 foo의 값 1이 반환되었을 것이다.

더 알아볼 내용

  • 클로저
  • 동적 스코프와 정적 스코프

참고
poiemaweb.com - let, const와 블록 레벨 스코프
MDN - let

profile
코드를 디자인하다
post-custom-banner

0개의 댓글