스코프란
- 스코프(Scope, 유효범위)란 식별자(identifier)를 찾아내기 위한 규칙이다. 식별자란 사람으로 따지면 이름, 주민등록번호 같은 것이 될 수 있겠다. 주민등록번호는 단 한개도 겹칠 수 없지만, 이름은 똑같은 사람이 있을 수 있다. 그러면 그 이름이 같은 사람은 어떻게 구별해야 할까? 컴퓨터는 이름이 같은 사람을 어떻게 다르게 구분할 수 있을까? 예를 들어서 강남구에 사는 김나라와 관악구에 사는 김나라는 다르다고 인식할 수 있지 않을까?
- 프로그래밍할 때도 변수명이나 함수명이 겹칠 수도 있다. 예를 들어
i
같이 흔한 변수명을 사용하는데 모든 코드를 통틀어i
를 단 한 번밖에 못 쓴다는 건 좀 당황스럽다. 그렇다면 영역을 분리할 수 있지 않을까? 마치 디렉토리 구조를 나누면 각각의 디렉토리 안에 같은 파일명을 사용해도 무방한 것처럼. - 위 예시처럼 디렉토리 역할을 해주는 규칙이 스코프다. 스코프는 이름이 같은 식별자의 충돌을 방지해준다. 스코프 내부의 것은, 그 내부에서만 유효하다.
함수 스코프
자바스크립트는 기본적으로 함수 스코프를 사용한다. 이는 변수 선언이 함수 수준에서 이루어짐을 의미한다. 코드 블록({...}
)은 새로운 스코프를 생성하지 않는다.
function scope() { // Scope
for(var i = 0 ; i < 10 ; i++) { // Scope 아님
// count
}
console.log(i); // 10
}
전역 스코프
위의 함수 스코프와 대비할 수 있는 것이 전역 스코프다. 즉 디렉토리가 없는 상태라고 생각할 수 있다. 전역 스코프에서 변수를 선언했을 때 생길 수 있는 문제는 아래 예시와 같다.
var foo = 42;
function scope() {
foo = 21;
}
scope();
foo; // 21
var foo = 42;
function scope() {
var foo = 21;
}
scope();
foo; // 42
단지 scope함수 내에서 var을 하나 빠뜨렸을 뿐인데, 변수 foo의 값이 달라진다.
첫번째 코드에서는 현재 전역 스코프에 선언된 foo가 하나 있다. scope 함수 내에 foo = 21
은 새로운 변수를 만든 것이 아니다. 스코프 내에서 해당 변수를 찾을 수 없는 경우 점점 상위의 스코프를 탐색하며 이를 찾는다. 만약 전역 스코프까지 갔는데 못 찾을 경우에는 Reference Error를 리턴한다. 이 경우에는 scope 함수 내에 foo가 선언된 적이 없고, 그러므로 상위 스코프인 전역스코프로 가서 찾았는데 foo라는 변수가 있었기에 이 변수의 값을 재할당한 것이다. 그러므로 foo는 재할당된 값인 21이 된다.
두번째 코드에서는 foo가 두 개 있다. 하나는 전역스코프의 foo, 하나는 scope함수 내에 선언된 foo이다. 같은 이름이지만 선언될 수 있는 것은 스코프 규칙때문이다. 다시 언급하지만 scope함수 내의 var foo
는 해당 함수의 스코프 내에서만 유효하다. 이 함수 스코프 밖에서는 함수 내부의 foo를 참조할 수 없다. 그러므로 맨 마지막줄의 foo는 전역 스코프의 foo를 참조하게 되고, 이는 재할당된 적이 없으므로 42라는 값을 갖고 있다.
이렇듯 var 하나만으로 결과가 달라지는 일이 발생하고, 이 부분에서 실수를 했을 때 잡아내기가 무척 어려우므로 ES6부터 사용되는 const
와 let
을 가급적 사용해야 한다.
블록 스코프
const와 let은 블록 수준의 스코프를 만들어준다. 즉 함수 스코프 내에서는 코드블록 {…}
이 새로운 스코프를 형성하지 않았으나 const와 let을 사용하면 이 코드블록이 새로운 스코프가 되는 것이다. let과 const의 차이는 값의 재할당이 가능하냐 아니냐이고, 변수명을 중복해서 선언할 수 없다는 점은 공통적이다.
아까 위의 예시를 가져온다면
const foo = 42;
function scope() {
foo = 21;
}
scope();
foo; // TypeError: Assignment to constatnt variable
이처럼 const를 사용하면 재할당이 불가능하므로 에러가 난다. 그러므로 가능하다면 let보다도 const를 사용하는 것이 좋을 것이다.
다시 블록 스코프로 돌아가자면, 이제 코드블록도 스코프를 형성하므로 아래 예시가 가능하다.
{ // Block scope
let foo = 42;
if(true) { // Block scope
let foo = 21;
}
}
foo; // Reference Error: foo is not defined
일단 첫번째 코드블록 내에서 foo가 선언된다. 그리고 두번째 코드블록 내에서도 foo가 선언되는데, 둘다 let을 사용하여 선언하였고 즉 중복 변수 선언이 불가능함에도 스코프가 다르기 때문에 가능하다. 그리고 마지막 줄의 foo를 부를 때에는 Reference Error가 발생하는데, 전역 스코프에는 foo가 선언된 적이 없기 때문이다. 다시 말하지만 스코프 내부의 것은 스코프 내에서만 유효하기 때문에, 해당 스코프 밖에서는 참조할 수 없다.