앞서 var와 let의 차이점이라는 게시물에서 언급했지만 클로저에 대해 좀 더 자세히 공부해보고자 게시글을 남깁니다. 대략적인 감을 잡았다고 생각했었는데, 비슷한 예임에도 실행 결과 예측이 힘들어서 공부의 필요성을 느꼈기 때문입니다. 게시글에서 틀린 부분이 있거나 혹 문제가 되는 부분이 있다면 댓글 남겨주시면 감사하겠습니다. 글의 목차는 다음과 같습니다.
1. 클로저란
- 함수
- 렉시컬 스코프
2. 클로저의 예
_____
클로저란
클로저는 자바스크립트에서 중요한 개념 중 하나이지만 자바스크립트 고유의 개념이 아니기 때문에 ECMAScript 명세에 등장하지 않는다고 한다. 다만 MDN의 정의를 살펴보면 다음과 같다.
클로저는 함수와 함수가 선언된 어휘적 환경(Lexical environment)의 조합이다.
이 문장을 이해하기 위해서는 두 가지 키워드를 이해해야 한다. 바로 함수와 렉시컬 스코핑[1]이다. 먼저 함수에 대해 살펴보면, 자바스크립트의 함수는 자바의 그것과는 다른 일급 객체[2][3]다. 코드로 살펴보자.
function f1() {
var a = 1;
return a;
}
var a = f1();
console.log(a);
이미 알고 있듯 var 지역 변수는 함수 스코프를 갖는다. 함수 f1이 실행된 후 지역변수 a의 생명 주기는 끝나고 전역 변수 a에 1이 세팅된다.
function f1() {
var a = 1;
return function f2() {
return a;
}
}
var f = f1();
var a = f();
console.log(a);
자바스크립트에서 함수는 일급 객체이기에 변수와 마찬가지로 리턴값으로 쓰일 수 있다. 위 코드에서 f2는 a를 반환하는데 a는 f2에 선언되지 않았으므로 상위 스코프인 f1에 선언된 a의 값을 반환한다[4][5]. 다만 이 경우 f1 함수가 실행된 후에도 f2 함수가 지역 변수 a를 물고 있게 된다. 당연한 거 아닌가? 하고 생각할 수 있지만 다음 코드를 한 번 살펴보자.
var name = "zero";
function log() {
console.log(name);
}
function wrapper() {
name = "nero";
log();
}
wrapper(); // nero
위 코드의 실행 결과는 nero다. 호출되기 전 name 변수가 변화되었기 때문이다. 그렇다면 다음 코드는 어떨까?
var name = "zero";
function log() {
console.log(name);
}
function wrapper() {
var name = "nero";
log();
}
wrapper(); // zero
실행 결과는 zero이다. 그 이유는 자바스크립트(ES5 이전)는 함수 레벨의 렉시컬 스코프(Lexical scope) 규칙을 따르기 때문이다. 렉시컬 스코프란 정적 스코프로 호출 위치가 아닌 선언 위치에 따라 상위 스코프를 결정한다는 것이다. 즉 렉시컬 스코프 규칙을 따르는 자바스크립트의 함수는 호출 스택과 관계 없이 각각의 대응표를 소스 코드 기준으로 정의하고, 런타임에 그 대응표를 변경시키지 않는다고 한다[6]. 즉 위에서 log 함수 호출 시에 name을 호출 위치 기준이 아닌 선언 위치 기준으로 찾기에 zero를 출력하는 것이다.
정리하면 외부 스코프를 참조하는 함수가 반환되는 경우[7], 그 함수를 클로저라고 부른다(고 이해했다).
클로저의 예
가장 많이 알려진 예는 var와 let의 차이점으로도 알려진 다음 코드다.
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
위 코드의 의도는 1, 2, 3이 차례로 찍히는 것이었겠으나 3, 3, 3이 찍힌다. 자바스크립트의 특성인 함수 레벨의 렉시컬 스코프를 염두해두고 살펴보면 이해할 수 있을 것이다. 의도대로 실행 결과를 출력하도록 할 수 있는 방법[8] 중 하나가 바로 클로저다. 아래와 같다.
var funcs = [];
function createfunc(i) {
return function() {
console.log("My value: " + i);
};
}
for (var i = 0; i < 3; i++) {
funcs[i] = createfunc(i);
}
for (var j = 0; j < 3; j++) {
// and now let's run each one to see
funcs[j]();
}
위 코드를 보면 createfunc 함수가 반환하는 익명 함수는 이전과 다르게 상위 스코프(createfunc 함수)를 참조한다. 따라서 실행 결과는 의도대로 1, 2, 3이 차례로 출력된다. 비슷한 예로 이벤트 리스너를 바인딩하는 코드가 있다.
<button>0</button>
<button>1</button>
<button>2</button>
<script>
var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function() {
console.log("My value: " + i);
});
}
</script>
이미 살펴보았다시피 위 코드는 어떤 버튼을 누르던 3이 출력된다. 이벤트 리스너 함수가 출력해야 하는 변수 i는 상위 렉시컬 스코프 i를 바라보고 있으며, 호출 시점 즉 사용자가 버튼을 누르는 시점에서는 i의 값이 3이기 때문이다.
var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {
(function(i) {
buttons[i].addEventListener("click", function() {
console.log("My value: " + i);
});
})(i);
}
위와 같이 즉시 실행 함수 표현[9][10]을 이용하여 원하는 실행 결과를 얻을 수 있다(리턴이 없는데 이게 클로저인지는 의문이다. 클로저라고 말하는 곳도 있는 것 같아서). 한편 반복문 내부의 setTimeout과 같은 비동기 함수를 사용하는 경우 역시 비슷한 예다.
for(var i = 0; i < 3; i++) {
setTimeout(function (){
console.log(i);
}, 1000);
}
위의 경우 출력 결과는 3, 3, 3이다. 아래와 같이 클로저를 이용하여 1, 2, 3의 결과를 얻을 수 있다.
for(var i = 0; i < 3; i++) {
setTimeout((function(i){
return function(){
console.log(i);
}
})(i), 1000);
}
_____
1. 다음 글에 따르면 어휘적 환경은 렉시컬 스코프라고 이해해도 된다. 자바스크립트는 동적 스코프가 아닌 렉시컬 스코프를 따르기 때문이다.
2. JavaScript Functions are First-Class Citizens
3. 클로저는 자바스크립트 고유의 개념이 아니라 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.
4. 자바스크립트 엔진이 식별자를 찾을 때 자신이 속한 스코프에서 찾고, 없으면 상위 스코프에서 찾는 현상을 스코프 체인이라고 한다.
5. 즉 f2 함수는 outer environment 참조로 f1의 enviroment를 저장한다.
6. 사실 런타임에 렉시컬 스코프를 수정할 수 있는 방법들(eval, with)이 있으나 권장하지 않는다(참고).
7. 클로저의 의미는 참조하는 상위 렉시컬 스코프 변수의 생명 주기를 끝낸다는 의미가 내포되어 있다고 한다(참고). 따라서 리턴문이 없다면 클로저가 아니라고 할 수 있다.
8.
9. 즉시 실행 함수 표현(IIFE, Immediately Invoked Function Expression)은 정의되자마자 즉시 실행하는 자바스크립트 함수를 말한다.
10. 클로저와 IIFE 개념을 사용하여 카운터 등 변수 선언을 할 수 있다. 다음 글의 각주 12를 참고하자.
11.
_____
참고
- PoiemaWeb] Closure
- MDN] 클로저
- Youtube
- Taehoon] 자바스크립트 클로저, 쉽게 이해하기
- 뉴렉처] 클로저
- 이화랑] 스코프와 스코프 체인, 렉시컬 스코프, 클로저
- NHN Cloud Meetup] 자바스크립트의 스코프와 클로저
- shldhee] Scope, Lexical scoping, Closure
- .
'공부 > JavaScript' 카테고리의 다른 글
배열 그룹화 (0) | 2021.12.21 |
---|---|
JavaScript 참고자료 (0) | 2021.07.31 |
배열 API (0) | 2021.07.19 |
Date validation (0) | 2021.04.10 |
JSON (0) | 2021.01.31 |
댓글