... 이 스코프가 아닌가...? 😆
이제 드디어 <모던 자바스크립트 Deep Dive>의 방대한 양을 뚫고(...) 제어문을 들어갈 차례군요!
그런데, 저는 제어문을 할 때 가장 1순위로 알아놓아야 할 것이 있다고 생각하는 개발자입니다.
그것은, 바로 스코프에요!
이 스코프를 제대로 알고 넘어가야, 추후에 나올 실행 컨텍스트, 클로저 등을 정확히 이해할 수 있기 때문입니다! 🚀
이 스코프라는 개념이 약간 이 책에서는 약하게 설명(15장)되어 있는 것 같아서, 저는 이것을 좀 더 톺아보려고 해요.
어차피 조금 늦어도, 나중에 더 빠른 추진력을 얻을 수 있으니, 천천히 톺아보도록 하죠! 🙇🏻♂️
참고로, 지금 내용은 <YOU DON'T KNOW JS>에서 나온 개념을 어느정도 풀어서 설명하는 겁니다! (이 책도 정말 추천드릴게요 🙆🏻)
쉽게 말해서 {}
를 의미한다고 생각하시면 될 것 같아요!
😉 그래, 이거 나도 잘 아는데!
그런데 말이죠, 우리의 이 자바스크립트 코드 안에는 보이지 않는 스코프가 존재한다는 것도 알고 계셨나요? 다음과 같이 말이죠!
// 이 코드는 전역 스페이스에서 실제로 동작합니다.
{
let i = 1;
}
{
let j = 2;
}
😮 응? 이것이 어떻게 가능하죠?!
이것은 일단, 사실 컴파일러의 과정을 어느정도 숙지한 다음 이해하시는 것이 좋습니다!
일단 먼저 이 이유를 간단히 말씀드리자면, 컴파일하는 과정 속에서 변수를 각각의 렉시컬 환경마다 등록을 시켜야하는데, 이는 스코프를 기반으로 변수의 유효한 영역을 관리하기 때문이에요. (나중에는 스코프 체인을 이용하여 검색합니다!)
일단 스코프를 이해하기 위해서는 컴파일이 중요해요. 결국 스코프는 어떻게 자바스크립트에서 구문이 해석되는가?가 포인트이기 때문입니다.
우리가 흔히 착각하는 것 중 하나가 자바스크립트 엔진은 인터프리터지, 컴파일러가 아니다!라는 것인데요. 놀라운 사실!
자바스크립트는 관점에 따라 인터프리터가 될 수도, 컴파일러가 될 수도 있다!
라는 것이에요! (사실 <YOU DON'T KNOW JS>도 너무나 컴파일러다!라는 의미가 세서 좀 아쉬웠어요.)
이 문제에 대한 논의가 큰 이유는, 아무래도 자바스크립트 엔진이 시간이 흐르며 진화했기 때문이에요.
따라서 이전의 자바스크립트 엔진을 보면 인터프리터의 성향이 짙은 것이 사실입니다.
그런데 최근 엔진들은 전통적이던 자바스크립트의 인터프리터 과정과 많이 다른 경향을 보여요.
대표적으로 V8
엔진의 경우에는 ignition
인터프리터로 바이트 코드로 변환시킨 다음, 이를 인라인 캐싱하여 turbofan
컴파일러를 통해 최적화시켜요. 이러한 방식을 JIT 컴파일러
라 합니다.
그리고 최근에는 이 과정에서 파생되는 비효율성을 개선하기 위해 sparkplug
라는 비최적화 컴파일러를 chrome 91
버전에서 사용하고 있죠! 앞으로도 성능 개선을 위해 이러한 컴파일 최적화는 더 진행될 것 같습니다 :) 링크
따라서 자바스크립트는 컴파일러의 성격도 가지고 있다!고 해석할 수 있겠네요. 다만 이 전체적인 과정이 보통 컴파일러와는 살짝 달라서, 이에 관해서 어떻게 바라보느냐에 대한 차이도 존재할 수 있는 것입니다!
이에 관해서는 조만간 또 글을 쓸 예정입니다!
(늘어나는 나의 블로그 포스트 부채... 🥰)
여튼! 최근의 자바스크립트는 완전한 인터프리터가 아니라 컴파일러의 특징도 갖고 있습니다.
이게 왜 중요하냐구요! 바로 컴파일러가 갖고 있는 다음 과정 때문입니다 😆
결국 우리가 작성한 코드는 고레벨 수준의 언어를 통해 작성된 것이고, 이는 기계어 수준의 저레벨로 변환이 되어야 하죠.
이때, 컴파일러는 다음과 같은 3단계를 거쳐 기계어 수준으로 번역하게 돼요.
Tokenizer
- 구문을 쪼개다.Lexer
- 그래서 이 친구, 의미가 뭐지...?Tokenizer
과 Lexer
의 과정을 합하여, 의미를 분석한다는 의미를 담아서, lexical analyze
라고 한답니다! Parser
- 해석하기 위해 정리해야지!결과적으로 이 3단계를 거쳐서, 컴퓨터에서는 실행 코드를 만들어내는 거죠.
🤬 아니 그래서 이 얘기를 왜 하냐구요! 저는 스코프가 뭔지 알려 왔다니까요!
결국 이 이야기를 하는 이유는, 저러한 과정 속에서, 스코프라는 아이가 컴파일 과정에서 의미를 파악하는 데 매우 중요한 기능을 하기 때문입니다! 😭
우리, 자기 자신을 어떻게 소개하는지를 떠올려봅시다.
보통 자신을 소개할 때, 가장 떠올리기 쉬운 케이스가 바로 지역을 기반으로 소개하는 것입니다.
저만 해도 이렇게, 다양하게 소개할 수 있어요!
- 안녕하세요! 지구에서 거주 중인 재영입니다.
- 안녕하세요! 한국에서 거주 중인 재영입니다.
- 안녕하세요! 서울에서 거주 중인 재영입니다.
...
그런데, 만약 재영
이 변수라면 어떻게 해야 할까요?
컴퓨터는 재영이라는 변수의 정의를 가장 잘 대답하기를 원해요.
보통 우리는 원하는 질문을 잘 대답할 때 문맥(context)를 보게 됩니다.
예컨대, 우주에서 저를 찾는다면, 그 넓은 우주 속에서 저라는 사람이 지구로부터 소개된 것을 전혀 듣지 못했으니 찾을 수 없겠죠? 우리는 이처럼 집합, 포함 관계를 바탕으로, 전체 상황을 분류하고, 이러한 문맥을 파악할 수가 있습니다.
이때, 컴퓨터 역시 마찬가지에요. 그 변수가 어디에서 등록된 것인지 확인하고, 이를 포함, 집합 관계를 따져서 문맥에 맞게 결과를 반환하는데요. 우리가 말하는 그 문맥이 바로 스코프라고 얘기할 수 있겠네요.
이를 코드로 표현하면 다음과 같습니다.
const 재영 = '지구에 거주 중인 개발자';
console.log('지구 스코프에서 소개하면?', 재영)
{
const 재영 = '한국에 거주 중인 개발자';
console.log('한국 스코프에서 소개하면?', 재영)
{
const 재영 = '서울에 거주 중인 개발자';
console.log('서울 스코프에서 소개하면?', 재영)
}
}
// '지구 스코프에서 소개하면?', '지구에 거주 중인 개발자'
// '한국 스코프에서 소개하면?', '한국에 거주 중인 개발자'
// '서울 스코프에서 소개하면?', '서울에 거주 중인 개발자'
우리가 환경에 의해 유연하게 우리 자신을 평가하듯, 코드에 살고 있는 변수들도 자신들이 살고 있는 환경을 토대로 정의가 돼요!
즉, 스코프는 변수들이 살고 있는 각자의 나라이자, 변수라는 존재를 보증할 울타리라고 볼 수 있겠네요! 그리고 자신의 스코프에서 찾지 못한다면, 자신이 속한 더 큰 스코프로 찾아가 검색하는 거죠.
그리고 초반에 설명했듯, 위처럼 스코프를 그냥 씌워줄 수도 있어요. 신기하죠?!
전역 스코프라는 개념도, 결국 보이지 않는 스코프가 존재한다는 것을 유추할 수 있죠? 😉
이 책에서는 var a = 2
라는 방식이 자바스크립트 컴파일 시 어떻게 동작하는지를 서술해주고 있네요. 같이 들여다 보죠! 👀
var a
- 너 지금 있는 애인가?YES
: 에이, 이건 선언문이 아니었군!(휙)NO
: 아, 그러면 너 등록해줄게. 잠깐만... (현재 스코프에 a 선언 요청)a = 2
- 엔진이 실행 가능한 코드를 생성해줘야겠군!현재 스코프 내에 변수가 있다
: 접근이 가능하네!현재 스코프 내에 변수가 없다
: 응? 얘 어디있는 거지... (상위 스코프 추적)즉, 우리가 이 코드에 담긴 변수를 선언하거나, 값을 참조할 때에는 항상 스코프를 기반으로 추적하게 돼요. 즉, 어느 스코프에 속해 있느냐에 따라 렉시컬 환경이 달라지는 것이고, 결과도 달라지겠죠?
따라서 스코프는 변수들을 쉽게 검색하기 위해 선언된 모든 변수들의 목록들이 유효한 범위를, 엄격하게 관리하는 규칙이라 할 수 있겠어요!
이러한 스코프의 종류에는 2가지가 있어요.
쉽게 말하자면 컴파일러가 렉싱할 때 그 위치가 결정되는 스코프죠!
렉시컬 스코프는 컴파일할 때, 선언된 위치를 기준으로 결정합니다. 이 친구는 호출이 어디에서 되었는지는 관심이 없어요.
따라서 아래의 예제는 true
로 동작합니다. 코드로 보는 게 이해가 빠를 것 같아요!
function foo() {
/*
* 1. result를 가져올 당시의 `result`를 검색 ->
* 2. 상위 스코프인 전역 스코프에 `var result`가 등록된 걸 발견!
* 3. true 출력
*/
console.log(result);
}
function bar() {
// result: bar 스코프 안에 등록된 변수로 선언
var result = false;
foo();
}
// result: 전역 스코프에 등록된 변수로 선언
var result = true;
bar(); // true
어렵지 않죠?!
즉, 컴파일 시점에 선언된 시점을 토대로, 정적으로 스코프의 위치를 결정하는 것입니다!
이러한 렉시컬 스코프는 또 2개로 나뉠 수 있어요. 바로 함수 스코프와 블록 스코프입니다.
쉽게 말해서, 함수로 선언되었을 때의 환경을 결정하는 스코프입니다.
var
은 이러한 함수 스코프를 기반으로 선언이 되고, 등록된답니다!
대표적인 예시는 반복문이겠죠!
for (let i = 0; i < 10; i += 1) {
...
}
이러한 스코프가 블록 스코프에요. 엄밀히 함수는 아니지만, 해당 로직이 현재의 스코프에서 분리되고 있다는 것을 구분하는 스코프죠!
let
과 const
로 선언된 변수는 이러한 블록 스코프를 기반으로 결정된답니다!
동적 스코프는 선언된 위치와 관계 없이, 호출된 위치에 따라서 스코프를 동적으로 생성합니다! 즉, 런타임 때 스코프의 위치가 결정되는 거에요!
function foo() {
console.log(result);
}
function bar() {
var result = false;
foo();
}
var result = true;
bar(); // true
원래였다면 함수는 렉시컬 스코프로 동작하므로 true
입니다.
하지만 만약 동적 스코프로 동작하게 된다면, foo()
는 함수가 호출된 위치에 따라 동적으로 결정될 거고... 그렇게 되면 false
가 되겠죠?!
이러한 방식으로 동작하는 스코프를 동적 스코프라고 해요.
모든 게 다 정적으로 결정되면 좋을텐데, 안타깝게도 자바스크립트에서 동적으로 호출시점에 따라 결정되는 문법이 존재합니다. 그것이 바로 this
죠! 😭
그렇다면 우리는 이제 스코프의 종류까지 알았으니, 도대체 스코프를 통해 어떻게 검색하는지를 알아야 하겠죠?
여기서 사용되는 것이 바로 <스코프 체인>입니다.
스코프 체인은 연결 리스트로 구성되어 있다고 합니다.
따라서 만약 해당 스코프에서 찾지 못하면, 상위 스코프로 접근하여 탐색하는 로직인 거죠!
예시를 들자면, 우리가 대학교를 다니는 대학생인데, 주민등록증을 대학교에서 발급할 수는 없잖아요?
따라서 <대학교> 스코프가 아닌, <OOO동 주민센터>라는 <OOO동>이라는 더 상위 영역으로 접근하여 문제를 해결하는 겁니다!
이렇게 스코프의 정의와 이를 빌미로 한 TMI, 그리고 스코프체인까지 살펴봤네요!
이러한 스코프 체이닝 방식이 결국에는 문법을 파싱하는 데 근간이 되므로, 꽤나 자바스크립트에는 상위의 영역으로 체이닝하는 방식이 상당히 많아요.
따라서 스코프의 개념을 미리 알면, 어느 정도 큰 개념들을 이해하는 데 상당한 도움이 됩니다.
저 역시 이 글을 쓰면서 좀 더 기초가 단단해진 것 같아 기분이 좋네요! 🙆🏻 이상!
YOU DON'T KNOW JS - CHAPTER 2. 스코프, 부록 B - 렉시컬과 스코프
논문 - 자바스크립트 적시 컴파일러를 위한 생성 코드 재사용