You Don't Know JS Yet - Scope & Closures 요약 및 감상 (1)변수
들어가며
전반적인 introduction에 해당하는 1권 이후 스터디를 진행하며 You Don't Know JS Yet 2권을 읽어보았습니다. 2권은 부제에서 확인할 수 있듯 scope(스코프)와 closure(클로저)에 대한 이야기가 중점인데, 특히 클로저를 설명하기까지 필요한 기본적인 개념과 그 동작 원리를 독자들이 차근차근 이해할 수 있게 구성한 점이 굉장히 좋았습니다. 굉장히 당연시하고 넘어갔던 변수 선언이나 스코프에 대한 것도 JS의 특징과 함께 잘 엮어서 설명해주고 있습니다.
많은 이야기가 오가는 책인만큼, 모든 걸 요약하는 것보다는 제가 인상 깊게 읽은 부분을 가볍게 정리해보고자 합니다. 크게 꼽아보자면 아래와 같습니다.
- var 와 let (+hoisting)
- 글로벌 객체 window
- Closure
- Module
그럼 이번 글에서는 책에서 소개하는 var, let의 특징을 정리해보겠습니다.
JS는 컴파일 언어?
JS를 검색해보면 흔히 인터프리터 언어로 정의되는 것을 발견할 수 있습니다. 하지만 이 책에서 저자는 JS를 컴파일 언어로 정의하고 있습니다. 그 이유는 실행 전에 파싱(parsing)과정이 분명히 존재하고, 에러가 발생하면 애초에 코드가 실행되지 않고 에러 메시지만을 보여주기 때문이라고 합니다 (인터프리터 언어라면 정상적인 코드까지는 실행이 되어야하겠죠?). 이에 대해서는 정해진 답이 있는 것은 아니지만, 저자가 관련 사항을 강조하는 이유는 스코프를 이야기하기 위해서라고 합니다. JS의 호이스팅(hoisting) 같은 동작을 살펴보면, 코드를 전체적으로 훑고나서 파싱을 하지 않고서는 절대 구현될 수가 없다는 것이죠.
lexical scope
C와 같은 프로그래밍 언어들은 모두 살펴보면 스코프(scope)의 개념이 있습니다. 말 그대로 영역인데, 이 영역이란 어떤 변수가 참조될 수 있는 범위를 의미합니다. lexical scope란 말 그대로 lexing 단계에서 구분지어지는 스코프인데, 즉 코드가 작성된 위치에 따라 스코프가 결정이 난다는 뜻이기도 합니다.
이런 스코프를 결정 짓는 것은 컴파일 단계에서입니다. 어떤 변수가 어떤 스코프에서 필요로 할지에 대해서 컴파일 단계에서 지정을 하지만, 아직 프로그램이 실행된 것은 아니기 때문에 실제로 메모리를 할당받는 것과는 관계가 없다고 합니다.
스코프는 보통 컴파일 단계에서 결정나지만, non-strict-mode라면 런타임 중 스코프를 바꿀 수 있는 eval, with 두 가지 함수가 있습니다. 다만 사용을 추천하지는 않는다고 합니다.
shadowing
쉐도잉은 같은 이름의 변수가 겹치는(혹은 내부) 스코프에서 등장하는 것을 의미합니다. 코드로 보면 좀 더 이해가 빠릅니다.
var studentName = "Suzy";
function printStudent(studentName) {
studentName = studentName.toUpperCase();
console.log(studentName);
}
printStudent("Frank");// FRANK
printStudent(studentName);// SUZY
console.log(studentName);// Suzy
전역변수 studentName과 printStudent의 인자로 작성한 studentName은 이름은 똑같지만 엄연히 다른 스코프에 존재하는 변수명입니다. 어렵게 생각할 것 없이, 이름이 겹치더라도 printStudent 안에서는 가장 가까운 스코프에 있는 studentName을 참조하게 됩니다. 감싸여있는 스코프에서 변수의 값을 찾을 때는 어차피 스코프 체인을 타고 올라가기 때문입니다. 다만, 이런 쉐도잉을 사용하면 안쪽 스코프에서는 절대 그 밖 스코프의 같은 변수명을 가진 변수를 접근할 수 없게 된다는 점이 있습니다.
var? let?
2권에서 가장 재밌게 읽은 부분은 var, let의 비교입니다. JS는 입문하면서부터 var를 사용하지말라는 이야기를 참 많이 들었고, 개인적으로도 eslint 등을 적용하면서 let, const 정도만 사용해왔습니다. 대체 var는 어쩌다 이런 악명이 생겨버린 걸까요?
우선 저자의 결론부터 이야기해보자면, 저자는 var가 절대로 망가지거나 오작동을 하는 것이 아니라는 걸 알아두었으면 좋겠다고 말합니다. 그저 let이나 const와 동작방식이 다를 뿐이고, 스코프가 다를 뿐, 제대로 이해하고 쓴다면 문제될 것이 전혀 없다는 겁니다. (물론 이 또한 저자의 의견일 뿐이며 예상치 못한 동작을 방지하기 위해 많이들 let으로 옮겨가고 있는 추세인 것도 사실입니다. 저자 또한 이것은 자신의 주관적인 이야기일 뿐이라고 거듭 강조합니다.)
이들의 스코프
간단하게 비교하면 var는 function scope, let(const도 자연히 포함하는 개념으로 이해해주시면 됩니다)은 block scope입니다. 그래서 var는 이런 재미난 현상이 발생합니다.
function example() {
let sum = 0;
for (var i = 0; i < 10; i++) {
sum += i;
}
console.log(i); // 10
return sum;
}
example(); // 45
비록 var가 for loop 안에서 선언되었지만, 함수 example 안에 있기 때문에 for loop 밖에서도 i는 참조가 가능합니다. 만약 저 var가 let으로 선언되었다면 오류가 발생합니다.
function example() {
let sum = 0;
for (let i = 0; i < 10; i++) {
sum += i;
}
console.log(i); // 10
return sum;
}
example(); // 45
ReferenceError: i is not defined
let으로 선언된 i는 for loop 스코프를 벗어나는 순간 접근할 수 없기 때문입니다.
hoisting
흔히 var와 함수 선언식은 hoisting 되는 것으로 잘 알려져있습니다. 주의할 부분은, 선언부가 위로 올라간다고 흔히들 설명하는데, 실제로 코드가 위로 끌어올려지는 것이 아니라, 이해를 돕기 위한 비유적인 설명입니다. 컴파일 단계에서 실제로 코드 위치가 바뀌지는 않습니다. 그저 필요한 변수와 함수를 파악하고 해당 스코프에 미리 지정해둡니다. 책에 나오는 예시는 이렇습니다.
studentName = "Suzy";
greeting();// Hello Suzy!
function greeting() {
console.log(`Hello ${ studentName }!`);
}
var studentName;
이 코드를 보면, greeting 함수와 studentName이 실제 참조되거나 호출되는 위치보다 아래쪽에 선언되어있지만 코드는 문제 없이 동작합니다. hoisting을 코드로 옮겨본다면 이런 느낌입니다.
function greeting() {
console.log(`Hello ${ studentName }!`);
}
var studentName;
studentName = "Suzy";
greeting();// Hello Suzy!
이렇게 hoisting된 var 변수는 할당이 이루어지기 전까지는 undefined입니다. 그렇지만 선언이 곧 undefined값을 할당하는 개념은 아닙니다. var는 여러번 같은 이름으로 선언하여도 아무런 변화가 생기지 않습니다.
var studentName = "Grace";
console.log(studentName); // Grace
var studentName;
console.log(studentName); // Grace (변화 없음)
var studentName = undefined;
console.log(studentName); // undefined
재선언은 실제 변수에는 어떠한 영향도 미치지 않습니다.
그렇다면 let은? TDZ 이해하기
let은 스코프 안에서도 선언되기 전의 지점에선 해당 변수를 참조할 수 없게 합니다. 스코프의 시작 지점부터 해당 변수가 선언되는 지점까지, 해당 변수가 참조될 수 없는 영역을 Temporal Dead Zone이라 부릅니다. 어찌보면 당연한 이야기지만, 책에서는 선언 전까지 해당 변수가 없다라는 개념으로 설명하지 않습니다. 저자는 let 또한 호이스팅이 분명 일어난다고 말합니다. 그래서 호이스팅 된 후 값이 선언되기 전까지의 시간적 간극을 TDZ라고 설명하고 있습니다.
studentName = "Suzy"; // TDZ에서 접근
// ReferenceError
console.log(studentName);
let studentName; // 선언은 여기
TDZ를 방지하는 가장 간단한 방법은 해당 스코프에서 필요한 선언을 먼저 작성하는 것입니다. 단순하죠?
그럼 let은 hoisting 되나요?
저자가 설명하길, let이 호이스팅인 이유는 auto-registration이 분명히 일어나기 때문이라고 합니다. 한마디로 스코프에서 선언된 let 변수는 컴파일 단계에서 해당 스코프에 '등록'은 된다는 의미입니다. 다만 아직 값이 초기화되지 않은 상태고, var는 이에 반해 등록 후 초기화까지 되는 auto-initialization의 개념인 것입니다. 책에선 auto-initialization, auto-registration 이 두 가지를 엄연히 분리하여 생각해야 한다고 이야기하며, 저자는 auto-registration을 호이스팅이라고 정의합니다. 그렇기에 let도 호이스팅은 되어있다고 말할 수 있는 것입니다.
정리하며
변수와 스코프를 지나면 클로저와 모듈에 대한 이야기가 등장합니다. 하지만 이 두 개념을 이해하기 위해선 저자가 설명하는 변수와 스코프를 이해하는 것이 중요합니다. 그 중에서도 제가 인상 깊게 읽었던 부분만을 요약했기 때문에 빠져있는 내용도 제법 많습니다. 위의 내용이 흥미롭게 다가오신다면 책을 읽어보시면 더욱 다양하고 특이한(?) 예시가 많이 소개되기 때문에 재밌게 읽으실 수 있을 것 같습니다.
막연하게 var를 절대 쓰지마라! 라는 이야기는 참 많이 들었지만, 구체적으로 어떤 점 때문인지는 책을 읽으면서 처음 자각해본 것 같습니다. 저자의 말처럼, 사실 정해진 방법은 없고 분명 누군가는 var을 절대 쓰지 않을 겁니다. 그래도 '망가진 기능'이라는 인식보다는 그 나름의 동작 방식을 이해해보고 연습문제를 풀 때 function scope의 변수라면 var, block scope라면 let을 이용해 코드를 작성해보았는데, 이렇게 하니 제가 작성하는 코드의 스코프에 대해 좀 더 인지하고 고민하게 되는 것 같습니다. 아마도 저는 앞으로는 계속 미연의 실수를 방지하기 위해 let 과 const에 의존하게 되겠지만, var를 만나게 되더라도 이제는 당황하지는 않겠죠? 😉