스코프
스코프(Scope)란?
우리말로 번역하면 ‘범위’라는 뜻으로 ‘변수에 접근할 수 있는 범위’ 이다.
1
2
3
4
5
6
7
8
9
10
var a = 1;
function print() {
var a = 111;
console.log(a);
}
print();
console.log(a);
이름이 같은 변수 a가 중복 선언되었다. 전역에서 변수 a를 참조할 때, 그리고 함수 print 내부에서 변수 a를 참조할 때 이름이 중복된 2개의 변수 중 어떤 변수를 참조 해야 하는가? 자바스크립트는 어떻게 변수를 식별하는 것일까?
**스코프는 참조 대상 식별자(identifier, 변수, 함수의 이름과 같이 어떤 대상을 다른 대상과 구분하여 식별할 수 있는 유일한 이름)를 찾아내기 위한 규칙 **이다. 자바스크립트는 이 규칙대로 식별자를 찾는다.
위 예제에서 전역에 선언된 변수 a는 어디에든 참조 **할 수 있다. 하지만 함수 **print 내에서 선언된 변수 a는 함수 print 내부에서만 참조 할 수 있고 함수 외부에서는 참조할 수 없다. 이러한 규칙을 스코프라고 한다.
스코프 구분
자바스크립트에서 **스코프를 구분 **해보면 다음과 같이 2가지로 나눌 수 있다.
1. 전역 스코프(global scope)
전역에 선언되어있어 어느 곳에서든지 해당 변수에 접근할 수 있다.
1
2
3
4
5
6
7
var a = 1;
function print() {
console.log(a); // 1
}
print();
위 예제에서 print 함수는 변수 a를 가지고 있지 않지만 a가 전역 변수로 선언되어있기 때문에 1이 출력된다.
2. 지역 스코프(local scope)
해당 지역에서만 접근할 수 있어 지역을 벗어난 곳에선 접근할 수 없다.
1
2
3
4
5
6
7
8
function print() {
var a = 1;
console.log(a); // 1
}
print();
console.log(a); // error
위 예제에서 변수 a는 지역 스코프를 가지므로 print 함수 내부에서만 유효하다.
따라서 print 내의 console은 1이 잘 출력 되겠지만, print 외부의 console에서는 a를 찾을 수 없기 때문에 error가 발생한다.
즉, 변수는 선언 위치(전역 또는 지역)에 의해 스코프를 가지게 되며 전역에서 선언된 변수는 전역 스코프를 갖는 전역 변수이고, 지역(자바스크립트의 경우 함수 내부)에서 선언된 변수는 지역 스코프를 갖는 지역 변수가 된다.
블록스코프와 함수 스코프 (block scope & function scope)
대부분의 C-family language는 블록 레벨 스코프(block-level scope)를 따른다. 블록 레벨 스코프란 코드 블록({…})내에서 유효한 스코프를 의미한다. 여기서 “유효하다”라는 것은 “참조(접근)할 수 있다”라는 뜻이다.
1. 블록 스코프
function 을 제외한 if나 for 등의 ’{ }’ 안에 있는 범위
1
2
3
4
5
6
7
8
9
10
11
int main(void) {
// block-level scope
if (1) {
int x = 5;
printf("x = %d\n", x);
}
printf("x = %d\n", x); // use of undeclared identifier 'x'
return 0;
}
위의 C언어 코드를 보면 if문 내에서 선언된 변수 x는 if문 코드 블록 내에서만 유효하다. 즉, if문 코드 블록 밖에서는 참조가 불가능하다.
하지만 자바스크립트는 함수 레벨 스코프를 따른다. 함수 레벨 스코프란 함수 코드 블록 내에서 선언된 변수는 함수 코드 블록 내에서만 유효하고 함수 외부에서는 유효하지 않다(참조할 수 없다)는 것이다.
위의 코드를 자바스크립트로 변환하고 어떻게 출력되는지 살펴보겠다.
2. 함수 스코프
function{ } <- 안에있는 범위
1
2
3
4
5
6
7
8
9
10
11
// function-level scope
function main() {
if (1) {
var x = 5;
console.log("x= ", x); // x= 5
}
console.log("x= ", x); // x= 5
}
main();
위의 자바스크립트 코드에서는 if의 블록 스코프가 아닌 main의 function 스코프를 가지기 때문에 두 console 모두 오류없이 잘 출력된다.
그렇기 때문에 함수 스코프를 따르는 var keyword는 함수 내에서만 지역 변수로 유지된다.
자바스크립트에서 블록 스코프 따르기 위해서는 ES6에서 도입된 let, const keyword를 사용할 수 있다.
- var, let, const
var | let | const | |
---|---|---|---|
재할당 | 가능 | 가능 | 불가능 |
재선언 | 가능 | 불가 | 불가 |
재선언 | 함수스코프 | 블록 스코프 | 블록 스코프 |
자세한 내용은var, let, const 차이점 게시글을 통해 확인 할 수 있다.
렉시컬 스코프(Lexical scope)
함수를 어디서 호출하는지가 아니라 어디에 선언하였는지에 따라 결정되는 것을 말한다.
즉, 함수를 어디서 선언하였는지에 따라 상위 스코프를 결정한다는 뜻이며, 가장 중요한 점은 함수의 호출이 아니라 함수의 선언에 따라 결정된다는 점이다.
다른 말로, 정적 스코프(Static scope)라 부르기도 한다.
아래의 예시 코드가 어떻게 동작할지 생각해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
var x = 1; // global
function first() {
var x = 10;
second();
}
function second() {
console.log(x);
}
first(); // ?
second(); // ?
실행 결과 first, second모두 1이 출력된다. 그 이유는 선언한 위치에 따라 상위 스코프를 결정했기 때문이다.
스코프 | 렉시컬 스코프 |
---|---|
global : x, first() | first() : global |
first : x, second() | second() : global |
second() : |
각 위치 별 스코프와 렉시컬 스코프를 나타내 보았다. 렉시컬 스코트는 각 함수의 상위 스코프를 말한다.
위 처럼 second는 x 변수를 가지고 있지 않기 때문에 second의 상위인 global에서 변수 x를 찾게 된다.
즉, second() 함수가 first() 함수 안에서 호출된 것과 상관없이 second() 함수는 global 범위에 선언되어 있으므로, global 범위에 있는 변수 x의 값 1이 두 번 출력된 것이다.
스코프 체인(Scope chain)
위의 예제에서 second()함수의 스코프에 x가 존재하지 않아 second()함수의 상위 스코프인 global범위에 있는 변수 x를 출력하였다.
이렇게 유효 범위를 나타내는 스코프가 scope 프로퍼티로 각 함수 객체 내에서 연결리스트 형식으로 관리되는데, 이 스코프 간의 상하관계를 스코프 체인이라고 한다.
이러한 정보는 실행 컨텍스트가 가지고 있으며 실행 컨텍스트가 실행되면, 엔진이 스코프 체인을 통해 렉시컬 스코프를 먼저 파악한다.
그러고 나서, 함수가 중첩 상태일 때 하위 함수 내에서 상위 함수의 스코프와 전역 스코프까지 참조할 수 있는데 이것을 스코프 체인을 통해 탐색하는 것이다.
즉 실행 컨텍스트는 현재 자신의 scope에서 사용하고자 하는 변수가 없으면 Scope Chain을 통해 해당 변수를 찾는다.
- 실행 컨텍스트(Execution context)
우리가 작성한 코드가 실행되는 환경을 말하며, scope, hoisting, this, function, closure 등의 동작원리를 담고 있는 자바스크립트의 핵심원리
아래의 코드가 어떻게 동작하는지 스코프와 스코프 체인을 통해 알아보겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const x = "x";
function c() {
const y = "y";
console.log("c");
function b() {
const z = "z";
console.log("b");
c();
}
}
function a() {
const x = "x";
console.log("a");
b();
}
a();
c();
스코프 | 스코프 체인 |
---|---|
global : x, c(), a() | c() : global |
c() : y, b() | a() : global |
b() : z | b() : c() -> global |
a() : x |
- 과정
- 함수 a()의 스코프에서 b()를 찾음 -> 없음
- 상위인 global에서 b()를 찾음 -> 없음
- ReferenceError: b is not defined -> 호출 불가
a를 호출하지 않고 c만 호출한다고 가정하고 다시 과정을 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const x = "x";
function c() {
const y = "y";
console.log("c");
function b() {
const z = "z";
console.log("b");
c();
}
}
function a() {
const x = "x";
console.log("a");
b();
}
c();
- 과정
- 함수 b()의 스코프에서 a()를 찾음 -> 없음
- 상위인 c()에서 a()를 찾음 -> 없음 ->
- 상위인 global에서 a()를 찾음 -> 있음 -> 호출 가능
위 처럼 함수가 중첩 상태일 때 하위 함수 내에서 상위 함수의 스코프와 전역 스코프까지 참조하는 관계를 연결리스트로 나타낸 것을 스코프 체인이라 한다.
이 스코프 체인은 실행 컨텍스트가 생성하고 유지하며, 실행 컨텍스트는 코드가 실행되면 스코프 체인을 따라 변수 및 함수를 검색하고 실행한다.
📑 참고 자료