[JavaScript] 스코프
모던 자바스크립트 Deep Dive 스터디 5회차
자바스크립트 스터디를 진행하며 해당 회차에 공부했던 내용을 직접 그림을 그리고 코드를 실행하며 저의 글로 작성합니다.
스코프
스코프란 식별자가 유효한 범위다. 모든 식별자는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정된다.
자바스크립트에서는 var, let, const 어떤 키워드를 사용하느냐에 따라 스코프도 다르게 결정된다.
var a = 'a';
function foo(b) {
var a = "new a";
var c = "c";
console.log(a, b, c); // new a, b
}
console.log(a, b, c) // Reference Error (b, c)
위 예제 코드를 살펴보면 a 식별자는 전역 스코프를 가지며 b와 c 식별자는 함수 스코프를 가진다. 따라서 8행에서 b와 c를 참조 하려면 참조 에러가 발생한다.
자바스크립트 엔진은 스코프를 통해 어떤 식별자를 참조해야 할 지 결정한다. 따라서 스코프란 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙이라고 할 수 있다.
위 코드의 문맥을 살펴보자. a 변수는 전역 스코프에서 선언된 식별자이며 foo 함수 내부 변수인 b, a, c는 함수 스코프에서 선언된 식별자이다.
전역 스코프의 a변수와 함수 스코프의 a변수는 식별자가 같지만 스코프가 다르다. 그렇기 때문에 6행과 9행에서 모두 a라는 이름의 변수를 참조 했지만 다른 값이 출력되는 것이다.
전역 스코프, 지역 스코프
스코프는 전역 스코프와 지역 스코프가 존재한다. 지역 스코프는 보통 함수 몸체에 해당하며 전역 스코프는 지역 스코프가 아닌 모든 스코프이다. 전역 스코프에서 선언한 변수는 지역 스코프 어디서든 참조할 수 있다.
스코프는 실행 컨텍스트에 바인딩 된다. 실행 컨텍스트에는 자신의 스코프와 상위 레벨 스코프에 대한 정보를 담고 있다. 그래서 실행 컨텍스트가 상위 스코프를 기억하며 상위 스코프의 식별자를 참조할 수 있는 것이다.
스코프 체인
var a = "a";
function foo() {
var b = "b";
console.log(a, b); // a, b
function boo() {
var b = "new b";
var c = "c";
console.log(a, b); // a, new b
function poo() {
var b = "new new b";
var c = "new c";
var d = "d";
console.log(a, b, c, d) // a, new new b, new c, d
}
console.log(d); // Reference Error
}
console.log(a, b); // // a, b
}
console.log(b); // Reference Error
foo();
위 예문은 전역 스코프에 foo 함수가 선언되고, foo 함수 내부에 boo 함수가, boo 함수 내부에 poo 함수가 선언되며 스코프가 계층적 구조로 연결되며 스코프 체인을 이루고 있다.
19행은 boo 스코프를 가진다. 참조하는 d 변수는 boo 스코프에 존재하는 식별자이고 스코프를 벗어난 식별자이기 때문에 참조할 수 없고 참조 에러가 발생한다. 정확히는 자바스크립트 엔진이 스코프 내에서 d 식별자를 찾을 수 없기 때문에 에러가 발생하는 것이다.
자바스크립트 엔진은 변수를 참조할 때 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 상위 스코프 방향으로 이동하며 (스코프를 거슬러 올라가며) 선언된 변수를 검색한다.
함수 선언문으로 작성된 함수는 코드 평가 단계에서 메모리에 함수 객체가 생성이 되며 식별자가 바인딩 된다. 함수 객체도 식별자에 할당되기 때문에 함수도 스코프를 갖는다.
렉시컬 스코프
렉시컬 스코프는 정적 스코프로도 불리며 코드의 평가 시점에 함수가 어디서 작성 됐는지에 따라서 스코프를 결정한다.
var v = "global";
function foo() {
var v = "foo local";
boo();
}
function boo() {
console.log(v);
}
foo();
boo();
위 코드의 출력 결과를 예상해보자. 자바스크립트는 렉시컬 스코프(정적 스코프)를 가지며, 코드의 평가 시점(함수의 정의가 평가되는 시점)에 상위 스코프를 결정한다고 했다. (코드의 평가 시점에 함수 객체를 메모리에 올려놓는다.)
스코프의 관점에서 살펴보자. 코드의 평가 단계에서 전역 스코프가 생성이되며 v, foo, boo 식별자(변수와 함수)가 평가되며 전역 스코프에 생성이 된다. 이때 foo, boo함수의 상위 스코프는 전역 스코프이다.
13행에서 foo함수를 호출하고 내부에서 boo함수를 호출한다. boo함수를 호출했고 v 식별자를 참조하려고 하는데 지역 스코프에는 존재하지 않아 상위 스코프인 전역 스코프에서 검색을 통해 v 식별자를 참조하게 된다.
14행에서 boo함수를 호출할 경우 역시 상위 스코프인 전역 스코프에서 v 식별자를 참조한다.
함수가 어디에서 호출이 됐는지가 아닌 어디서 정의 됐는지에 따라서 스코프가 결정되기 때문에 모두 “global” 이라는 값을 출력한다.
렉시컬 스코프란 조금 어려운 개념이다. 하지만 자바스크립트는 코드의 평가와 실행 단계가 나눠져있고, 평가 단계에 스코프가 결정된다 라는 것을 생각하면 이해에 조금은 도움이 될 것이다.
렉시컬이라는 것은 어휘라는 것에 집중해보자. 앞으로 자바스크립트를 공부하면 많이 나오는 단어이기 때문에 꼭 이해하고 넘어가야 한다.
함수 스코프와 함수의 실행
스코프는 코드의 평가 단계에서 결정된다. 코드의 평가 단계에서 함수 객체가 메모리에 할당이 된다. 그럼 함수 내부에 있는 지역 스코프는 언제 생성이 될까?
코드 실행 단계에서 함수를 호출하면 함수 내부의 코드 평가가 시작된다. 이 과정에서 코드 평가 단계에서 발생하는 일들이 발생한다. 식별자의 선언문을 실행해 식별자를 메모리에 할당하고 undefined 값을 할당한다. 함수 내부에 선언된 함수 선언문은 함수 객체가 메모리에 올라가고 식별자가 바인딩 된다. 이 과정에서 함수 스코프가 결정된다.
함수 스코프는 함수가 종료되는 시점에 소멸된다.
foo();
function foo() {
var a = 1;
return;
}
코드의 평가 단계에서 전역 실행 컨텍스트가 생성되고 foo 함수 객체가 생성되어 렉시컬 환경에 바인딩 되고 실행 컨텍스트는 전역 스코프를 갖는다.
코드의 실행 단계에서 foo 함수가 실행되고 함수 코드 평가가 시작된다. 함수 실행 컨텍스트가 생성되고 a 변수가 렉시컬 환경에 바인딩되며 실행 컨텍스트는 foo 지역 스코프를 갖는다.
함수가 종료 되며 함수 실행 컨텍스트가 소멸된다. 실행 컨텍스트가 소멸되며 a 변수 또한 메모리에서 내려오게 된다.
다만 a 변수가 메모리에서 해제되는 시점은 알수가 없다. 이는 자바스크립트의 GC가 수행하는 일이기 때문이다.
만약 foo 함수가 종료가 되어도 함수 스코프에 등록된 식별자를 다른 스코프에서 참조하고 있다면 해당 실행 컨텍스트는 소멸되지 않는다. 이는 클로저라는 개념이다.
전역 스코프
전역 스코프는 자바스크립트 엔진이 코드를 평가하는 시점에 생성이 된다. 전역 스코프는 함수 스코프와 다르게 생명주기의 끝이 없다. 다시 말해서 브라우저가 닫히기 전까지 스코프가 소멸되지 않는다는 것 이다.
전역 스코프를 사용할 때 주의할 몇 가지 점들이 있다.
암묵적 결합
전역 변수는 어느 스코프에서든 참조할 수 있고 값을 변경할 수 있다. 이 말은 즉 어디서 사이드 이펙트가 발생할지 예측할 수 없다는 점이다. 또한 특정 스코프에서 전역 변수를 참조할 경우 해당 변수가 어느 스코프에서 참조가 됐는지 예측하기 어렵다. 그렇기 때문에 가급적 전역 변수는 변하지 않는 값으로 사용해야 한다. 가급적 전역 변수는 const 키워드를 사용해 재할당, 재선언을 할 수 없게 만들며 역할이 명확한 값 만을 사용해야 한다.
메모리 누수
전역변수의 생명주기는 프로그램이 종료할 때 소멸된다. 그렇기 때문에 많은 전역 변수를 사용하거나 아주 커다란 객체를 전역 변수로 사용한다면 메모리 누수가 발생할 수 있다.
스코프의 끝
전역 스코프는 가장 상위 계층에 위치한 스코프이다. 그렇기 때문에 스코프 체인에서 식별자를 검색할 때 가장 끝까지 검색해야 하는 점이 존재한다. 이 말은 검색 속도가 느리다는 점이다.
네임스페이스 오염
전역 변수는 가장 상위 스코프에 위치하기 때문에, 특정 스코프에서 var 키워드를 활용해 해당 식별자를 사용할 경우나 변수에 재할당을 한다면 예상치 못한 사이드 이펙트가 발생할 수 있다.