본문 바로가기

JavaScript

[ JavaScript ] 스코프와 호이스팅(feat. var let const)

반응형

1. 스코프 (Scope)

스코프는 참조 대상 식별자(변수)를 찾아내기 위한 규칙이다. 자바스크립트 엔진은 스코프 규칙에 따라 변수를 찾아 쓴다.

 

 

위 문장이 스코프의 일반적인 정의다. 변수는 선언 위치에 의해 스코프를 가지게 된다. 모든 변수는 크게 봤을때 전역, 코드 블록(if, for while, try/catch), 함수 안에서 선언된다. 따라서 스코프의 종류도 전역 스코프, 블록 레벨 스코프, 함수 레벨 스코프로 나뉜다. 그림으로 살펴보면 아래와 같다. 

 

 

  • 전역 스코프 : 전역에 변수를 선언하면 이 변수는 어디서든 참조할 수 있다. 
  • 함수 레벨 스코프 : 함수 내에서 선언된 변수는 함수 내에서만 유효하며 함수 외부에서는 참조할 수 없다.
  • 블록 레벨 스코프 : 코드 블록 내에서 선언된 변수는 코드 블록 내에서만 유효하며 코드 블록 외부에서는 참조할 수 없다. 

블록 레벨 스코프 함수 레벨 스코프를 묶어 지역 스코프라고도 한다. 지역 스코프 안에서 선언된 변수를 '지역 변수'라 부르고, 전역 스코프에서 선언된 변수를 '전역 변수'라고 한다. 그리고 크게 보면 함수 레벨 스코프는 블록 레벨 스코프이기도 하다. 그런데 이 둘을 구분하는 이유는 var 때문이다. 자바스크립트에서 var는 함수 레벨 스코프를 따르고, let과 const는 블록 레벨 스코프를 따른다. 아래 그림을 통해 이해해보자. 

 

  • (왼쪽 그림) - let은 블록 레벨 스코프이므로 모든 a는 독립적이다
  • (오른쪽 그림) - var는 함수 레벨 스코프이므로 Function의 a만 독립적이다. Block a는 전역 스코프다.

아래 더보기란을 클릭하면 let과 var의 차이를 알 수 있는 예제 코드가 있다. 

더보기
// let은 블록 레벨 스코프
let i = 0;
for(let i = 0; i < 5; i++){}
console.log(i); // 0

(function() {
  let i = 6;
}());
console.log(i); // 0


// var는 함수 레벨 스코프
var n = 0;
for(var n = 0; n < 5; n++){}
console.log(n); // 5

(function() {
  var n = 6;
}());
console.log(n); // 5

 

 

 

var와 let의 차이를 이해했다면, 이를 기반으로 아래의 네 가지 규칙만 숙지하고 있으면 된다. 

 

 

1) 내부 스코프는 외부 스코프를 참조할 수 있지만, 외부 스코프는 내부 스코프를 참조할 수 없다. 

더보기
let global = 'global';

function inner_1() {
  let inner1 = 'inner1';

  function inner_2() {
    let inner2 = 'inner2';
    console.log(global, inner1);
  }
  inner_2();

  // console.log(inner2); // ReferenceError: inner2 is not defined
}


inner_1();
// console.log(inner1, inner2); ReferenceError: inner1 is not defined

 

2) 외부에서 선언한 변수를 내부에서 재할당하면 외부변수가 전역변수처럼 작용하고, 내부에서 재선언하면 독립적인 변수가 된다. 

더보기
let x = 10;

function foo(){
  let x = 100;
  console.log(x); // 100

  function bar(){
    x = 1000;  // var x = 1000; 하면 외부와 다른 x
  }

  bar();
  console.log(x);  // bar 안에 x가 재선언, 재할당인지에 따라 달라짐
}

foo();
console.log(x); // 10

 

3) 중첩 스코프는 가장 인접한 지역을 우선하여 참조한다. 

참고로 이를 '스코프 체이닝'이라고 한다

더보기
let foo = function ( ) {

	let a = 3, b = 5;

	let bar = function ( ) {
	  let b = 7, c = 11;

	  console.log(a, b, c); // 두번째 출력
	  a += b + c;

	  console.log(a, b, c); // 세번째 출력
	};

	console.log(a, b); // 첫번째 출력
	bar();

	console.log(a, b); // 네번째 출력
};

foo();

/* 출력결과
3 5
3 7 11
21 7 11
21 5
*/

 

4) 함수는 함수 선언 시점에 상위 스코프가 결정된다. 함수를 어디에서 호출하였는지는 스코프 결정에 아무런 의미를 주지 않는다. 

 

let x = 1;

function foo() {
  let x = 10;
  bar(); // x = 10일 것 같지만 함수 선언 시점에서 x = 1 이므로 x = 1
}

function bar() { // bar 선언 시점
  console.log(x);
}

foo(); 
bar(); 

/* 출력결과
1
1
*/

 

함수는 선언 시점에 상위 스코프가 결정된다. 이를 좀 있어보이는 말로 렉시컬 스코프(lexical scope)라고 한다. 

 

2. 호이스팅 (Hoisting)

자바스크립트 엔진은 소스코드를 실행하기에 앞서, 변수 선언을 포함한 모든 선언문(var, let, const, function, class 등)을 찾아내 먼저 실행한다. 이를 좀 어렵게 '변수 선언은 런타임 이전에 실행된다'로 표현할 수 있다. 그리고 이와 동일한 표현이 '변수는 호이스팅 된다' 이다. 

 

호이스팅
: 모든 선언문이 해당 Scope의 선두로 옮겨진 것처럼 동작하는 특성을 말한다.

 

