안녕하세요. 이번 시간에는 함수 스코프(scope)에 대해서 설명드리겠습니다. 스코프는 범위라는 뜻입니다.
전역 변수와 지역 변수
자바스크립트에서 주로 변수를 사용해 데이터를 저장했었는데요. var로 선언했었죠? 물론 변수를 만드는 일은 문제가 되지 않지만, 전역(global)변수를 만드는 일은 최대한 지양해야합니다. 전역변수란 자바스크립트에서 제일 바깥 범위(함수 안에 포함되지 않은)에 변수를 만드는 건데요. window 객체에 변수를 만드는 겁니다.
var x = 'global';
function ex() {
var x = 'local';
x = 'change';
}
ex(); // x를 바꿔본다.
alert(x); // 여전히 'global'
다음을 보시면 같은 x여도 ex 함수 바깥의 x는 전역변수고, ex 함수 안의 x는 ex 함수의 지역변수입니다. 지역 변수는 함수 안에 들어있는 변수를 의미합니다. var x = 'global'
을 한 게 왜 전역변수이고, window 객체의 속성이 되는지 모르겠다면, window 강좌 를 다시 복습하세요!
스코프(Scope)
위의 상황에서 지역변수는 아무리 해도 전역변수에 영향을 끼칠 수 없습니다. 바로 함수 스코프 때문이죠. 범위라는 말처럼 함수 안에서 선언된 변수는 해당 함수 안에서만 사용할 수 있습니다. var x = 'local'
은 ex 함수 안에서만 그 데이터를 사용할 수 있습니다. 그 밑에 x = 'change'
도 함수 안의 지역변수 x를 바꾸는 겁니다. 밑에 아까와는 살짝 다른 예를 보시죠.
var x = 'global';
function ex() {
x = 'change';
}
ex();
alert(x); // 'change'
아까와는 달리 ex 함수에 안에서 var을 선언하지 않았습니다. 이제는 x = 'change'
를 했을 때 전역변수가 바뀌는데요. 자바스크립트는 변수의 범위를 호출한 함수의 지역 스코프부터 전역 변수들이 있는 전역 스코프까지 점차 넓혀가며 찾기 때문입니다. 함수 ex의 범위 안에 x가 없기 때문에 더 넓은 범위인 전역 스코프에서 찾는거죠. 정확한 원리는 다음 시간 실행 컨텍스트에서 배울 수 있습니다. 첫 번째 예에서는 ex의 범위 안에 바로 x(ex의 지역변수)가 있었기 때문에 지역변수 x를 바꾸고, 전역변수 x는 바꾸지 않았습니다.
스코프 체인
바로 전역변수와 지역변수의 관계에서 스코프 체인(scope chain)이란 개념이 나옵니다. 내부 함수에서는 외부 함수의 변수에 접근 가능하지만 외부 함수에서는 내부 함수의 변수에 접근할 수 없습니다. (아래 enemy가 undefined인 것을 보시죠) 그리고 모든 함수들은 전역 객체에 접근할 수 있죠. 위의 예제와 비슷한 예제를 하나 더 보여드리겠습니다.
var name = 'zero';
function outer() {
console.log('외부', name);
function inner() {
var enemy = 'nero';
console.log('내부', name);
}
inner();
}
outer();
console.log(enemy); // undefined
inner 함수는 name 변수를 찾기 위해 먼저 자기 자신의 스코프에서 찾고, 없으면 한 단계 올라가 outer 스코프에서 찾고, 없으면 다시 올라가 결국 전역 스코프에서 찾습니다. 다행히 전역 스코프에서 name 변수를 찾아서 'zero'라는 값을 얻었습니다. 만약 전역 스코프에도 없다면 변수를 찾지 못하였다는 에러가 발생합니다. 이렇게 꼬리를 물고 계속 범위를 넓히면서 찾는 관계를 스코프 체인이라고 부릅니다.
렉시컬 스코핑(lexical scoping)
많이들 헷갈리는 개념인데 스코프는 함수를 호출할 때가 아니라 선언할 때 생깁니다. 호출이 아니라 선언요! 정적 스코프라고도 불립니다. 다음 코드에서 console이 어떻게 찍힐 지 예상해보세요.
var name = 'zero';
function log() {
console.log(name);
}
function wrapper() {
name = 'nero';
log();
}
wrapper();
쉽죠? nero입니다. log를 호출하기 전에 name을 'nero'로 바꿨거든요. 그럼 다음은요? 문제를 살짝 바꿨습니다.
var name = 'zero';
function log() {
console.log(name);
}
function wrapper() {
var name = 'nero';
log();
}
wrapper();
똑같이 nero 아니냐고요? 땡! zero입니다. 스코프는 함수를 선언할 때 생긴다고 했죠? log 안의 name은 wrapper 안의 지역변수 name이 아니라, 전역변수 name을 가리키고 있는 겁니다. 이런 것을 lexical scoping이라고 합니다. 한글로는 어떻게 번역해야 할지 모르겠네요. 직역하면 어휘적 범위인데 좀 어색하죠? 정적 스코프가 더 나아 보이네요.
lexical scoping이 좀 이해하기 힘들기 때문에 다시 설명드리겠습니다. 함수를 처음 선언하는 순간, 함수 내부의 변수는 자기 스코프로부터 가장 가까운 곳(상위 범위에서)에 있는 변수를 계속 참조하게 됩니다. 위의 예시에서는 log 함수 안의 name 변수는 선언 시 가장 가까운 전역변수 name을 참조하게 됩니다. 그래서 wrapper 안에서 log를 호출해도 지역변수 name='nero'를 참조하는 게 아니라 그대로 전역변수 name의 값인 zero가 나오는 겁니다.
무슨 짓을 해도 log 함수가 한 번 선언된 이상, 전역변수를 가리키게 되어있는 name 변수가 다른 걸 가리키게 할 수 없습니다. 유일한 방법은 아까처럼 전역변수를 다른 값으로 바꾸는 겁니다. 다음 시간에 실행 컨텍스트에 대해서 배울텐데 그 때 정확한 원리를 설명드리겠습니다.
전역변수를 만드는 일은 지양하라고 했는데, 그 이유는 변수가 섞일 수 있기 때문입니다. 자바스크립트 앱을 만들면서 혼자만 개발하는 게 아니라, 여러 명과 협동도 하고, 다른 사람의 라이브러리(자바스크립트 코드 모음)를 사용하는 일도 많습니다. 그런데 전역변수를 사용하다보면, 우연의 일치로 인해 같은 변수 이름을 사용해서 이전에 있던 변수를 덮어쓰는 불상사가 발생할 수 있습니다. (특히 $나 _같은 유니크한 변수들은 경쟁이 치열합니다)
간단한 해결 방법은 전역 변수 대신 한 번 함수 안에 넣어 지역변수로 만드는 겁니다. 아니면 객체 안의 속성으로 만들 수도 있습니다.
var obj = {
x: 'local',
y: function() {
alert(this.x);
}
}
위 처럼 하면 obj.x
, obj.y()
이렇게 접근해야 하기 때문에 다른 사람과 섞일 염려가 없죠. obj를 통째로 덮어쓰지 않는 이상은요. 전역변수를 하나로 최소화해서 변수가 겹칠 우려도 최소화하는 거죠.
이런 방법을 네임스페이스를 만든다고 표현합니다. obj라는 고유 네임스페이스를 만들어서 겹치지 않게 하는 거죠. 대부분의 라이브러리가 네임스페이스를 사용하고 있죠. naver는 jindo, facebook은 FB, jquery는 jQuery(또는 $)같이요.
하지만, 위 방법의 단점은 누군가 고의적으로 x와 y를 바꿀 수 있습니다. 코드 밑에 스크립트를 추가해서요. obj를 통째로 바꾸지 않더라도 밑에 obj.x = 'hacked';
라고 한 줄 추가만 하면 obj.y();
를 했을 시 local 대신 hacked가 alert됩니다. 그것을 방지하려면
var another = function () {
var x = 'local';
function y() {
alert(x);
}
return { y: y };
}
var newScope = another();
조금 복잡하지만 위와 같이 하면 됩니다. another();
하는 순간 return에 의해 { y: function () { alert(x) } };
가 newScope에 저장됩니다. 이제 newScope라는 네임스페이스를 통해서 y에 접근할 수 있습니다. x는 접근할 수 없죠. 위처럼 함수로 감싼 후 return을 통해 공개할 변수(y)만 공개하고 비공개할 변수(x)는 비공개하는 방법을 취할 수 있습니다. 즉, return하는 변수는 공개 변수고, 다른 것은 비공개 변수인거죠.
위의 코드를 간략하게 바꾸면
var newScope = (function () {
var x = 'local';
return {
y: function() {
alert(x);
}
};
})();
위와 같이 쓸 수 있습니다. another같은 변수를 한 번 거치는 대신, newScope에 바로 집어넣은거죠. 처음보는 게 나왔는데요. (function() {})();
구문입니다. IIFE(즉시 호출 함수 표현식)이라고도 하고, 모듈 패턴이라고도 하는데, 함수를 선언하자마자 바로 실행시켜버리는 거죠. 함수를 function() {}
로 선언하면서 동시에 ()를 붙이니까 즉시 실행됩니다. 이 구문이 라이브러리를 만들 때 기본입니다. 많은 라이브러리가 이 구문을 활용하고 있습니다. 비공개 변수가 없는 자바스크립트에 비공개 변수 기능을 만들어주기 때문이죠. 이 패턴은 꼭 기억하고 있어야합니다!
위의 패턴의 원리를 알려면 실행 컨텍스트에 대해 알아야 합니다. 다음 시간에는 제일 중요한 개념인 실행 컨텍스트와 그것을 활용한 클로저에 대해 알아보겠습니다.
2. 변수 값을 판단하는 기준은 scope chain에 따라서 위로 물고 물면서 올라가서 값을 지정한다.
3. 렉시컬 스코프에 의해서 변수든 함수든 선언된 순간 스코프가 정해진다.
4. 비동기 함수(setTimeout 함수)안에 있는 function() 안에 있는 변수의 값은 "실행" 될때 결정된다.
여기서 4번에서 비동기 함수안에서의 콜백함수에서의 변수값은 실행될때 결정되므로 렉시컬 스코프가 적용이 되지 않는 예외적인 상황이 맞나요?
setTimeout(function() {
console.log(i)
}, j * 1000);
}
강의에서 이 코드를 예를 드셨는데 setTimeout(function(){console.log(j)}, j*1000)에서의 콜백함수인 function(){console.log(i)}도 렉시컬 스코프를 따른다는건가요?
for(var i=0; i\u003c100; i++){
setTimeout(function(){ console.log(i) }.i*1000)
} 에서
비동기함수인 setTimeout의 파라미터로 있는 익명함수의 변수값은 setTimeout이 실행되고나서 결정된다는거네요. 렉시컬 스코프에 의해서 익명함수가 선언된 순간 익명함수에 있는 변수 i의 scope는 for(var i=0; i\u003c100; i++)가 있는 global scope가 되는거죠 ?