스코프
그렇다고 추상적으로 존재하는 것이 아니라 실재로 스코프의 개념이 있고 물리적으로 존재합니다. 그것에 대해서는 나중에 자세히 알아보도록 하고 이번에는 스코프에 대해 알아보죠.
스코프
함수를 공부하면서 스코프란 말을 들어봤을 것입니다. 스코프(scope)는 범위라는 뜻을 가지고 있죠. 여기서도 비슷합니다. 어떤 것의 범위를 나타내는 것이 스코프입니다.
그렇다고 추상적으로 존재하는 것이 아니라 실재로 스코프의 개념이 있고 물리적으로 존재합니다. 그것에 대해서는 나중에 자세히 알아보도록 하고 이번에는 스코프에 대해 알아보죠.
스코프란
스코프는 식별자의 유효 범위라고 할 수 있습니다. 함수 포스트에서 봤듯이 함수 내부에서 선언한 변수는 외부에서 참조할 수 없었습니다. 이것이 스코프에 의한 것인데요. 변수와 같은 식별자들을 참조할 수 있는 범위를 말하는 것입니다.
스코프는 크게 전역과 지역으로 볼 수 있죠. 함수에서 잠깐 언급하긴 했습니다. 일단 전역은 코드의 가장 바깥부분을 말합니다. 지역은 함수 내부를 말하죠. 각 범위에 있는 변수를 각각 전역변수, 지역변수라고 합니다.
전역변수는 어디서든 참조가능하다는 특징이 있습니다. 가장 바깥에 있는 범위이기 때문입니다. 지역변수는 같은 스코프와 하위 스코프에서 참조가능하다는 특징이 있죠. 즉, 지역변수의 상위 스코프에서는 참조하지 못한다는 것을 반드시 기억하세요.
// 전역 스코프
var x = 1;
if(true){
var x1 = 2;
}
function foo(){
//함수 foo의 지역 스코프
var x3 = 3;
console.log(x3);
}
// 다시 전역스코프
console.log(x); // 1
console.log(x1); // 2
foo(); // 3
console.log(x3); // Reference Error: x3 is not defined
위 코드를 보면 x와 x1의 경우 출력이 되지만, x3의 경우 참조 에러가 발생하는 것을 볼 수 있습니다. x와 x1은 전역 스코프에 있는 전역 변수이지만, x3는 함수 foo에 있는 지역변수이기 때문이죠. 그래서 foo()를 실행했을 때는 3을 출력하지만 전역에서 참조하려고 하면 오류가 나는 것입니다.
스코프의 중첩
위에서 지역 변수는 같은 스코프 또는 하위의 스코프에서 접근이 가능하다고 했습니다. 그렇다면 하위 스코프는 무엇일까요? 바로 스코프가 중첩되는 경우, 즉 함수가 중첩된 경우라고 할 수 있습니다.
function outer(){
var outVar = 'outer';
function inner(){
var inVar = 'inner';
console.log(outVar); // 'outer'
console.log(inVar); // 'inner'
}
inner();
console.log(outVar); // 'outer'
console.log(inVar); // ReferenceError: inVar is not defined
}
중첩된 함수를 보면 inner함수에서는 outer함수의 변수인 outVar를 참조하여 'outer'를 출력할 수 있습니다. 또한,outer함수에서도 outVar를 참조하여 출력할 수 있죠.
하지만, inner함수의 지역변수인 inVar은 inner함수에서만 참조되기 때문에 outer함수에서 출력하려고 하면 참조 에러가 발생하는 것입니다.
스코프 체인
위와 같이 여러 스코프가 중첩되어 있을 때 자바스크립트는 식별자를 어떤 방식으로 찾을까요? 바로 스코프 체인을 이용합니다. 스코프가 중첩되어 있을때 그 연결 관계를 나타낸 것이 스코프 체인입니다.
// 전역
var foo = 1;
function outer(){
// outer 지역
var outVar = 'outer';
function inner(){
// inner 지역
var inVar = 'inner';
console.log(outVar); // 'outer'
console.log(inVar); // 'inner'
}
inner();
console.log(outVar); // 'outer'
console.log(inVar); // ReferenceError: inVar is not defined
}
위의 코드를 보면 스코프는 전역에서 outer로 outer에서 inner로 연결됩니다. 하지만 식별자를 찾을 때는 반대로 올라갑니다. 상위 스코프는 하위 스코프의 식별자를 참조하지 못하기 때문에 현재 스코프에 찾고자 하는 식별자가 없다면 하위 스코프에서 식별자를 찾지 못하는 것은 당연한 것이죠.
다시 위의 코드로 돌아가면, 처음 출력되는 inner함수의 console.log(outVar)는 outVar를 찾기위해 최초 inner함수 스코프를 확인합니다. 하지만 그곳에는 없죠. 그렇기 때문에 상위 스코프인 outer스코프에서 다시 outVar를 찾습니다. 있기 때문에 참조할 수 있게 되는 것이죠.
그렇다면 오류가 난 부분의 코드를 확인해보죠. 먼저, outer함수의 스코프에서 inVar를 찾아보지만 존재하지 않습니다. 여기서 하위 스코프를 확인하는 것이 아니라 상위 스코프인 전역 스코프에서 찾는 것입니다. 하지만 전역에도 그 식별자는 없죠. 그렇기 때문에 해당 식별자가 정의되지 않았다는 참조 오류가 발생하는 것입니다.
자바스크립트 엔진이 식별자를 찾는 방법은 이런 방식입니다. 바로 스코프 체인을 이용하는 것이죠.
함수를 호출하는 것도 같은 방식입니다.
function foo(){
console.log('global');
}
function bar(){
function foo(){
console.log('local');
}
foo();
}
bar();
위 코드를 실행하면 무슨 값이 출력 될까요? 바로 local입니다. 함수가 호출된 당시 같은 스코프에 있다면 같은 이름의 함수가 존재한다면, 그 함수를 호출하는 것이죠.
함수 레벨 스코프
함수 레벨 스코프는 함수 수준에서 지역 스코프를 만든다는 의미입니다. 지금까지 봤던 것이 함수 레벨 스코프인 것이죠.
이와 다르게 블록 레벨 스코프도 있습니다. 함수를 포함해서 블록문을 지역 스코프로 보겠다는 것이죠. 보통 프로그래밍 언어는 블록 레벨 스코프를 지원합니다. 하지만 자바스크립트의 var는 함수 레벨 스코프죠.
var x = 1;
if (true){
var x = 10;
}
x; // 10
블록 레벨의 경우 위 코드의 결과가 1이어야 합니다. 하지만 10이 출력되죠. 함수가 아닌 블록에서 선언한 변수는 지역 스코프가 아니기 때문입니다. 이렇게 되면 예상하지 못하게 값을 재할당하게 되는 경우가 발생합니다. 그렇기 때문에 let const는 블록 레벨 스코프를 지원하게 됩니다.
for(var i = 0; i< 5; i++){
console.log(i); // 0~4까지 출력
}
console.log(i); // 5
위의 코드를 보면 var는 함수 레벨 스코프이기 때문에 반복문의 코드 블록에서 증가를 하게 되면 그 값이 계속 남아있는 것을 확인할 수 있습니다.
렉시컬 스코프
함수의 상위 스코프를 결정하는 두 가지 방법이 있습니다.
- 함수를 호출한 위치에 따라 결정
- 함수를 정의한 위치에 따라 결정
var x = 1;
function foo(){
var x = 10;
bar();
}
function bar(){
console.log(x);
}
foo();
bar();
위의 코드 결과를 생각해보세요. 무슨 값이 출력 될까요? 함수의 상위 스코프를 결정할 때는 두 가지 방법이 있다고 했습니다. 먼저 첫 번째 호출되는 위치에 따라 결정하는 것을 동적 스코프라고 합니다. 두 번째 정의한 위치에 따라 결정하는 것을 정적 스코프( 렉시컬 스코프 ) 라고합니다.
대부분의 프로그래밍 언어는 정적 스코프를 지원하죠. 자바스크립트도 마찬가지입니다. 함수가 정의된 위치가 자신의 상위 스코프인 것이죠. 위 예제로 돌아가보면, bar함수의 상위 스코프는 자신이 정의된 전역이 되는 것입니다. foo함수가 아닌 것이죠. 그래서 출력은 1과 1을 출력합니다.
함수 정의가 실행되어 생성된 함수 객체는 이렇게 결정된 상위 스코프를 기억합니다. 함수가 호출될 때마다 상위 스코프를 참조할 필요가 있기 때문입니다.