First written: 23-02-17
Uploaded: 23-08-05
Last modified: 23-08-08
Closer
란 어떤 함수가 자신이 속해 있는 lexical scope를 기억했다가, 설령 그 lexical scope가 실행 과정에서 더 이상 필요가 없어졌다고 하더라도 해당 lexical scope가 차지하던 memory를 해제하는 것이 아니라, 함수가 그 scope에 접근을 마칠 때까지 유지시키는 기능을 뜻한다. 어떤 함수가 처음 선언된 lexical scope를 기억하는 행위를 모두 Closer
가 작동했다고 말할 수 있는 것이다.
말로는 너무 장황하니 차근차근 코드를 따라가보자.
function fnOuter() {
var a = 1;
function fnInner() {
console.log(a);
}
return fnInner;
}
var anotherFn = fnOuter();
anotherFn(); // 1
fnOuter의 lexical scope에 접근할 수 없어보이는(실제로도 그렇다) anotherFn이 variable a
에 접근했다! 무슨 일이 벌어진 걸까?
Variable anotherFn
에 할당하는 것을 끝으로 fnOuter()
의 실행이 끝났다. 그러므로 당연히 fnOuter()
의 lexical scope는 사라져야 한다. Garbige collector를 이용해 Engine이 사용되지 않는 memory를 해제할 것이니까. 하지만 놀랍게도 fnOuter의 lexical scope는 살아있다. fnInner()
는 여전히 fnOuter의 lexical scope에 대한 참조를 가진다. 그러므로 fnInner()
는 anotherFn
이 실행된 직후 자신이 실행되면서 참조를 유지하고 있던 fnOuter의 lexical scope에 접근해 a
의 값을 찾기 위해 RHS 검색을 실행한다. 이것이 바로 Closer
다.
이것을 더 극적으로 보여주기 좋은 예시는 아래와 같다.
var fn;
function foo() {
var a = 1;
function baz() {
console.log(a);
}
fn = baz;
}
function bar() {
fn();
}
foo();
bar(); // 1
setTimeout을 가지고도 Closer
를 느껴볼 수 있다.
function getMessage(message) {
setTimeout( function timer(){
console.log(message);
}, i*1000);
}
가장 흔하게 Closer
의 예시로 사용되는 반복문도 살펴보자. setTimeout
을 곁들여서.
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000);
}
// 6
// 6
// 6
// 6
// 6
우리는 1초 뒤부터 1초 간격을 두고 실행되어 차례로 1, 2, 3 ,4, 5를 볼 수 있기를 바랐다. 하지만 문제는 i가 global scope에 선언되었고, setTimeout 함수는 for 반복문이 다 돌아 i가 6이 된 이후에야 global scope에서 i를 찾아내 출력하게 된다는 것이다.
우선은 각각의 반복문이 실행될 때 반복문이 각각의 scope를 갖게 만들어보자. 기억력이 좋다면, 이전의 글 "Functional Scope"에서 본 IIFE가 기억날 것이다. IIFE가 실행될 때마다 그들은 자신만의 scope를 가지고, global scope(혹은 상위 scope)를 오염시키지 않는다는 장점을 가지고 있었다.
for (var i=1; i<=5; i++) {
(function (){
setTimeout( function timer() {
console.log(i);
}, i*1000);
})();
}
// 6
// 6
// 6
// 6
// 6
하지만 여전히 6만을 내뱉고 있다. 눈치가 빠르다면 여기서의 문제가 무엇인지는 눈치챘을지 모르겠다. 문제는 자신만의 scope 내에 당시의 i값을 저장하지 못했기 때문에 결국 한 단계 위 scope에 가서 i값을 찾아 출력하고 있는 것이다. 아래와 같이 i값을 그때그때 IIFE lexical scope에 저장해주면 드디어 우리가 원하던대로 코드가 돌아간다.
for (var i=1; i<=5; i++) {
(function (){
var j = i;
setTimeout( function timer() {
console.log(j);
}, j*1000);
})();
}
// 1
// 2
// 3
// 4
// 5
for (var i=1; i<=5; i++) {
(function (j){
setTimeout( function timer() {
console.log(j);
}, j*1000);
})(i);
}
// 1
// 2
// 3
// 4
// 5
"Functional Scope" 글을 보고 왔다면 두 코드는 style의 차이일 뿐이라는 걸 알 수 있을 것이다.
자, 혹시 let
과 const
를 기억하는가? let
은 자신이 속한 block을 closed scope로 바꾸는 능력이 있다. IIFE를 활용한 이유와 같은 이유로, let
을 활용해보자.
for (var i=1; i<=5; i++) {
let j = i;
setTimeout( function timer() {
console.log(j);
}, j*1000);
}
// 1
// 2
// 3
// 4
// 5
더 간단하게는 아래와 같이 쓸 수 있다.
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000);
}
// 1
// 2
// 3
// 4
// 5
let 선언문이 for문 조건 안에서 실행되면 반복문 도입부에서 let으로 선언된 variable은 한 번 선언되고 끝나는 것이 아니라, 반복할 때마다 선언되게 된다. 그러므로 위의 let j=i;
가 생략되어도 우리가 원해는대로 코드가 동작한다. Block scope와 Closer
가 협동해 마법을 부린 것이다.
흔하디 흔한 module들에서도 역시 Closer
가 열일하고 있다.
function someModule() {
var name = "good module";
var datas = [1, 2, 3, 4, 5];
function showDatas() {
console.log(datas);
}
function showUpperName() {
console.log(name.toUpperCase());
}
return {
showDatas: showDatas,
showUpperName: ShowUppername
}
}
var exampleModule = someModule();
exampleModule.showDatas(); // [1, 2, 3, 4, 5]
exampleModule.showUpperName(); // GOOD MODULE
눈치를 챈 사람도 있겠지만, 결국 모듈도 Closer
를 사용하기 위해선 두 가지 조건을 만족해야 한다.
Closer
)를 가져야 한다.IIFE pattern으로 module을 바꾸거나 parameter로 값을 입력받을 수도 있다. 위에서부터 보아왔다면 중복되는 내용임을 충분히 알 수 있으니 생략하도록 한다.
참고로, ES6부터는 각각의 file을 하나의 module처럼 인식한다. 하여, 예시로 들었던 function 기반의 module은 runtime 이전까지 해석되지 않는 반면, ES6 module은 cmopiler가 이미 참조들 간의 관계를 알고 있고, 실제 참조들이 유효한지를 compile 시에 체크한다. 만일 오류가 있으면 초기에 오류를 발생시킨다. Runtime까지 기다리지 않는 것이다.
참고 서적 : 카일 심슨, 2017, 한빛미디어, 『YOU DON'T KNOW JS: 타입과 문법, 스코프와 클로저』