본문 바로가기
이론/Frontend

JavaScript의 스코프 - 1 (Scope of JavaScript)

by 유세지 2021. 4. 11.

이번 시간에는 자바스크립트의 스코프(Scope) 개념에 대해 알아보겠습니다.

스코프는 변수가 어디에 저장이 되는지, 이후에 이 변수를 어떻게 찾을지를 정하는 규칙입니다. 변수를 제대로 사용하려면, 어떻게 스코프가 정의되는지 그 규칙을 아는 것이 매우 중요합니다.

 

먼저 자바스크립트가 변수를 어떻게 다루는지부터 살펴보겠습니다.

 

컴파일러 이론

자바스크립트는 얼핏 보기엔 인터프리터 언어처럼 보이지만, 사실은 컴파일러 언어에 속합니다.

 

대중적인 오해와 달리, 
Javascript는 인터프리트 형태 자바가 아니다. 
간단히 말하면, Javascript는 프로토 타입 기반 객체 생성을 지원하는 동적 스크립트 언어이다.

- MDN 문서 'JavaScript에 대하여' 中

 

자바스크립트 엔진 중 하나인 라이노(Rhino) 엔진이 인터프리트 모드와 컴파일 모드를 함께 사용하는 특징을 가지고 있지만, 요즘 가장 많이 사용되는 대표적인 V8 엔진의 경우 두 개의 컴파일러로만 구성되어 있습니다.

 

컴파일러 언어의 특징은 소스 코드가 실행되기 전에 일련의 처리 과정을 거치는데 그 중 가장 먼저 실행되는 것이 토크나이징(Tokenizing)입니다. 토크나이징은 문자열을 의미를 가진 토큰 단위로 나누어 내는 과정입니다.

 

간단한 코드 하나를 토크나이징 시켜보겠습니다.

 

let test = 123;

 

위 코드는 이렇게 토크나이징 시킬 수 있습니다.

 

let, test, =, 123, ;

 

여기에 공백 또한 의미를 가진다면 토큰으로 나누어 질 수 있습니다.

 

 

다음은 파싱(Parsing)입니다. 위에서 나눈 토큰들을 문법 구조를 반영하여 중첩 원소를 갖는 트리 형태로 만들어내는 과정입니다. 이 결과로 만들어진 트리를 추상 구문 트리(Abstract Syntax Tree) 라고 부릅니다.

 

위의 코드의 경우 변수 선언이라는 최상위 노드에서 시작해 test를 확인자로 갖고, 대입 수식이라는 자식 노드를 갖습니다. 대입 수식은 다시 123이라는 숫자 리터럴을 자식 노드로 갖습니다.

 

 

마지막 단계는 코드 생성입니다. 파싱 작업으로 만든 추상 구문 트리를 실행 코드로 변환하는 작업입니다.

 

Franziska Hinkelmann: JavaScript engines - how do they even? (https://youtu.be/p-iiEDtpy6I)

 

V8 엔진을 예를 들어보면 인터프리터 이그니션에서 추상 구문 트리를 받아 바이트 코드로 전환하고, 일부는 컴파일러 터보팬으로 보내 최적화 된 기계 코드로 변환합니다. 이러한 과정을 코드 생성(code generation) 이라고 합니다. 구체적으로 어떻게 코드를 변환하는지까지는 우리가 살펴볼 범위를 벗어나기 때문에 넘어가도록 하겠습니다.

 

그럼 위에 예시로 적었던 코드를 컴파일러가 맞닥뜨렸을때 어떻게 접근하게 되는지 살펴보겠습니다.

 

let test = 123;

 

가장 먼저 컴파일러는 let test를 만나고, 변수 test가 스코프 컬렉션 안에 있는지 확인합니다. 만약 그렇다면 그냥 넘어가지만, 선언되어 있지 않다면 test라는 이름의 변수를 선언하기를 요청합니다.

 

다음으로 컴파일러는 대입문 test = 123을 처리할 수 있는 엔진을 위한 코드를 생성합니다. 코드가 생성이 되면 이제 엔진이 동작할 차례입니다. 엔진이 현재 스코프에서 test 변수가 접근 가능한지 확인하고, 가능하다면 변수를 사용합니다. 그렇지 않으면 중첩 스코프 부분을 확인하게 됩니다.

 

그렇게 스코프를 확인하고 난 뒤에도 변수를 찾을 수 없다면 에러를 발생시킵니다.

 

결국 변수 선언은 컴파일러가, 스코프에서 변수 탐색 및 대입은 엔진이 진행한다고 요약할 수 있겠네요.

 

 

LHS / RHS

 

스코프에서 변수를 탐색하는 방법에는 두 가지가 있는데, 각각 LHS 검색RHS 검색입니다.

 

LHS 검색은 Left-Hand Side의 약자로 대입되는 변수를 찾기 위해 사용하며, 변수의 값이 담긴 공간(컨테이너)를 찾습니다. 대입되는 변수는 보통 왼쪽에 위치하기 때문에 Left-Hand 라는 이름이 붙게 되었습니다. 위의 코드에서 변수 test를 탐색할때도 test에 값을 대입해야 하기때문에 LHS 검색이 사용됩니다.

 

RHS 검색은 Right-Hand Side의 약자입니다. LHS 검색과 반대로 값을 찾는 검색에서 사용됩니다. 다음과 같은 코드들이 모두 RHS 검색을 이용하는 예시입니다.

 

console.log(test);
add(num1, num2);
str = anotherstr; // str은 LHS, anotherstr은 RHS

 

LHS와 RHS를 구분하는 이유는 그 동작방식의 차이에도 있지만, 문제 상황에서 서로 가져오는 결과값이 다르기 때문이기도 합니다.

 

가령 선언되어있지 않은 변수를 검색하려고 시도했을때, LHS의 경우 최상위 스코프까지 검색을 마치고서도 찾을 수 없다면 해당 이름을 가진 글로벌 변수를 새로 하나 만들어줍니다. 선언하지 않은 변수에 값을 대입할 수 있는 이유도 이러한 엔진의 동작 때문입니다.

 

반대로 RHS의 경우 검색을 마치고서도 찾을 수 없다면 Reference Error를 출력하게 됩니다.

 

특수한 경우로, 엄격 모드(Strict Mode)를 적용하고 있다면 글로벌 변수를 암시적으로 생성할 수 없기 때문에 LHS 검색에 실패했을때도 Reference Error를 출력하게 된다는 점은 알아두시는 것이 좋습니다.

 

 

중첩 스코프

위쪽에서 엔진이 변수에 값을 대입할때 현재 스코프에서 변수를 검색하고, 찾지 못했다면 중첩 스코프를 찾는다고 이야기 했었습니다. 우리가 상위에서 선언된 변수나 함수를 사용할 수 있는 것처럼, 엔진은 중첩 스코프 검색을 통해 현재 스코프에 선언 되어있지 않은 변수라도 찾아낼 수 있습니다.

 

이미 알고 있는 사실처럼, 스코프도 블록처럼 다른 스코프 안쪽에 중첩될 수 있습니다. 따라서 엔진은 검색 과정에서 바깥쪽으로 하나씩 이동하며 최상위 스코프에 다다를때까지 검색을 계속합니다.

 

중첩 스코프에 대해 생각할때는 이 두 가지 원칙을 기억해두셔야 합니다.

 

 

 

 

 

오늘은 스코프가 무엇인지, 컴파일러와 엔진이 어떻게 코드를 받아들이는지에 대해 알아보았습니다.

다음 시간에는 또 다른 스코프들에 대해 알아보겠습니다.

반응형

댓글