JS - Scope
스코프란?
- 스코프는 식별자가 유효한 범위이다.
let a = 1;
function help() {
let b = 2;
console.log(a); // ouput : 1
console.log(b); // output : 2
}
help();
console.log(a); // ouput : 1
console.log(b); // output : Error!
왜 에러가 나는걸까?
- 생명주기가 존재하기 때문임. 변수 b의 경우 help함수가 종료되면 저장공간에서 함께 사라짐. 없는 변수를 출력하려 오류 발생.
- 스코프는 네임스페이스이다.
let a = 1;
function help() {
let a = 2;
console.log(a); // ouput : 2
}
help();
console.log(a); // ouput : 1
대부분의 프로그래밍 언어는 같은 스코프 내에서 동일한 식별자를 가진 변수를 선언하면 안됨. ⇒ 이는 윈도우의 동일한 폴더 내에 이름이 같은 파일이 생성될 수 없는 것과 비슷함.
하지만, 같은 이름의 변수를 두 번 선언했지만 에러가 발생하지 않음. ⇒ JS에서 스코프를 통해 동일한 식별자를 가진 변수끼리의 충돌을 방지해주기 때문임.
ㅤ 💡 다음으로 가기 전에 알아둬야 될 사항이 있음.
function hello() {
let a = 10;
let a = 20; // SyntaxError : x가 이미 존재함!
}
💡 이렇게, let, const 키워드를 통한 변수 중복선언은 JS가 “아 이녀석 중복선언을 했구나”하고 바로 잡을 수 있음.
그러나, var는 원래 중복선언이 허용되기 때문에 이게 문제가 안되는 상황이 발생함.
function hello() {
var a = 10;
var a = 20;
}
💡 꼭 let과 const를 통해서 변수를 만들도록 하자…
ㅤ ㅤ ㅤ
스코프의 종류
- 전역 스코프와 지역 스코프가 있음.
let a = 10;
function help() {
let b = 20;
}
전역 스코프는 코드의 가장 바깥 영역을 말함. ⇒ 여기서 변수 a는 전역 스코프에 있기 때문에 전역변수임.
지역 스코프의 지역은 보통 함수 몸체 내부임. ⇒ 여기서 변수 b는 지역 스코프에 있기 때문에 지역변수임.
스코프 체인
- 중첩 함수가 계층적으로 연결된 것을 말함.
function a() {
// outer function : nested functiond을 포함하는 함수.
let res = 1;
function b() {
// nested function이자, c의 outer function
function c() {
// nested function
console.log(res); // output : 1
}
}
}
- 변수를 참조할 때 JS 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.
⇒ console.log(res)를 위해 c ⇒ b ⇒ a까지 올라가서 변수 res를 찾고 출력하는 모습을 볼 수 있다. ㅤ ㅤ ㅤ
var, let, const의 스코프
아까, 스코프의 종류는 전역 스코프와 지역스코프가 있다고 했었다.
전역스코프의 경우 말 그대로 함수의 제일 바깥쪽인건 모든 언어가 동일하나, 지역스코프의 경우는 두 가지 경우가 존재한다.
- 함수 레벨 스코프
- 무조건 함수만을 지역스코프로 인정하는 것.
- var이 이에 해당함.
- 블록 레벨 스코프
- if, for, while, try/catch, 함수등 모든 코드 블록을 지역 스코프로 인정하는 것.
- 대부분의 언어 및 let과 const가 이에 해당함.
여기서 var을 쓰면 안되는 이유가 한번 더 나온다.
자, 아래의 코드를 보자. 변수 i를 var, let으로 선언했다는 차이점이 있는 코드이다.
//i를 var로 선언한 경우
var i = 10;
for (var i = 0; i < 5; i++) {
console.log(i); // output : 0,1,2,3,4
}
console.log(i); //output : 5
//i를 let으로 선언한 경우
let i = 10;
for (let i = 0; i < 5; i++) {
console.log(i); //output : 0,1,2,3,4
}
console.log(i); //output : 10
(물논, 코딩 시에 혼란이 있을 수 있기에 for문을 도는게 아니라면 변수 식별자에 의미없는 i를 붙이진 않는게 좋다.)
var을 사용한 경우에
- for은 지역 스코프로 인정받지 못했다.
- 이후, for문이 시작될 때 i가 0으로 중첩 선언되었다.
- 반복문이 끝났을 때 i는 5가 되었다.
- 우린 10을 출력하길 원했지만 5가 출력됬다.
let을 사용한 경우에
- for은 코드 블록으로 구성됬기 때문에 지역 스코프로 인정받았다.
- “전역 스코프의 i” 와 “for 안의 i”는 다른 변수로 인정받았다.
- 마지막에 우리가 원하는대로 10이 출력된다.
이렇게, var을 사용 시 본인도 모르는 사이에 중첩 선언을 해서 코드가 꼬여버릴 수 있다.
⇒ 제발, var 쓰지말자!!!!
렉시컬 스코프
다음의 코드는 어떤 결과가 나올까?
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // ??
bar(); // ??
(과거 나의 생각)
foo를 실행할 때 var x = 10으로 중첩 선언이 되니까 첫번째 출력은 10이다!
bar을 실행할 때는 그래도 전역 스코프의 x를 보니까 1이네?
아쉽게도 필자는 그렇게 생각했고 틀렸다. 아마, 본인과 비슷한 생각을 한 사람들이 많을 것 같다.
답은 1,1이다. 왜 이런 현상이 발생했는지 알아보자.
우선, bar함수의 상위 스코프가 무엇인지 추리해봐야 한다.
- 함수를 호출한 곳(foo 함수 안)을 기반으로 상위 스코프를 정함(foo, 전역이 상위스코프가 됨)
- 함수를 선언한 곳(전역 스코프)을 기반으로 상위 스코프를 정함(전역이 상위스코프가 됨)
첫 번째 방법을 동적 스코프, 두 번째 방법을 렉시컬 스코프 또는 정적 스코프라고 부른다.
이미 제목으로 나왔기에 알아차릴 수 있듯이 JS 및 대부분의 언어는 정적 스코프를 따른다.
그럼 왜 1이 나왔는지는 간단해진다. 아까, js는 스코프 체인이 존재한다고 말했었고, 현재 스코프 체인은 다음과 같다.
전역 ← foo
← bar
또한, 함수에서 특정 변수를 쓸 일이 있을 때는 본인보다 상위의 스코프를 올라가면서 찾는다고 했다.
⇒ 그럼 뭐다? bar에는 x가 없으니까 전역 스코프에서 x를 찾아 출력한다!
⇒ 그렇기 때문에 1,1이 정답이다.
💡 렉시컬 스코프는 JS의 그 유명한 ‘클로저’와 깊은 연관이 있다. 클로저에 대해서도 알아보자.