Scope | 스코프

스코프(Scope)란?

특정 장소에 변수를 저장하고 나중에 그 변수를 찾는데 잘 정의된 규칙을 스코프라고 한다.

컴파일러 이론

자바스크립트는 동적 또는 인터프리터 언어로 분류했으나 최근에는 컴파일러 언어로 불린다.

전통적인 컴파일러 언어 처럼 미리 컴파일하거나 컴파일한 결과를 분산 시스템에서 사용할 수 있는 것은 아니다.

컴파일러 언어 처리과정에서 프로그램 소스 코드가 실행되기 전에 일반적으로 3단계를 거친다. 이를 컴파일레이션이라고 한다.

토크나이징/렉싱

문자열을 나누어 토큰이라는 의미 있는 조각으로 만드는 과정이다.

var a = 1;은 -> var , a , = , 1 , ; 로 나눌 수 있다.

빈칸은 의미가 있냐 없냐에 따라 토큰이 될수도 있고 안될 수도 있다.

파싱

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

이 트리를 AST(Abstract Syntax Tree) 추상 구문 트리라 한다.

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

대입 수식 노드는 1이라는 값을 가진 숫자 리터럴을 자식 노드로 가진다.

var a
=
1

코드 생성

AST를 컴퓨터에서 실행 코드로 바꾸는 과정이다.

var a = 1 이라는 AST를 기계어 집합으로 바꾸어 실제로 a라는 변수를 담을 메모리를 확보하고 값을 저장하는 방법이 있다고 가정한다.

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

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

간단하게 말하자면 어떤 자바스크립트 조각이라도 실행되려면 바로 직전에 컴파일 되어야 한다.

스코프 이해하기

var a = 1 이라는 구문이 있다면 엔진에서는 2가지 구문으로 처리한다.

하나는 컴파일러가 컴파일레이션 과정에서 처리할 구문이고 다른 하나는 실행 과정에서 엔진이 처리할 구문이다.

컴파일러가 렉싱을 통해 구문을 토큰으로 쪼개는 것이다. 그 후 토큰을 파싱해 트리 구조로 만든다.

그리고 코드 생성 과정에 들어가면 컴파일러는 var a를 만나면 스코프에서 변수 a가 특정한 스코프 컬렉션에 안에 있는지 확인한다.

변수 a가 있다면 컴파일러는 선언을 무시하고 아니면 새로운 변수를 스코프 컬렉션 내에 선언하라고 요청한다.

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

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

가능하다면 엔진은 변수 a를 사용하고 아니면 엔진은 중첩 스코프 부분을 본다.

엔진이 변수 a를 찾으면 값 2를 넣고 못 찾으면 에러를 발생시킨다.

쉽게 말하자면 컴파일러가 변수를 선언한다. 그리고 엔진이 스코프에서 변수를 찾고 변수가 있다면 값을 대입한다.

컴파일러체

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

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

앞의 경우 엔진은 변수 a를 찾기 위해 LHS(Left-Hand Side) 왼쪽 방향 검색을 수행한다.

다른 종류의 검색을 RHS(Right-Hand Side) 오른쪽 방향 검색라고 한다.

방향은 대입 연산자의 방향을 의미한다.

LHS 검색은 변수가 대입 연산자의 왼쪽에 있을 때 수행하고 RHS 검색은 변수가 대입 연산자의 오른쪽에 있을 떄 수행한다.

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

LHS, RHS는 개념적으로 대입할 대상과 대입한 값으로 생각하는게 좋다.

function foo(a) {
console.log(a);
}
foo(1);

foo 함수를 호출하는데 RHS 참조를 사용한다. 그리고 실행을 한 후 foo에 넘겨줄 값 1을 인자 a에 대입하는 연산이 일어난다.

이 떄 대입에 대한 LHS 검색이 수행된다.

변수 a에 대한 RHS 참조 도 수행되는데 그 결과는 console.log 함수에 넘겨진다

console.log 함수도 실행되려면 참조가 필요하다. console 객체를 RHS 검색하여 log 메서드가 있는지 확인한다.