그러면 이렇게 생각할 수 있다. '모든 선언문은 호이스팅 되니깐 소스코드 내에서 선언문의 위치는 자유롭겠네?'. 반은 맞고 반은 틀렸다. 이 질문이 왜 틀렸는지를 이해하는게 우리의 목표다! 

변수 선언과 할당

변수는 크게 '변수 선언'과 '값 할당'으로 이루어진다. 선언과 할당을 코드로 구분하면 아래와 같다. 

 

let a; // 변수 선언
a = 1; // 값 할당

 

'변수 선언'은 다시 2단계로 나뉘는데, 절차는 아래와 같다.

  • 선언 단계: 변수 이름을 등록해서 자바스크립트 엔진에 변수의 존재를 알린다. 
  • 초기화 단계: 값을 저장하기 위한 메모리 공간을 확보하고 암묵적으로 undefined를 할당해 초기화한다.

호이스팅은 따지고 보면 '선언 단계'까지로 말할 수 있다. 왜냐하면 '선언 단계'만 거치면 자바스크립트 엔진이 변수의 존재를 알기 때문이다. 여기서 재밌는 사실은... 어떤 변수를 선언 단계 이전에 접근할 때 출력되는 오류와, 초기화 단계 이전에 접근할 때 출력되는 오류가 다르다는 사실... 이 오류를 활용하면 변수가 호이스팅 되는지를 검사할 수 있다. 

 

1) 선언 단계 이전 접근 오류

console.log(a);
// ReferenceError: a is not defined

 

 

2) 초기화 단계 이전 접근 오류

console.log(a);
let a;
// ReferenceError: Cannot access 'a' before initialization

 

변수 호이스팅

자바스크립트에는 세 종류의 변수 선언문이 존재한다. 바로 var, let, const이다. 세 종류 모두 호이스팅 되지만 동작 구조는 약간씩 다르다

 

 

1) var

 

var는 선언 단계와 초기화 단계가 동시에 이루어진다. 따라서 아래와 같은 코드를 실행시켜도 오류가 발생하지 않는다. 

console.log(a); // undefined
var a;

 

 

 

2) let

 

let은 선언 단계와 초기화 단계가 분리되어 진행된다. 선언 단계런타임 이전에 변수 선언문에서 이루어지지만, 초기화 단계런타임 이후에 변수 선언문에서 이루어진다.  

console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a; // 여기서 초기화 단계 실행. a에 undefined를 암묵적으로 할당
console.log(a); // undefined

 

만약에 호이스팅이 이루어지지 않았다면 첫째 줄에서 'not defined' 오류가 출력되어야 한다. 그런데 초기화 오류가 출력됐으므로 let은 호이스팅이 이루어졌다고 할 수 있다. 이렇게 선언 단계와 초기화 단계 사이의 구간을 일시적 사각지대(Temporal Dead Zone; TDZ) 라고 한다. 따라서 let은 Scope의 시작 위치에서 선언하는게 좋다. 

 

 

 

3) const

 

const는 선언과 동시에 초기화해야 한다. 그렇지 않으면 약간 다른 에러가 출력된다. 

const a; // SyntaxError: Missing initializer in const declaration
console.log(a);

 

ReferenceError가 아닌 SyntaxError임에 유의하자. 즉, 문법적으로 그냥 그렇게 정해진 것이다. 그러면 const의 호이스팅 여부를 판별해보자. 

 

console.log(a); // ReferenceError: Cannot access 'a' before initialization
const a = 1;

 

분명 초기화를 했는데 초기화 오류가 나고 있다. 이는 TDZ 때문인데, 문법을 지켜서 a의 존재가 자바스크립트 엔진에 등록은 됐지만, 초기화 단계는 런타임 이후 변수 선언문에서 이루어지기 때문이다. 따라서 let과 마찬가지로 const는 Scope의 시작 위치에서 선언하는게 좋다.

 

 

 

4) 추가적인 차이점

 

var는 재선언이 가능하지만, let은 재선언이 불가능하다. 또한 const는 재할당이 불가능하다. '='을 딱 한 번만 쓸 수 있다는 뜻이다. 원시값이 아닌 경우 값 변경은 가능하다는 뜻이다.

 

// 1. var는 재선언 가능
var a;
var a;


// 2. let은 재선언 불가능
let b;
let b; // SyntaxError: Identifier 'a' has already been declared


// 3. const는 재할당 불가능
const c = 1;
c = 2; // TypeError: Assignment to constant variable.

const d = [1,2,3,4];
d[2] = 11; // 가능

 

함수 호이스팅

1) 함수 선언문의 경우, 함수 선언 위치와 상관없이 Scope 내 어느 곳에서든지 호출 가능하다. 

console.log(square(5));

// 함수 선언문
function square(number) {
  return number * number;
}

 

따라서 함수 선언문의 위치는 Scope 내에서 자유롭다. 함수 호이스팅을 좀 어렵게 말하면 '함수 선언문은 생성 단계에서 함수명과 함수 전체가 저장된다' 라고 할 수 있다.  

 

 

2)  함수 표현식의 경우, 함수 호이스팅이 아니라 변수 호이스팅이 발생한다.

// 1. let으로 선언시 초기화 오류
f1(5); // ReferenceError: Cannot access 'square' before initialization

let f1 = function(number) {
  return number * number;
}


// 2. var로 선언시 '함수가 아님' 오류
f2(5); //TypeError: f2 is not a function

var f2 = function(number) {
  return number * number;
}

 

 

 

 

아래 질문에 스스로 답해보자. 대답을 잘 했으면 잘 이해한 것이다. 이상으로 이 글을 마친다.

 

모든 선언문은 호이스팅 되니깐 소스코드 내에서 선언문의 위치는 자유롭겠네?

 

반응형