Programming/Javascript

[Javascript] You Don't Know JS (스코프)

알로그 2020. 4. 5. 23:49
반응형

Chapter1. 스코프란 무엇인가

1.1 컴파일러 이론

자바스크립트는 '동적' 또는 '인터프리터' 언어로 분류하나 사실은 '컴파일러 언어'다.
물론 많은 컴파일러 언어처럼 코드를 미리 컴파일하거나 컴파일한 결과를 분산 시스템에서 이용할 수 있는 것 은 아니다.

하지만 자바스크립트 엔진은 전통적인 컴파일러 언어에서 컴파일러가 하는 일의 상당 부분을 우리가 아는 것보다 세련된 방식으로 처리한다.
전통적인 컴파일러 언어의 처리과정에서 소스코드가 실행되기 전에 보통 3단계를 거치는데, 이를 컴파일레이션(Compilation) 이라고 한다.

 

토크나이징(Tokenizing)/렉싱(Lexing)

문자열을 토큰이라 불리는 의미있는 조각으로 만드는 과정이다.
'var a = 2;' 라는 프로그램은 다음과 같이 나눌 수 있다.
var, a, =, 2, ; 처럼 나눌 수 있고 빈칸은 의미가 있냐 없냐에 따라 토큰이 될 수도 있다.

 

파싱

토큰 배열을 프로그램 문법 구조를 반영하여 중첩 원소를 갖는 트리 형태로 바꾸는 과정이다.
파싱의 결과로 만들어진 트리를 AST(Abstract Syntax Tree)라 부른다.

'var a=2;'의 트리는 먼저 변수 선언이라 부르는 최상위 노드에서 시작하고, 최상위 노드는 'a' 값을 가지는 확인자와 대입 수식이라 부르는 자식 노드를 가진다.
대입 수식 노드는 '2'라는 값을 가지는 숫자 리터럴을 자식 노드로 가진다.

 

코드 생성

AST를 컴퓨터에서 실행 코드로 바꾸는 과정이다.
자바스크립트 엔진은 파싱과 코드 생성 과정에서 불필요한 요소를 삭제하는 과정을 거쳐 실행 시 성능을 최적화한다.
자바스크립트 엔진은 기존 컴파일러와 다르게 컴파일레이션을 미리 수행하지 않아서 최적화할 시간이 많지 않다.

 

1.2 스코프 이해하기

1.2.1 출연진

  • 엔진 : 컴파일레이션의 시작부터 끝까지 전 과정과 자바스크립트 프로그램의 실행을 책임진다.
  • 컴파일러 : 파싱과 코드 생성의 모든 잡일을 도맡아 한다.
  • 스코프 : 선언된 모든 변수 검색 목록을 작성하고 유지한다. 또한 엄격한 규칙을 강제하여 현재 실행 코드에서 변수의 적용 방식을 정한다.

 

1.2.2 앞과뒤

프로그램 'var a= 2;'를 보면 하나의 구문으로 보인다.
사실 엔진은 이를 두 개의 서로 다른 구문으로 본다.
하나는 컴파일러가 컴파일레이션 과정에서 처리할 구문이고, 다른 하나는 실행과정에서 엔진이 처리할 구문이다.

이 프로그램에서 컴파일러가 렉싱을 통해 구문을 토큰으로 쪼갠다.
그 후 토큰을 파싱해 트리 구조로 만든다.

  1. 컴파일러가 'var a'를 만나면 스코프에게 변수 a가 특정 스코프 컬렉션 안에 있는지 묻는다.
    변수 a가 이미 있다면 컴파일러는 무시하고 지나가고, 그렇지 않으면 컴파일러는 새로운 변수 a를 스코프 컬렉션 내에 선언하라고 요청한다.
  2. 그 후 컴파일러는 'a=2' 대입문을 처리하기 위해 나중에 엔진이 실행할 수 있는 코드를 생성한다.
    엔진이 실행하는 코드는 먼저 스코프에게 a라 부르는 변수가 현재 스코프 컬랙션 내에서 접근할 수 있는지 확인한다.
    가능하다면 엔진은 변수 a를 사용하고 아니라면 엔진은 다른 곳을 살핀다.

