본문 바로가기
JavaScript Study

JavaScript - #16. 스코프

by KMS_99 2023. 8. 30.

스코프란 자바스크립트를 포함한 모든 프로그래밍 언어의 기본적이며 중요한 개념이다.

 

Scope는 해석해보면 범위라는 뜻을 가지고 있다.

프로그래밍에서 스코프 역시 범위를 뜻한다.

 

우리는 앞선 공부에서 스코프를 경험했고 그 내용을 보며 스코프가 무엇인지 알아보자.

function add (a,b) {
    console.log(a+b); //5
    return a+b;
}

add(2,3);

console.log(a,b)//ReferenceError

위 코드는 이전시간에 알아본 함수 선언식이다.

이때 add라는 함수는 매개변수 a, b를 가지게 된다.

함수 내부에서는 이 매개변수 a, b를 사용한 문들이 실행이 잘 되지만, 마지막줄에 함수 외부에서 이 매개변수에 접근하려고 하면 참조오류가 발생한다.

오류가 발생하는 이유는 매개변수가 사용될수있는 즉, 유효한 사용범위가 함수 내부에 한정되어 있기 때문이다.

우리는 이것을 매개변수의 스코프가 함수 내부로 한정된다고 한다.

 

변수는 코드의 가장 바깥 영역 뿐 아니라 코드블럭, 함수 몸체 어디서든 선언이 가능되며, 이 코드블록이나 함수는 중첩이 가능하다. 다음 코드를 보자.

var var1 = 1;
 
if (true) {
    var var2 = 2;
    if(true) {
        var var3 = 3;
    }
}

function foo(){
    var var4 = 4;

    function bar(){
        var var5=5;
    }
}

console.log(var1) // 1
console.log(var2) // 2
console.log(var3) // 3
console.log(var4) // ReferenceError
console.log(var5) // ReferenceError

변수는 자신이 선언된 위치에 의해서 자신의 유효범위가 결정된다.

이는 모든 식별자를 가진 것들은 동일하게 적용된다.

결론적으로 스코프는 식별자가 유효한 범위라고 할 수 있다.

(위 코드에서 var2, var3는 참조가 가능하고 var4, var5가 참조 에러가 발생한 이유는 var가 함수레벨 스코프를 가지기 때문이다.)

 

그렇다면 다음 코드를 예측해보자

var x = 'global'; // 전역변수

function foo(){
    var x = 'local'; //지역변수
    console.log(x); // 지역변수 x의 'local'
}

foo();

console.log(x) // 전역변수 x의 'global'

 

위 코드에서는 x라는 변수가 코드의 가장 바깥부분에서 먼저 선언되고 이후 foo 함수 내부에서 추가로 선언되었다.

만약 x 변수를 참조하게 된다면 자바스크립트 엔진에서는 어떤 x를 참조하는 것인지 결정해야할 것이다.

이를 식별자 결정이라고 한다.

식별자 결정을 하는데 필요한 규칙이 바로 스코프이다.

먼저 선언된 x변수는 코드 가장 바깥에 선언되어 전체 코드에서 모두 참조가 가능한 전역스코프를 가진다.

이에 반해 foo 함수 내부에서 선언된 x변수는 foo 함수 내부에서만 참조할 수 있는 함수 스코프를 가진다.

이렇게 스코프가 다른 변수는 아무리 같은 식별자를 가질지라도 다른 변수라고 볼 수 있다.

 

스코프를 통해 변수의 이름충돌을 방지하며, 같은 이름을 사용할 수 있도록 해준다. 식별자의 이름은 고유하고 유일해야 하지만 스코프는 네임스페이스의 개념을 가지기 때문에 같은이름을 사용할 수 있도록 해준다.

 

스코프는 전역과 지역 스코프로 나누어진다.

구분 설명 스코프 변수
전역 코드의 가장 바깥 영역 전역 스코프 전역 변수
지역 함수 몸체 내부 지역 스코프 지역 변수

변수는 스코프에 따라서 유효범위가 결정된다.

 

다음 코드 예제를 살펴보자

var x = "global x"; //전역스코프 / 전역변수
var y = "global y"; //전역스코프 / 전역변수

function outer(){
    var z = "outer's local z"; //지역스코프 / 지역변수

    console.log(x); // 전역변수 x 참조 "global x"
    console.log(y); // 전역변수 y 참조 "global y"
    console.log(z); // outer함수의 지역변수 z참조 "outer's local z"

    function inner() {
        var x = "inner's local x" //지역스코프 / 지역변수

        console.log(x); // inner함수의 지역변수 x 참조 "inner's local x"
        console.log(y); // 전역변수 y 참조 "global y"
        console.log(z); // outer함수의 지역변수 z참조 "outer's local z"
    }

    inner();
}
outer();
console.log(x); // 전역변수 x 참조 "global x"
console.log(z); // ReferenceError / 가장 바깥 부분에서는 함수내부에 선언된 지역변수 참조가 불가능

지역변수는 함수 몸체 내부에서 선언된 변수로 지역스코프를 가지기 때문에 지역변수가 된다.

지역변수가 유효한 범위는 선언된 지역과 하위 지역이다. 외부에서는 유효하지 않다.

따라서 지역변수인 z를 외부에서 참조하려고 했을 때 참조에러가 발생하였다.

그리고 지역변수인 z를 하위 지역에서는 참조가 가능한 것도 해당이유이다.

 

전역변수는 함수 가장 바깥부분에서 선언된 변수로 전역스코프에서 선언되었기에 전역변수가 된다.

