본문 바로가기

Javascript

[JavaScript] 스코프, Scope

반응형

 

⌗ 스코프, Scope
특정 상소에 변수를 저장하고 나중에 그 변수를 찾는데는 잘 정의된 규칙이 필요하는데 이를 스코프(Scope)라고 한다.
변수를 검색하는 이유는 변수에 값을 대입(LHS 참조)하거나 변수의 값을 얻어오기(RHS 참조) 위해서다.

자바스크립트는 일반적으로 '동적' 또는 '인터프리터' 언어로 분류하나, 사실은 '컴파일러 언어'다.

물론 자바스크립트가 전통적인 많은 컴파일러 언어처럼 코드를 미리 컴파일하거나, 컴파일한 결과를 분산 시스템에서 이용할 수 있는 것은 아니다.

 

전통적인 컴파일러 언어의 처리과정에서는 프로그램을 이루는 소스 코드가 실행되기 전에 보통 3단계를 거치는데,

이를 '컴파일레이션, Compilation'이라고 한다.

 

1 ) 토크나이징, Tokenizing / 렉싱, Lexing

  문자열을 나눠 '토큰'이라 불리는 의미있는 조각으로 만드는 과정이다.

  예시 ) var a = 2; ⇢ var, a, =, 2, ;

 

2 ) 파싱, parsing

  토큰 배열을 프로그램의 문법 구조를 반영하여 중첩 원소를 갖는 트리 형태로 바꾸는 과정이다.

  파싱의 결과로 만들어진 트리를 AST(추상 구문 트리, Abstract Syntax Tree)라 부른다.

 

3 ) 코드 생성, Code-Generation

  AST를 컴퓨터에서 실행 코드로 바꾸는 과정이다. 이부분은 사용하는 언어 혹은 목표하는 플랫폼에 따라 크게 달라진다.

 

자바스크립트 엔진은 이 3가지 단계뿐만 아니라 다른 프로그래밍 언어의 컴파일러보다 훨씬 복잡하다.

자바스크립트 엔진은 파싱과 코드 생성과정에서 불필요한 요소를 삭제하는 과정을 거쳐 실행시 성능을 최적화한다.

 

이때, 기존 컴파일러와 다른 점은 자바스크립트 컴파일레이션을 미리 수행하지 않아서 최적화할 시간이 많지 않다는 것이다.

자바스크립트 컴파일레이션은 보통 코드가 실행되기 겨우 수백만 분의 일초 전에 수행한다.

 

즉, 어떤 자바스크립트 조각이라도 실행되려면 먼저 컴파일되어야 한다는 것이다.

 

그러면 여기서 본론으로 들어가 다음의 구문은 자바스크립트 엔진에서 어떻게 처리하는 것일까?

" var a  = 2; "

 

처음에 컴파일러가 'var a'를 만나면 스코프에게 변수 a가 특정한 스코프 컬렉션 안에 있는지 문는다.

  변수 a가 이미 있다면 컴파일러는 선언을 무시하고 지나가고,

  그렇지않으면 컴파일러는 새로운 변수 a를 스코프 컬렉션 내에 선언하라고 요청한다.

 

그 후 컴파일러는 'a = 2' 대입문을 처리하기 위해 나중에 엔진이 실행할 수 있는 코드를 생성한다.

  엔진이 실행하는 코드는 먼저 스코프에게 a라 부르는 변수가 현재 스코프 컬렉션 내에서 접근할 수 있는지 확인한다.

  가능하다면 엔진은 변수 a를 사용하고, 아니라면 엔진은 다른 곳을 살핀다.

 

게다가 컴파일러가 생성한 코드를 실행할 때 엔진은 변수 a가 선언된 적이 있는지 스코프에서 검색한다.

  이때 엔진이 어떤 종류의 검색을 하느냐에 따라 검색 결과가 달라진다.

  위의 경우에서는 엔진은 변수 a를 찾기 위해 *LHS 검색을 수행한다.

    *LHS 검색: 변수가 대입 연산자의 왼족에 있을 때 수행함 (반대로는 RHS 검색으로 대입 연산자의 왼쪽이 아닌쪽에 있을 때 수행함)

 

