13-1 스코프
스코프(scope): 유효범위, 스코프는 특히 자바스크립트 내에서 변수 var 와 let, const 와의 구별에 밀접한 관계가 있다. 여기서 유효범위는 이전에 함수를 다루면서 경험한 적이 있는데, 함수의 매개변수는 함수 몸체 외부에서는 참조 할 수 없다고 했었다. 즉, 매개변수의 유효범위가 함수 몸체인것이다.
함수 매개변수는 함수 내에서 선언이 되었다. 변수가 함수 내에서 선언이 되었다는 것은, 아니 이렇게 표현하게 되는 이유가 있는데, 변수를 포함한 식별자가 어디에서 어느 위치에서 선언되는지에 따라 그 변수를 참조할 수 있는 범위가 결정되기 때문이다. 즉,
모든 식별자(변수 이름, 함수 이름, 클래스 이름 등)는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정된다. 이를 스코프라고 한다. 스코프는 식별자가 유효한 범위를 말한다.
var x = 'global';
function foo(){
var x = 'local';
console.log(x); // 1
}
foo();
console.log(x); // 2
위 예제를 보개 되면 두번의 콘솔창을 호출하게 된다. 변수와 함수선언문은 런타임 이전에 선언되고, 이후 foo 라는 함수가 호출된다.
동일한 x 를 호출하는 두 콘솔의 호출의 결과는 어떻게 나올까? 일단 이러한 경우, 어떠한 식별자를 참조할 지 결정하는 과정을 식별자 결정(identifier resolution) 이라고 한다. 자바스크립트 엔진은 스코프를 통해 어떤 변수를 참조해야 할 지 결정하는데, 이때문에 스코프란 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙 이라고도 할 수 있다.
다시 예제로 돌아와보자. 맨 위 global 로 할당 된 변수 x 는 어디서든 참조 할 수 있다. 하지만 foo 내부에서의 변수 x 는 foo 함수 내부에서만 참조할 수 있고, 이렇기에 두 변수는 이름은 동일하지만 식별자가 가진 유효한 범위가 다른 별개의 변수다. 따라서 1번은 'local', 2번은 'global' 을 출력한다.
같은 이름을 가졌지만 별개의 변수다. 이게 어쩌면 핵심일 수 있겠다. 이 말은 우리가 코드를 작성할 때 변수를 어떠한 위치에서 선언하느냐에 따라 중복된 이름의 변수를 사용할 수 있다는 의미이다. 만일 그렇지 않다면, 우리는 하나의 이름이 정해진 변수는 두번 다시 선언할 수 없게 될 것이다.
다만, 특이하게도 var 키워드는 같은 스코프 내에도 중복 선언이 가능하다. 그와 대조적으로 let, const 의 경우 같은 스코프일 경우 중복 선언 허용을 하지 않는다.
13-2 스코프의 종류
크게 코드는 전역(global) 과 지역(local) 로 구분할 수 있다.
구분 | 설명 | 스코프 | 변수 |
전역 | 코드의 가장 바깥 영역 | 전역 스코프 | 전역 변수 |
지역 | 함수 몸체 내부 | 지역 스코프 | 지역 변수 |
여기서 보면 알 수 있듯, 변수가 어느 위치에서 선언되는지에 따라 전역과 지역으로 나뉘게 된다.
전역은 코드의 가장 바깥 영역을 말한다. 전역은 전역 스코프(global scope) 를 만들고, 전역에서 변수를 선언하면 전역 스코프를 갖는 전역 변수가 된다. 이 변수는 어디서든 참조할 수 있다.
반면 지역이란 함수 몸체 내부를 말한다. 지역은 지역 스코프(local scope) 를 만들고, 지역변수는 자신의 지역 스코프와 자신의 지역의 하위 스코프에서만 유효하다.
그런데 위 사진을 보게 되면, 아까 첫 예시도 그렇고, 같은 이름의 변수이자 다른 스코프를 가지는 x 가 2개 존재한다. 전역에 하나, 지역에 하나. 함수 inner 안에 있는 console.log 의 경우 x 를 호출할 때, 어느 x 를 선택할것인가? 앞서서 살펴보았듯이 여기서도 지역 inner 내부에 있는 변수 x 를 참조하게 된다. 이렇게 선택하는 과정을 자바스크립트 엔진이 스코프 체인을 통해 참조할 변수를 검색했다고 한다.
13-3 스코프 체인
전역과 달리 지역은 함수 몸체 내부를 말한다고 하였다. 함수는 전역에서도 정의할 수 있지만 함수 몸체 내부에서도 정의할 수 있다. 이렇다는 말은 지역 스코프 조차 중첩에 따라 계층적인 구조를 가질 수 있다는 말이다.
가장 최 상단은 전역이라는 사실은 쉽게 알 수 있다. 그렇다면 함수 outer 와 inner 의 스코프는 그냥 지역 스코프로서 같은 범위를 가지는 것일까? 예상컨대 그렇진 않을 것이다. 중괄호로 이루어진 몸채 내를 유효한 범위로 가지는 지역 스코프이기에, 부분 집합의 개념으로 이해하면 좀 더 쉽게 이해가 될 것 같다. inner 를 감싸고 있는 outer 의 범위가 더 넓다고 볼 수 있고, 그렇기에 위에서 설명하였던 것 처럼 자기 자신의 스코프와 하위 스코프를 유효 범위로 가지게 될 수 있다. 이런 경우 outer 를 중첩 함수의 상위 스코프라고 하게 된다.
위 사진을 살펴보면, 예시의 함수들이 가지는 스코프가 계층적인 구조로 연결되어있고, 하단이 하위 스코프이고 상위로 갈 수록 범위가 넓어지는 상위 스코프가 된다. 이러한 계층 연결을 스코프 체인(scope chain) 이라고 한다.
변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작해서 상위 스코프 방향으로 이동하며 선언된 변수를 검색(식별자검색) 한다.
천천히 읽어보면 이해가 될 문장이지만 좀 더 예시를 들어 설명해보는게 좋겠다.
var x = 'global x';
var y = 'global y';
function foo(){
console.log('global function foo'); //1
}
function bar(){
var x = 'xxx'
function foo(){
console.log('local function foo'); //2
console.log(x); //3
}
foo();
}
bar();
console.log(x); //4
우선 스코프 체인에 의해 변수를 검색하는 과정은 말 그대로 하위 스코프에서 상위 스코프로 참조범위를 넓혀 가면 된다.
3번째 console.log 를 살펴보자. 이 코드는 변수x 를 참조해야 하는 코드다. 가장 하위 스코프인 foo 내부를 살펴보자. x 가 있는가? 없다면 계층적으로 연결된 상위 스코프 bar 를 참조해보자. 여기에 변수 x 가 선언되어 있는가? 있다! 따라서 3번은 'xxx' 가 출력이 된다.
함수는 어떠할까? 가장 전역에서 bar 가 호출이 되었고, 이 함수가 호출이 되면, foo 함수를 호출하게 된다. 근데 foo 함수는 2군데 선언되었다. 이때 역시나 스코프 체인에 의해 하위스코프에서 상위 스코프로 검색을 이어나간다. 우선 코드가 실행된 스코프 내에서 foo 를 참조할 수 있는가? 있다! 바로 위에 선언되어있다. 따라서 bar 내부 foo 를 호출하여 내부 console 값들이 호출이 되게 된다.
결론적으로 명심할 것은 어떠한 변수를 참조하고 있는 코드가 실행될 때, 가장 먼저 자신이 속해있는 스코프 내에서 참조할 변수를 검색하고 이후 계층적으로 상위 스코프를 검색한다고 생각하자. 참고로 이러한 계층적 구조는 물리적으로 존재하는데, 자바스크립트 엔진은 런타임에서 코드를 실행하기에 앞서서 이러한 계층구조로 되어있는 자료구조인 렉시컬 환경(Lexical Enviroment) 를 실제 생성하여, 이러한 변수들을 렉시컬 환경의 key 값으로 등록하고, 변수 할당이 일어나면 변수 식별자에 해당하는 값을 변경한다. 추후에 좀 더 자세히 다뤄보도록 하자.
13-4 함수 레벨 스코프
위에서 명확하게 지역 스코프를 함수 몸체 내부를 유효한 범위로 가지는 스코프라고 하였다. 사실 함수 몸체를 표현하는 중괄호( '{' )는 함수 뿐 아니라 조건문이나 반복문에서도 나타난다. 즉, 지역 스코프는 코드 블록이 아닌 함수에 의해서만 지역 스코프가 생긴다는 것을 의미한다. 다른 블록에서는 스코프가 형성이 되지 않게 된다. 이러한 특성을 함수 레벨 스코프(function level scope) 라고 한다.
근데 이상하다. 기존 반복문이나 조건문에서 우린 알게 모르게 스코프의 개념을 많이 활용하였다. 그리고 문제없이 코드가 작동하였었다. 이렇다면 어떠한 특정한 조건 내에서만 함수 레벨 스코프가 작동한다고 생각할 수 있겠다. 그리고 그러한 이유에 맞게, 함수 레벨 스코프는 var 키워드로 선언된 변수에게만 나타나는 특성이다. 예제를 살펴보자
var x = 1
if(true){
// var 키워드로 선언된 변수는 함수의 코드 블럭만을 지역 스코프로 인정한다.
// 함수 밖에서 var 키워드로 선언된 변수는 코드 블록 내라 할 지라도 모두 전연 변수다.
var x = 10;
}
console.log(x); // 10
함수 몸체가 아닌 이상 설사 코드블록 내에 선언되었다 한들, 자바스크립트는 모두 전역변수로 판단하게 된다. 또한 앞에서 var 키워드로 선언된 변수는 같은 스코프 내에서도 중복 선언이 가능하다고 했다. 이러한 결과물이 합쳐져서 의도치 않게 x 가 10으로 할당되는 결과를 초래한다.
이러한 결과는 반복문에서도 나타나는데
var i = 10;
for(var i = 0; i < 5; i++){
console.log(i); // 0 1 2 3 4
}
// 의도치 않게 값이 변경된다
console.log(i) // 5
반복문 블록이라 할 지라도 전역변수로 인정되기 때문에, 0부터 5까지 순차적으로 선언이 되어서 마지막 콘솔창에는 5가 찍히게 된다. 반복문 i 를 전역변수로 이용하고자 하는 개발자는 없을 것이다. 허나 자바스크립트에서는 이렇게 작동을 하게 된다.
이러한 점은 코드를 작성하는데 있어서 상당히 불편하다. 그렇기에 ES6 에서 let, const 가 나타나게 되고, 이는 추후에 다시 다뤄보도록 하자.
13-5. 렉시컬 스코프
var x = 1;
function foo(){
var x = 10;
bar();
}
function bar(){
console.log(x);
}
foo(); // ?
bar(); // ?
위 예제는 함정이 있다. 달콤한 함정~
런타임에 들어가기 전 렉시컬 환경에 변수 x 와 두 함수 foo 와 bar 는 선언이 된다. 그 밑 지역 스코프로는 foo, bar 가 각각 가지게 된다. 이런 다음 런타임에서 foo 를 호출한다. 호출이 됬을 때 foo 내부를 보니 참조할 수 있는 변수 x 가 있다. 그리고 그 밑에 bar 함수를 호출한다. bar 함수를 가니 console.log(x) 가 있고 마치 타고 올라가 var x = 10 을 마주쳐서 10을 출력할 거 같이 예상된다.
허나 이는 오답이다. 두 호출 모두 콘솔창에는 1이 띄어지게 된다. 명심해야 할 점이 있다
- 함수를 어디서 호출했는지에 따라 함수의 상위 스코프를 결정한다
- 함수를 어디서 정의했는지에 따랄 함수의 상위 스코프를 결정한다
이 두가지 중 자바스크립트는 어느 방법을 따를까? 우선 함수를 어디서 호출했는지에 따라 결정한다면 bar 함수의 상위 스코프는 bar 함수를 호출한 foo 함수가 상위 스코프일 것이다. 그렇다면 콘솔 창에는 10이 출력되어야 한다. 이러한 방식을 동적 스코프(dynamic scope) 라고 하는데, 함수가 호출되는 시점에서 동적으로 상위 스코프를 결정해야 한다고 해서 붙여진 이름이다. 어찌되었던 이 방식은 자바스크립트의 방식이 아니다.
그럼 자바스크립트는 밑의 방식인데, 이를 정적 스코프(static scope) 또는 렉시컬 스코프(lexical scope) 라고 한다. 함수 정의가 평가되는 시점에서 상위 스코프가 정적으로 결정되기 때문에 정적 스코프라고 부른다.
이러한 점을 참고해보면 왜 출력값이 1이 나왔는지 알 수 있다. 자바스크립트는 함수를 어디서 호출했는지는 전혀 관계가 없다. 오직 함수가 어디에서 선언되었느냐에 따라 상위 스코프가 결정이 된다. 즉, 함수의 상위 스코프는 언제나 자신이 정의된 스코프다.(왜냐면 함수 자체도 스코프를 가지기 때문). bar 함수는 전역에서 선언되었다. 즉 상위 스코프가 전역이다. 따라서 전역의 변수 x 를 참조함을 선언때부터 정해졌기에 값 1을 출력하게 된다.
스코프라는 개념을 예전에 공부했었는데 시간이 지나다 보니 까먹고있었다. 그래서 공부를 하다보니 계속해서 지역 스코프가 함수 몸체를 유효 범위로 가지는 것이라 하여 자꾸 햇갈리기도 하였다. 사실 자바스크립트를 최근에 사용한 사람이라면 let 과 const 가 익숙할 테고, 이는 블록 레벨 스코프라는 사실을 알 수 있기 때문이다. 반복문을 썻는데 전역변수를 건든적은 없었으니 말이다..
이전 ES5 때의 var 키워드를 거진 사용하지 않다보니, 좀 신선한 느낌이다. 이 시절 사람들은 어떤식으로 코드를 완성시켜왔을지가 궁금하긴하다. 난 너무 자바스크립트를 편하게 사용하게 된 것일까...
'Programing > Javascript' 카테고리의 다른 글
[Deep Dive] 프로토 타입 - 프로토타입이란? (0) | 2022.11.16 |
---|---|
[Deep Dive] 전역 변수의 문제점 (0) | 2022.11.08 |
[Deep Dive] 함수 (0) | 2022.10.27 |
[Deep Dive] 원시값과 객체 (0) | 2022.10.25 |
[Deep Dive] 객체 리터럴 (1) | 2022.10.23 |