전역변수가 유효한 범위는 코드전체이다.

따라서 중첩된 지역스코프인 inner 함수 내에서도 전역변수 y가 참조될 수 있다.

 

함수는 중첩이 가능하다.

위 코드에서는 outer 함수 내부에 inner 함수가 정의되어있다.

이 경우에는 inner함수가 outer함수의 하위 스코프가 되며, outer 함수는 inner함수의 상위 스코프이다.

또한 outer함수 자체는 전역 스코프의 하위 스코프이며, 전역스코프는 outer함수를 하위 스코프로 가지고 있다.

즉, 다음과 같은 계층적 구조가 가능하다.

이런식으로의 계층 구조를 스코프 체인이라고 한다.

변수를 참조할 때 스코프 체인에 의하여 참조 대상을 결정하며, 해당 스코프에서 상위 스코프로 참조 변수를 탐색한다.

이 과정은 렉시컬 환경이라는 물리적인 실체로도 존재하며, 변수의 선언이 실행되면 변수 식별자가 렉시컬 환경에 키로 등록되고, 할당이 일어나면 렉시컬 환경의 변수 식별자에 해당하는 값을 변경한다. 

여기서 기억해야 할 것은 변수 참조가 일어나면 현재 스코프 부터 상위 스코프로 순차적으로 참조 변수를 탐색한다는 것이다.

 

스코프 체인을 통해 변수도 검색을 할 수 있지만 함수도 검색이 가능하다.

function fun1(){
    console.log('global function fun1');
}

function fun2(){
    function fun1 (){
        console.log('local function fun1');
    }
    fun1(); // local fun1
}

fun2();

여기서 fun1이 전역스코프와 지역스코프에 한번씩 정의된 것을 할 수 있다.

함수 선언문은 정의될 때 암묵적으로 함수이름이 식별자로 생성되는 것을 배웠었다.

따라서 식별자가 생성되며, 해당 식별자는 스코프를 가지게 된다.

따라서 fun2에서 fun1를 참조하면 fun2의 선언문을 통해 정의된 fun1을 가리키게 된다.

결론적으로 스코프는 변수를 검색하는 것이 아닌 식별자를 검색한다고 볼 수 있다.

 

다음으로 함수레벨 스코프를 알아보자.

함수레벨 스코프는 함수 내부를 지역으로 판단하는것이며, 이와 비슷한 블록레벨 스코프는 코드블록을 지역으로 판단한다. 

함수의 몸체는 여러 구문을 담고 있는 문이다. 따라서 코드 블록에 함수 몸체도 존재한다고 생각할 수 있다.

즉, 코드블록 스코프가 더 큰 개념이라는 것이다.

코드블록 스코프는 함수블록스코프에서 함수만 지역으로 판단하는 것 외에도 if, for, while 등의 코드블록에서도 지역스코프를 만든다.

우리가 지금까지 사용했던 var는 함수블록 스코프이며, ES6에서 나온 let과 const는 블록레벨 스코프이다.

함수블록스코프는 다음과 같은 문제를 발생시킨다.

var i =10;

for(var i =0; i<5; i++){
    console.log(i); //0, 1, 2, 3, 4
}

console.log(i); //5

전역 부분에 선언된 i변수는 10의 값을 가지고 있다. 

우리는 통산적으로 for문에 반복 여부를 판단하는 변수로 i,x 등 의미 없는 식별자를 사용한다.

함수레벨스코프에서는 for문이나 if문 등 코드블록 내에서 선언된 변수는 전역스코프에서 생성되었다고 판단하기 for문에서 선언한 var i = 0은 전역변수이며 기존에 있던 전역변수 i가 재할당되었다.

마지막 출력값을 보면 10이 아니라 반복문을 빠져나왔을 때 의 값인 5가 나온다.

 

이렇게 의도치 않은 재할당이 이루어질 수 있기에 함수레벨스코프 보다는 블록레벨스코프를 사용하는 것이 바람직하다.

 

마지막으로 스코프에서 알아볼 개념은 렉시컬 스코프이다.

다음 코드를 예측해보자.

var x =1;
function lex(){
    var x =10;
    lexicalTest();
}

function lexicalTest(){
    console.log(x); // 1
}

lex();
lexicalTest();

여기서 알아볼 것은 전역스코프에서 정의된 함수가 지역스코프에서 실행될때 해당 지역스코프 하위 지역이 되는것인지 이다.

결론은 아니다이다.

자바스크립트는 렉시컬스코프를 따른다.

이 렉시컬스코프는 함수가 정의되는 시점의 스코프를 동적으로 변화하지 않고 정적으로 따른다는 것이다.

즉 렉시컬스코프는 정의되는 시점의 스코프를 따르며, 동적으로 변화하지 않는다는 것이다

위 코드의 lexicalTest함수는 전역스코프에서 정의되었고, 어느 스코프에서 사용되던 전역스코프를 따른다.

즉 lex함수 내부에서 실행이 되어도 전역스코프를 따르게 된다.

 

결론적으로 해당 코드의 결과는 1을 두번 출력한다.

'JavaScript Study' 카테고리의 다른 글

JavaScript - #18. let, const  (0) 2023.08.31
JavaScript - #17. 전역변수의 문제점  (0) 2023.08.31
JavaScript - #15. 함수(2)  (0) 2023.08.29
JavaScript - #14. 함수(1)  (0) 2023.08.29
JavaScript - #13. 원시값과 객체(2)  (0) 2023.08.28