스코프란 무엇인가

김연후·2022년 6월 11일
0
post-custom-banner

출처
You Don't Know JS (타입과 문법, 스코프와 클로저)

소개

프로그래밍 언어의 기본 패러다임 중 하나는 변수에 값을 저장하고 저장된 값을 가져다 쓰고 수정하는 것이다.

변수를 프로그램에 추가하면...

  • 변수는 어디에 저장될까?
  • 필요할 때 프로그램은 어떻게 변수를 찾아올까?

특정 장소에 변수를 저장하고 나중에 그 변수를 찾으려면 잘 정의된 규칙이 필요하다.
이런 규칙을 스코프(scope)라 한다.

1. 컴파일러 이론

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

하지만 자바스크립트는 전통적인 컴파일러 언어에서 컴파일러가 하는 일의 상당 부분을 세련된 방식으로 처리하기에 컴파일러 언어라 할 수 있다.

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

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

문자열을 나누어 토큰(token)이라는 조각으로 만드는 과정

  • var a=2; 라는 프로그램을 보자
    • var
    • a
    • =
    • 2
    • ;

빈칸은 하나의 토큰으로 남을 수도 있고 아닐 수도 있다. 빈칸이 의미가 있느냐 없느냐에 나뉨

파싱

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

var a=2; 의 AST
1. 변수 선언(Variable Declaration)이라 부르는 최상위 노드에서 시작
2. 최상위 노드는 'a'라는 값을 갖는 확인자(identifier)와 대입 수식(assignment expression)이라 부르는 자식 노드를 가짐
3. 대입 수식 노드는 '2'라는 값을 갖는 숫자 리터럴을 자식 노드로 가짐

코드 생성

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

언어에 따라, 목표하는 플랫폼에 따라 크게 다름

자바스크립트 엔진은 위 3가지 단계 외에도 많은 부분에서 다른 프로그래밍 언어의 컴파일러보다 훨씬 복잡하다. 예컨대, 자바스크립트 엔진은 파싱과 코드 생성 과정에서 불필요한 요소를 삭제하는 과정을 거쳐 실행 시 성능을 최적화 시킴

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

자바스크립트 컴파일레이션은 보통 코드가 실행되기 겨우 수백만분의 일초 전에 수행된다. 따라서 자바스크립트 엔진은 가능한 한 가장 빠른 성능을 위해 Lazy Compile, Hot Recompile 등의 트릭을 사용

정리하자면,

  • 어떤 자바스크립트 조각이라도 실행되려면 먼저 컴파일 되어야 한다.
  • 자바스크립트 컴파일러는 프로그램 var a=2; 를 받아 컴파일하여 바로 실행될 수 있게 함

2. 스코프 이해하기

  • 엔진
    • 컴파일레이션의 모든 과정과 자바스크립트 프로그램 실행 담당
  • 컴파일러
    • 파싱과 코드 생성의 모든 잡일을 도맡음
  • 스코프
    • 선언된 모든 변수(확인자) 검색 목록을 작성하고 유지함
    • 엄격한 규칙을 강제하여 현재 실행 코드에서 변수의 적용 방식을 정함

앞과 뒤

프로그램 var a=2; 를 보면 하나의 구문으로 보이나 엔진은 두 가지로 본다.

  • 컴파일러가 컴파일레이션 과정에서 처리할 구문
  • 실행 과정에서 엔진이 처리할 구문

먼저 컴파일러를 보면, 컴파일러는

  1. 렉싱을 통해 구문을 토큰으로 분할
  2. 토큰을 파싱해 트리구조로 만듬
  3. 코드 생성

컴파일러가 다음 의사코드로 요약될 수 있는 코드를 생성한다고 생각할 수 있다.

변수를 위해 메모리를 할당하고 할당된 메모리를 a라 명명한 후 그 변수에 값 2를 넣는다.

그러나 실제 코드 생성 과정에서 컴파일러는 다소 다른 방식으로 프로그램을 처리한다.

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

엔진이 마침내 변수를 찾으면 값 2를 할당하고, 못 찾는다면 에러를 발생시킬 것이다.

정리하자면

별개의 2가지 동작을 취하여 변수 대입문을 처리

  • 컴파일러가 변수를 선언 (현재 스코프에 미리 변수가 선언되지 않았으면)
  • 엔진이 스코프에서 변수를 찾고, 변수가 있다면 값 할당

