Ch1 스코프란 무엇인가
- 특정 장소에 변수를 저장하고 나중에 그 변수를 찾는 데는 잘 정의된 규칙이 필요하다. 이런 규칙을 스코프라고 한다.
1.1 컴파일러 이론
- 자바스크립트는 사실 컴파일러 언어다.
- 전통적인 컴파일러 언어의 처리 과정에서는 프로그램을 이루는 소스 코드가 실행되기 전에 보통 3단계를 거치는데, 이를 '컴파일레이션' 이라고 한다.
토크나이징/렉싱
- 문자열을 나누어 '토큰'이라 불리는 의미 있는 조각으로 만드는 과정이다.
var a = 2;
// 위는 아래의 토큰으로 나눌 수 있다.
var,
a
=
2
;
- 토크나이징과 렉싱은 미묘한 차이가 있는데, 토큰을 인식할 때 무상태 방식으로 하는지 상태 유지 방식으로 하는지에 따라 구분한다. 토크나이저가 상태 유지 파싱 규칙을 적용해 a가 별개의 토큰인지 다른 토큰의 일부인지를 파악한다면 렉싱이다.
파싱
- 토큰 배열을 프로그램의 문법 구조를 반영하여 중첩 원소를 갖는 트리 형태로 바꾸는 과정이다. 파싱의 결과로 만들어진 트리를 AST (Abstract Syntax Tree)- 추상 구문 트리라고 부른다.
- var a = 2;의 경우에는 먼저 변수 선언이라 부르는 최상위 노드에서 시작하고, 최상위 노드는 'a'의 값을 가지는 확인자와 대입 수식이라 부르는 자식 노드를 가진다. 대입 수식 노드는 '2' 라는 값을 가지는 숫자 리터럴을 자식 노드로 가진다.
코드 생성
- AST를 컴퓨터에서 실행 코드로 바꾸는 과정이다.
- 자바스크립트 엔진은 파싱과 코드 생성 과정에서 불필요한 요소를 삭제하는 과정을 거쳐 실행 시 성능을 최적화한다.
- 자바스크립트는 컴파일레이션을 미리 수행하지 않아서 최적화할 시간이 많지 않다는 것이다.
- 결론은 어떤 자바스크립트 조각이라도 실행되려면 먼저 컴파일되어야 한다는 것이다.
1.2 스코프 이해하기
1.2.1 출연진
- 엔진: 컴파일레이션의 시작부터 끝까지 전 과정과 자바스크립트 프로그램 실행을 책임진다.
- 컴파일러: 엔진의 친구로, 파싱과 코드 생성의 모든 잡일을 도맡아 한다.
- 스코프: 엔진의 또 다른 친구로, 선언된 모든 확인자 검색 목록을 작성하고 유지한다. 또한, 엄격한 규칙을 강제하여 현재 실행 코드에서 확인자의 적용 방식을 정한다.
1.2.2 앞과 뒤
- 프로그램에서 컴파일러가 할 첫 번째 일은 렉싱을 통해 구문을 토큰으로 쪼개는 것이다. 그 후 토큰을 파싱해 트리 구조로 만든다.
- 컴파일러가 var a를 만나면 스코프에게 변수 a가 특정한 스코프 컬렉션 안에 있는지 묻는다. 변수 a가 이미 있다면 컴파일러는 선언을 무시하고 지나가고, 그렇지 않으면 컴파일러는 새로운 변수 a를 스코프 컬렉션 내에 선언하라고 요청한다.
- 그 후 컴파일러는 'a = 2' 대입문을 처리하기 위해 나중에 엔진이 실행할 수 있는 코드를 생성한다. 엔진이 실행하는 코드는 먼저 스코프에게 a라 부르는 변수가 현재 스코프 컬렉션 내에서 접근할 수 있는지 확인한다. 가능하다면 엔진은 변수 a를 사용하고, 아니라면 엔진은 다른 곳을 살핀다.
- 요약하자면 첫째로 컴파일러가 변수를 선언하고, 둘째로 엔진이 스코프에서 변수를 찾고 변수가 있다면 값을 대입한다.
1.2.3 컴파일러체
- 스코프에서 변수를 검색할 때 LHS 또는 RHS 검색을 수행한다.
- RHS 검색은 단순히 특정 변수의 값을 찾는 것과 다르지 않고, LHS 검색은 값을 넣어야 하므로 변수 컨테이너 자체를 찾는다.
console.log(a) // RHS 참조. 구문에서 a에 아무것도 대입하지 않는다. 단순히 a의 값을 가져와 함수의 인자로 넣어준다.
a = 2 // LHS 참조. 현재 a 값을 신경 쓰지 않고 대입 연산을 수행할 대상 변수를 찾기 때문이다.
function foo(a) {
console.log(a) // 2
}
foo(2)
- 함수 foo를 호출하는데 RHS 참조를 사용한다. (foo 값을 가져와야 함) 코드 속에 내재된 a = 2 라는 LHS 검색이 수행된다. 변수 a에 대한 RHS 참조도 수행되고, 그 결과 값은 console.log 함수에 넘겨진다. console.log도 RHS 참조가 필요하다. console 객체를 RHS 검색을 하기 때문이다. log 메서드 내부에 인자가 있을 것이고 arg1 = 2 로 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 객체에 대한 정보를 넘김
- 엔진: console 객체 내부의 log함수를 실행하려고 함
- 엔진: a의 RHS 참조가 필요하다.
- 엔진: 스코프에게서 받은 a의 값을 log함수에 인자로 넘긴다.
1.3 중첩 스코프
- 하나의 블록이나 함수는 다른 블록이나 함수 안에 중첩될 수 있으므로 스코프도 다른 스코프 안에 중첩될 수 있다. 대상 변수를 현재 스코프에서 발견하지 못하면 엔진은 다음 바깥의 스코프로 넘어가서 찾거나 글로벌 스코프라 부르는 가장 바깥 스코프에 도달할 때까지 계속 한다.
function foo(a) {
console.log(a + b)
}
var b = 2
foo(2) // 4
- b에 대한 RHS 참조는 함수 foo 안에서 처리할 수 없고, 함수를 포함하는 스코프에서 처리한다.
- 중첩 스코프 탐사할 때 사용하는 규칙
- 엔진은 현재 스코프에서 변수를 찾기 시작하고, 찾지 못하면 한 단계씩 올라간다.
- 최상위 글로벌 스코프에 도달하면 변수를 찾았든, 못 찾았든 검색을 멈춘다.
1.4 오류
- LHS, RHS 검색 방식은 변수가 아직 선언되지 않았을 때 서로 다르게 동작하기 때문에 반드시 구분해야 한다.
function foo(a) {
console.log(a + b)
}
var b = 2
foo(2) // 4
- b에 대한 첫 RHS 검색이 실패하면 다시는 b를 찾을 수 없다. RHS 검색이 중첩 스코프 안 어디에서도 변수를 찾지 못하면 엔진이 'ReferenceError'를 발생시킨다.
- 반면 엔진이 LHS 검색을 수행하여 변수를 찾지 못하고 최상위 층에 도착할 때 프로그램이 'strict mode'로 동작하고 있는 것이 아니라면, 글로벌 스코프는 엔진이 검색하는 이름을 가진 새로운 변수를 생성해서 엔진에게 넘겨준다. (결국 이렇게 되면 예상하지 못한 글로벌 변수를 생성하게 되는 것이다.)
- 'strict mode'로 동작할 경우는 RHS 검색과 마찬가지로 에러를 발생시킨다.