즉, LHS 검색은 값을 넣어야하므로 변수 컨테이너 자체를 찾고, RHS 검색은 단순히 특정 변수의 값을 찾는다.

 

여기서 문제 !!

위 내용을 참고하여 아래의 예시를 보고 모든 LHS(혹은 RHS) 검색의 개수가 총 몇개인지 찾아보자.

 

function test(a) {
  var b = a;
  return a + b;
}

var c = test(2);

 

정답 :

 * LHS 개수 - 3개 : a=2 (암시적 인자대입) , b = ... , c = ...

 * RHS 개수 - 4개 : test(2) ,  = a; , a ... , ... b 

 

중첩 스코프


앞서 설명한 내용처럼 스코프는 확인자 이름으로 변수를 찾기위한 규칙의 집합이라고 말했다.

그러나 대개 고려해야할 스코프는 여러개다.

 

하나의 블록이나 함수는 다른 블록이나 함수 안에 중첩될 수 있으므로 스코프도 다른 스코프안에 중첩될 수 있다.

이때, 중첩 스코프를 탐사할 때 사용하는 규칙은 다음과 같다.

 

  * 엔진은 현재 스코프에서 변수를 찾기 시작하고, 찾지 못하면 한 단계씩 올라간다.

  * 최상위 글로벌 스코프에 도달하면 변수를 찾았든, 못 찾았든 검색을 멈춘다.

 

function test(a) {
  console.log(a + b);
}

var b = 2;

test (2); // 4

 

오류


LHS 검색과 RHS 검색의 구분이 왜 중요한 걸까?

이는 두 종류의 검색 방식은 아직 변수가 선언되지 않았을 때(검색한 모든 스코프에서 찾지 못했을 때) 서로 다르게 동작하기 때문이다.

 

function test(a) {
  console.log(a + b);
  b = a;
}

test(2);

 

위의 코드에서 b에 대한 첫 RHS 검색이 실패하면 다시는 b를 찾을 수 없다.

이렇게 스코프에서 찾지 못한 변수는 '선언되지 않은 변수'라 하여 엔진이 'ReferenceError'를 발생시킨다.

 

반면에 엔진이 LHS 검색을 수행하여 변수를 찾지 못하고 최상위 층(글로벌 스코프)에 도착할때 프로그램이 'Strict Mode'로 동작하고 있는 것이 아니라면, 글로벌 스코프는 엔진이 검색하는 이름을 가진 새로운 변수를 생성해서 엔진에게 넘겨준다.

 

ES5 부터 지원하는 'Strict Mode'는 Normal / Relaxed / Lazy Mode 와는 여러면에서 다르게 동작한다.
예를 들어, Strict Mode 에서는 글로벌 변수를 자동으로(또는 암시적으로) 생성할 수 없다.
그래서 글로벌 스코프는 변수를 생성하지 않아 LHS 검색에서는 아무것도 얻지 못하고, 엔진은 ReferenceError를 발생시킨다.

 

더나아가 RHS 검색 결과 변수를 찾았지만, 그 값을 가지고 불가능한 일을 하려할 경우에는 'TypeError'를 발생시킨다.

예시 ) 함수가 아닌 값을 함수처럼 실행하거나 null/undefined 값을 참조하는 경우

 

다시말해,

ReferenceError는 스코프에서 대상을 찾았는지와 관계있지만,

TypeError는 스코프 검색은 성공했으나 결과값을 가지고 적합하지 않거나 불가능한 시도를 한 경우를 의미한다.

 


[참고] You Don't Know JS 타입과 문법, 스코프와 클로저 - 카일 심슨

 

반응형

❥ CHATI Github