스코프Scope는 '범위', '영역' 등을 뜻하는 영단어이다. 자바스크립트에서의 스코프Scope 역시 '범위'로 이해할 수 있다. 더 정확하는 바로 변수의 유효범위이다. 특정 변수에 접근하려고 할 때, 고민해야 할 것이 바로 스코프이다. 아래 예제를 살펴보자.
let username = "kimcoding";
if(username) {
let message = `Hello, ${username}!`;
console.log(message); // "Hello, kimcoding"
}
console.log(message); // ReferenceError
똑같이 console.log(message)
를 작성해주었지만, 해당 코드가 어떤 범위에 포함되어 있느냐에 따라 출력값이 달라지는 것을 볼 수 있다. 또, 살펴본 바와 같이 스코프의 가장 큰 특징은 "내부에서 외부로의 접근은 가능하지만, 외부에서 내부로의 접근은 가능하지 않다는 것"이다. 이제 왜 스코프에 유의해 코드를 작성해야 하는지 알 수 있다.
스코프는 크게 전역스코프와 지역스코프로 분류할 수 있다. 하나 미리 익혀두고 갈 점은 지역변수는 전역변수보다 더 높은 우선순위를 갖는다는 것이다.
만일 변수가 어떤 함수에도 속하지 않고 {}
에도 포함되어 있지 않다면, 우리는 그 변수를 전역변수라고 한다. (Node.js에서의 전역 변수 선언은 약간 다르지만, 웹브라우저 내부에서의 자바스크립트만 일단 고려한다.) 쉽게 말해 가장 바깥에 존재하는 스코프를 전역스코프라고 이해할 수도 있다. 전역 스코프에 변수를 선언하면 자바스크립트 코드 어디서든 불러 쓸 수 있다.
const globalVariable = 'global';
function foo () {
console.log(globalVariable)
}
foo() // 'global'
console.log(globalVariable) // 'global'
그러나! 결론부터 언급하면,
전역스코프에서의 변수선언은 최대한 지양하는 것이 좋다!
여러 개발자가 함께 코드를 짠다면, 얼마든지 네이밍 충돌 혹은 사이드이펙트가 발생할 수 있다. 그 과정에서 변수를 덮어쓸 수도 있으며, window 전역 객체의 속성을 덮어씌워버릴 수도 있다.
var console = "hello"
window.console //"hello"
console.log("hello") // 에러 발생!
변수를 선언하지 않고 할당하면 에러가 날 것 같지만, 그렇지 않다. 선언하지 않은 변수는 전역 객체의 프로퍼티가 된다.
코드 내 특정 구역에서만 사용할 수 있는 변수를 지역 변수라고 한다. 이제부터 살펴볼 함수스코프와 블록스코프가 바로 이 지역스코프의 두 종류이다.
변수의 접근범위를 구획하는 두 가지 방법이 있다. 즉, 스코프에는 두 가지 종류가 있다. 블록 스코프와 함수 스코프가 바로 그것들이다.
함수 스코프는 function 키워드가 등장하는 함수선언식 및 함수표현식에 의해 설정되는 스코프를 말한다. (예외적인 경우가 있는데, 화살표함수로 둘러싼 범위는 블록스코프로 취급된다!)
함수 내에서 변수를 선언하면, 우리는 함수 안에서만 이 변수에 접근할 수 있다. 함수 밖으로 나온 후에는 함수 내부에 있는 변수에 접근할 수 없다.
function sayHello () {
const hello = 'Hello CSS-Tricks Reader!';
console.log(hello);
}
sayHello(); // 'Hello CSS-Tricks Reader!'
console.log(hello) // Error, hello is not defined
블록 스코프는 쉽게 말해, 중괄호로 둘러싼 범위를 말한다. 중괄호로 감싸서 사용했던 조건문과 반복문 역시 블록스코프에 속한다.
{
const hello = "Hello block Scope";
console.log(hello) // "Hello block Scope"
}
console.log(hello) // Error, hello is not defined
var, let, 그리고 const는 함수스코프/블록스코프와 아주 밀접한 관계를 맺는 개념이다. 기본적으로 원래 자바스크립트는 함수스코프를 따랐지만, let과 const가 등장하며 블록스코프 형성이 가능해졌다
그 말인 즉,
var는 블록스코프를 무시하고 함수스코프만 따른다.
[예외] 화살표함수의 블록 스코프는 무시하지 않음!
떄문에 함수가 아닌 블록스코프에서 var를 사용해 변수를 선언한 경우, 해당 변수는 해당 블록의 구애를 받지않고, 외부에서 접근이 가능하다는 것을 알 수 있다.
for(var i = 0; i < 5; i++){
console.log(i)
}
console.log("final i: ", i) // 5
if(5 > 4) {
var secret = '12345';
}
secret // '12345'
위의 예시를 살펴보면 왜 var보다는 let이 권장되는지 알 수 있다. let을 사용하면, 함수스코프와 블록스코프가 모두 고려된다. 그러나 var은 블록스코프를 고려하지 않는다. 우리는 블록단위로 스코프를 구분해야 훨씬 더 예측가능한 코드를 작성할 수 있다. 때문에 함수스코프와 블록스코프를 모두 고려할 수 있는 let이 권장된다. 또한 let은 재선언을, const는 재할당을 방지할 수 있다!
지금까지 변수의 유효범위인 스코프에 대해 학습했다. 함수를 리턴하는 함수가 있을 때, 내부의 함수는 자신이 선언된 스코프(lexical scope)를 기억하며, 이는 매우 유용하게 활용된다. 이 내부함수를 우리는 클로저함수라고 한다. 흔히 함수 내에서 함수를 정의하고 사용하는 경우를 클로저라고 하는데, 대개 정의한 함수를 리턴하고 사용은 외부에서 하게 된다. 그렇다면, 클로저 함수의 세 가지 응용방식에 대해 탐구하도록 한다.
클로저는 외부함수의 실행이 끝나더라도, 외부함수 내 변수가 메모리 상에 저장된다. (어휘적 환경을 메모리에 저장하기 때문에 그렇다!)
const tagMaker = tag => content => `<${tag}>${content}</${tag}>`
const divMaker = tagMaker('div');
divMaker("hello")
divMaker("codestates")
const anchorMaker = tagMaker('a');
anchorMaker('go')
anchorMaker('urclass')
위와 같이 데이터를 스코프에 가두어 사용할 수 있다.
const makeCounter = () => {
let value = 0;
return {
increase: () => {
value = value + 1
},
decrease: () => {
value = value - 1
},
getValue: () => value
}
}
const counter1 = makeCounter();
위와 같이 선언하면 value에 직접 접근할 수 없다.
위 예시에서 살펴본 makeCounter 함수는 재활용이 가능하다.