본 포스트는 자바스크립트 Closure, 실행 컨텍스트, 렉시컬 환경 등의 심화된 개념의 근간이 되는 스코프에 대한 포스트입니다.
var x = 'foo';
function foo() {
var x = 'bar';
bar();
}
function bar() {
console.log(x);
}
foo();
bar();
먼저, 위 코드의 실행 결과를 알기 위해서는 변수의 유효범위인 스코프에 대해 알아야 합니다. 스코프란 식별자가 유효한 범위라는 뜻으로, 선언된 각 식별자가 소스 코드 내에서 어디까지 유효한지를 정해주는 범위입니다. 여기서 유효하다라는 말이 조금 애매하게 들릴 수 있습니다. 식별자가 유효하다는 것은, 식별자를 식별자로서 사용할 수 있다는 뜻입니다. 내가 x라고 선언한 하나의 변수를 x라는 이름으로 부를 수 있다는 것이죠. 당연한 것 아닌가 하고 생각하실 수 있습니다. 하지만 익숙하면서도 유사한 환경을 예시로 들어 설명해보겠습니다.
바탕화면에 A라는 폴더와, my_file.txt라는 텍스트 파일을 만들었다고 가정해보겠습니다. 바탕화면에서 my_file.txt를 찾는다면 금방 찾을수 있지만, A라는 폴더 안에서 찾는다면 찾지 못할것입니다. 이번엔 A라는 폴더 안에도 my_file.txt라는 파일을 만들었습니다. 이 두개의 파일은 분명 서로 다른 파일입니다. 그리고 어디서 my_file.txt를 찾느냐에 따라 찾은 대상 파일이 다르겠죠. 식별자도 마찬가지입니다. 어디서 선언되었는지, 어디서 참조하는 지에 따라 유효할수도, 유효하지 않을 수도 있으며, 식별자가 중복되는 경우 어떤 대상이 선택될지도 달라집니다. 이것을 식별자의 유효범위, 스코프라고 부릅니다.
스코프가 무엇인지 이해는 했습니다. 이제 이 스코프가 어떤 규칙으로 생겨나는지 알아보겠습니다.
스코프가 지정되는 규칙은 크게 두 가지가 있습니다. 블록레벨 스코프와 함수레벨 스코프입니다.
블록레벨 스코프
식별자가 선언된 가장 안쪽 블록(중괄호로 둘러쌓인 영역)까지를 유효범위로 한다.
함수레벨 스코프
식별자가 선언된 함수 내부까지를 유효범위로 한다.
자바스크립트의 var 키워드는 함수레벨 스코프를 따릅니다. 식별자(쉬운 설명을 위해 앞으로 변수라고 부르겠습니다)가 선언된 함수 내부에서만 해당 변수를 참조할 수 있습니다. A폴더 안에 생성된 파일들은 A폴더 안에서만 찾을 수 있는것 처럼요.
이제 이 지식을 가지고 위 코드를 다시 보면서 실행 결과를 예측해 보겠습니다.
1 var x = 'foo';
2 function foo() {
3 var x = 'bar';
4 bar();
5 }
6
7 function bar() {
8 console.log(x);
9 }
10
11 foo();
12 bar();
1번 줄에 선언된 변수 x
의 유효범위는 어디까지 일까요? var
키워드로 선언된 변수는 함수레벨 스코프를 따르니까…x
가 선언된 “함수”는 어디일까요? 위 코드처럼 변수가 특정 함수 내부에서 선언된 것이 아니면, 전역 스코프를 가지게 됩니다. 전역 스코프는 말 그대로 코드 전체를 유효범위로 가지는 스코프입니다. 따라서 변수 x는 1번 줄부터 12번 줄까지 모든 라인에서 참조가 가능합니다. 이제 함수 foo
내부를 보겠습니다. x
라는 중복된 식별자로 변수가 하나 선언되었습니다. 이 x
는 함수 foo
스코프를 따르게 되어, foo
함수 안에서만 참조가 가능합니다. bar
에서는 선언된 변수가 없으니 우선은 스킵하겠습니다. 이제 11번 줄을 보겠습니다. foo
함수를 호출합니다. 다시 foo
함수를 찾아가서 보니, bar
함수를 호출하고 있습니다. 이제 bar
함수로 가겠습니다. console.log(x)
를 실행하네요. 이 x
는 어떤 x
일까요? 위에서 설명한 대로라면 변수는 함수 안에서만 유효한데, bar
함수를 호출한 위치가 foo
함수 안이니까.. foo
함수 스코프를 찾아봐야 할까요? 아니면 1번 줄에 전역으로 선언된 x
가 전역 스코프니까 전역 스코프를 찾아봐야 할까요? 둘 다 일리가 있는 말처럼 들리지 않나요? 여기서 중요한 개념이 나옵니다. 전자처럼 함수가 호출된 위치를 기준으로 스코프를 결정하는 방식을 동적 스코프, 후자처럼 함수가 정의된 위치를 기준으로 스코프를 결정하는 것을 정적 스코프, 다른 말로 렉시컬 스코프라고 합니다. 그리고 자바스크립트는 렉시컬 스코프 방식을 택하고 있습니다.
그래서 bar
함수 안에서 참조되는 모든 식별자들은 bar
함수가 정의된 위치를 기준으로 해당 식별자를 찾게 됩니다. 이제 bar
함수 안에 x
를 먼저 찾게 되는데, x
라는 변수를 찾지 못했습니다. 그럼 에러가 발생할까요? 아닙니다. 스코프는 계층 구조를 띠고 있어, 특정 변수가 참조된 함수 내부 스코프부터 그 위의 스코프까지 차례대로 찾게 됩니다. 아래 스코프를 찾는 일은 없습니다. 단방향 탐색인것이죠. 최상위 스코프는 당연히 전역 스코프입니다. 결론적으로 bar
함수에서 참조하는 x는 전역에 선언된 x
를 찾게 됩니다. 여기까지 이해가 되었다면 마지막 줄에 bar
함수 호출결과는 고민할 필요가 없습니다. 함수의 호출 위치는 상관이 없다고 했죠? bar
함수가 어디에서 호출되던 상관없이 동일한 과정을 거쳐 x를 찾기 때문에 동일한 결과가 나오게 되고, 이래서 정적인 스코프라고 부르는 것입니다. 변할일이 없기 때문이죠.
정리하면 위 코드의 실행 결과는 다음과 같게 됩니다.
foo
foo