컴파일러체

컴파일러가 스코프에서 변수에 대한 검색을 할 때 종류로 LHS와 RHS가 있다.

  • LHS는 값을 할당하기 위해 변수 컨테이너를 찾을 때 수행
  • RHS는 변수 값을 찾아올 때 수행
var a=2; // LHS
console.log(a); // RHS

코드의 실행과정 예시

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

foo(2);
  1. 엔진은 foo를 스코프에서 검색 (RHS)
  2. 스코프에는 컴파일러가 foo를 함수로 선언한 정보 존재
  3. 엔진은 foo를 가져와서 실행
  4. 엔진은 a를 스코프에서 검색 (LHS)
  5. 스코프에는 컴파일러가 a를 foo의 인자로 선언한 정보 존재
  6. 엔진은 2를 a에 대입
  7. 엔진은 console을 스코프에서 검색 (RHS)
  8. 스코프에는 내장함수인 console 정보 존재
  9. 엔진은 log를 마찬가지로 찾아보고 함수임을 인식
  10. 엔진은 a를 스코프에서 검색 (RHS)
  11. 스코프는 a에 2라는 값이 할당된 정보 존재
  12. 엔진은 2의 값을 log에 넘김

중첩스코프

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

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

찾는 변수를 현재 스코프에서 발견하지 못하면 엔진은 상위 스코프로 넘어가는 식으로 변수를 찾고, 찾지 못한다면 최상위 스코프인 글로벌 스코프에 도달할 때까지 계속된다.

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

var b = 2;
foo(2); // 4

b에 대한 RHS 참조는 함수 foo 안에서 처리할 수 없고, 함수가 포함 된 스코프에서 처리함

  1. 엔진은 foo의 스코프(함수 내부)에서 b를 검색 (RHS)
  2. foo의 스코프는 b 정보 없음 -> 엔진이 찾지 못했기에 상위 스코프에서 검색 (글로벌 스코프)
  3. 엔진은 글로벌 스코프에서 b를 검색 (RHS)
  4. 글로벌 스코프는 b에 대한 정보 존재

중첩 스코프를 탐사할 때 사용하는 규칙

  • 엔진은 현재 스코프에서 변수를 찾기 시작, 찾지 못하면 한 단계씩 올라감
  • 최상위인 글로벌 스코프에 도달하면 변수를 찾든, 못찾든 검색을 중지

오류

LHS와 RHS를 구분하는 이유는 두 종류의 검색 방식은 변수가 아직 선언되지 않았을 때
-> 즉, 모든 스코프에서 찾지 못했을 때 서로 다르게 동작하기 때문이다.

function foo(a) {
	console.log(a + b); // RHS 실패
    b = a;
}

foo(2);
// Uncaught ReferenceError: b is not defined

b에 대한 첫 RHS 검색이 실패하면 스코프에서 찾지 못한 변수이므로 선언되지 않은 변수가 된다.

선언되지 않은 변수를 사용하려 했기에 엔진은 ReferenceError를 발생시킨다.

function foo(a) {
    b = a; // LHS 실패
    console.log(a + b);
}

foo(2); 
// 정상 작동

반면 엔진이 LHS 검색을 수행하고 실패하면, 글로벌 스코프 조차 엔진이 검색하던 변수가 없는 것이므로 글로벌 스코프는 엔진이 검색하는 이름의 새로운 변수를 생성해 엔진에게 넘겨줌

ES5부터 지원하는 'Strict Mode'는 글로벌 변수를 자동, 암시적으로 생성하지 못하게 하므로 이 모드가 적용되어 있다면 마찬가지로 ReferenceError를 발생시킨다.

이외에도 RHS 검색으로 변수를 찾았으나 그 값을 갖고 불가능한 일 하려 할 때 다음과 같은 에러 발생

  • 함수가 아닌 값을 함수처럼 실행하거나 null, undefined 값을 참조할 때 엔진은 TypeError 발생

ReferenceError는 스코프에서 대상을 찾았는가?
TypeError는 스코프 검색은 성공했으나 결과값을 갖고 적합한 일을 하는가?

profile
개발 지식 공부
post-custom-banner

0개의 댓글