요약하면 별개의 두 가지 동작을 취하여 변수 대입문을 처리한다.
첫째, 컴파일러가 변수를 선언한다. (스코프에 변수가 정의되어 있지 않은 경우)
둘째, 엔진이 스코프에서 변수를 찾고 변수가 있다면 값을 대입한다.

 

1.2.3 컴파일러체

컴파일러가 생성한 코드를 실행할 때, 엔진은 변수 a가 선언된 적이 있는지 스코프에서 검색한다.
이때 대입 연산자의 왼쪽에 있을 때 수행하는 LHS 검색과 그 반대인 RHS 검색이 있다.

좀 더 쉽게 말하면 RHS는 'Retrieve his/her source'의 약자로 볼 수 있다.

console.log(a);

LHS 참조의 예제를 보자.

a = 2;

LHS와 RHS 참조를 모두 수행하는 프로그램을 살펴보자.

function foo(a){
    console.log(a); //2
}

foo(2);

foo() 함수를 호출하는데 RHS 참조를 사용한다.
인자로 2를 foo()에 넘겨줄 때, a에 2를 대입하는 연산이 일어날 때 LHS 검색이 수행된다.
console 객체에서 RHS 검색하여 log 메서드를 찾고, 첫 번째 인자를 LHS 검색으로 찾아 2를 대입할 것이다.

 

1.2.4 엔진과 스코프의 대화

function foo(a){
    console.log(a); //2
}

foo(2);
  • 엔진: 스코프, foo에 대한 RHS 참조가 필요해
  • 스코프 : 컴파일러가 선언했음, foo는 함수야
  • 엔진 : foo를 실행할게. a에 대한 LHS 참조도 필요해
  • 스코프 : 컴파일러가 a를 foo의 인자로 선언했어
  • 엔진 : 2를 a에 대입할게. console에 대한 RHS 검색이 필요해
  • 스코프 : console은 내장되어있어
  • 엔진 : log는 함수군, a의 RHS 참조를 찾아줘
  • 엔진 : log에 a 값 2를 넘긴다

 

1.2.5 퀴즈

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

var c = foo(2);

모든 LHS 검색을 찾아보라. (3개)
모든 RHS 검색을 찾아보라. (4개)

 

1.3 중첩스코프

스코프는 확인자 이름으로 변수를 찾기 위한 규칙의 집합이다.
보통 고려해야 할 스코프는 여러 개다.

현재 스코프에서 발견하지 못하면 엔진은 바깥의 스코프를 넘어가는 식으로 변수를 찾거나 글로벌 스코프라 부르는 가장 바깥 스코프에 도달할 때까지 계속 찾는다.

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

var b = 2;
foo(2);

엔진에서 foo의 스코프에서 b를 찾고 없다면 글로벌 스코프에서 찾아서 RHS 참조를 한다.

 

1.4 오류

LHS와 RHS를 구분하는 것이 왜 중요한가?
두 종류의 검색 방식은 아직 변수가 선언되지 않았을 때 서로 다르게 동작하기 떄문이다.

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

var b = 2;
foo(2);

b에 대한 첫 RHS 검색이 실패하면 선언되지 않은 변수라 한다.
RHS 검색이 중첩 스코프 안 어디에서도 변수를 찾지 못하면 엔진이 ReferenceError를 발생시킨다.

반면 엔진이 LHS 검색을 수행해서 변수를 찾지 못하면 글로벌 스코프는 엔진이 검색하는 이름을 가진 새로운 변수를 생성해서 엔진에게 넘겨준다.
Strict Mode에서는 RHS 경우와 비슷하게 ReferenceError를 발생시킨다.

 

1.5 정리하기

  • 스코프는 어디서 어떻게 변수를 찾는가를 결정하는 규칙의 집합이다.
  • LHS 참조는 대입 연산과정에서 일어난다.
  • LHS와 RHS 참조 검색은 모두 현재 실행중인 스코프에서 시작하고 상위 스코프로 넘어가며 확인자를 찾는다.
  • RHS 참조가 대상을 찾지 못하면 ReferenceError가 발생한다.
  • LHS 참조가 대상을 찾지 못하면 자동적, 암시적으로 글로벌 스코프에 같은 이름의 새로운 변수가 생성된다.
반응형