마지막으로 값 1을 RHS로 불러온 변수 a를 log의 내부 인자를 LHR 검색을 해서 1을 대입한다.

엔진과 스코프의 대화

function foo(a) {
console.log(a);
}
foo(1);

스코프에서 foo에 대한 RHS 참조를 찾는다.

찾으면 foo 인자로 선언한 a에 대한 LHS 검색을 한다.

찾으면 a에 1을 대입한다.

console 객체에 대한 LHS 검색을 한다. 그리고 log 메서드가 있는지 확인한다.

console.log 안에 있는 a에 대해 RHS 참조를 한다.

찾으면 그 값을 log에 넘겨준다. 그리고 log 안에 선언된 인자를 LHS 검색을 해서 대입을 한다.

중첩 스코프

스코프는 확인자 이름으로 변수를 찾기 위한 규칙의 집합이다.

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

따라서 대상 변수를 현재 스코프에서 찾지 못하면 엔진은 다음 바깥 스코프로 넘어가는 식으로 변수를 찾거나 글로벌 스코프라고 불리는 가장 바깥 스코프에 도달할떄까지 진행한다.

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

b에 대한 RHS 참조는 foo 함수 안에서 찾을 수 없고 함수를 포함한 스코프에서 처리한다.

중첩 스코프를 탐색할 때 사용하는 간단한 규칙이다.

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

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

오류

LHS와 RHS의 검색은 변수가 아직 선언되지 않았을때 서로 다르게 동작한다.

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

b에 대한 RHS 검색이 실패하면 다시는 b를 찾을 수 없다.

이렇게 스코프에서 찾지 못한 변수는 선언되지 않은 변수라 한다.

RHS 검색이 중첩 스코프 안 어디에서도 변수를 찾지 못하면 엔진이 ReferenceError를 발생시킨다.

하지만 LHS 검색을 수행하여 변수를 찾지 못하고 최상위 스코프까지 도착할때 strict mode가 아니라면 글로벌 스코프는 엔진이 검색하는 이름으로 새로운 변수를 만들어 엔진에게 넘겨준다.

ES5 부터 지원하는 strict mode는 글로벌 변수를 자동으로, 암시적으로 생성할 수 없다.

그래서 글로벌 변수를 생성하지 않고 RHS와 비슷하게 ReferenceError를 발생시킨다.

만약 RHS 검색을 해서 찾았지만 그 값으로 불가능한 것을 시도한다면 TypeError를 발생시킨다.

함수가 아닌 값을 실행하거나 null 이나 undefined를 참조할때 처럼이다.

ReferenceError는 스코프에서 대상을 찾았는지 이지만 TypeError는 스코프 검색은 성공했으나 결과값을 가지고 적합하지 않는 시도를 행한 경우 발생한다.

정리

스코프는 어디서 어떻게 변수를 찾는지 결정하는 규칙의 집합이다.

변수를 검색하는 이유는 변수에 값을 대입하거나 변수의 값을 찾기 위해서이다.

LHS 검색은 대입 연산 과정에서 일어난다. 스코프와 관련된 대입 연산은 = 연산자가 사용되거나 인자를 함수의 인자로 넘겨줄때 일어난다.

자바스크립트 엔진은 코드를 실행하기 전에 컴파일을 한다. 이 과정에서 var a = 1이라는 구문을 독립된 두 단계로 나눈다.

  1. var a는 변수 a를 해당 스코프에서 선언한다. 이 단계는 코드 실행 전에 처음부터 수행된다.

  2. a = 2는 변수 a를 찾아 값을 대입한다.

LHS, RHS 참조 검색은 모두 현재 실행중인 스코프에서 시작한다.

그리고 찾지 못하면 상위 스코프로 넘어가면서 찾는다.

이 작업은 글로벌 스코프에 이를때까지 진행하고 찾던 못찾던 작업을 중단한다.

RHS 참조가 대상을 못찾으면 ReferenceError를 발생시키고 LHS 참조가 대상을 찾지 못하면 strict mode가 아닌 경우 자동적,암시적으로 글로벌 스코프에 새로운 변수를 생성해서 전달한다.