December 15, 2019
클로저는 Javascript 고유의 개념이 아니라 함수를 일급객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 개념입니다. Javascript가 따르고 있는 ECMAscript 명세에는 클로저의 정의를 다루지 않기 때문에 MDN에서 클로저 정의를 살펴보겠습니다.
※ 1급 객체 : 변수에 담을 수 있고 인자로 전달할 수 있고 반환값으로 전달할 수 있으면 1급객체로 취급합니다.
MDN에서는 클로저를 다음과 같이 정의하고 있습니다.
A closure is the combination of a function bundled together (enclosed)
with references to its surrounding state (the lexical environment). In
other words, a closure gives you access to an outer function’s scope
from an inner function.
위 정의는 다음과 같이 해석할 수 있습니다.
- 클로저는 '함수'와 '렉시컬 환경'의 조합이다.
- 내부함수가 외부함수의 스코프에 접근할 수 있게한다.
MDN정의에 나오는 렉시컬 환경은 무엇일까요? 구성요소를 살펴보겠습니다.
lexical environment = {
environment record: -,
outer environment reference: -,
}
렉시컬 환경은 envirionment record와 outer environment reference로 구성되어 있습니다.
아래 코드를 보며 렉시컬 환경을 더 쉽게 설명하겠습니다.
function sum(x,y) {
var result = x + y;
function printResult() {
console.log("foo: "+ result);
}
return printResult;
}
var print = sum(10, 20);
print();
함수가 생성된 시점에 렉시컬 환경이 조성되며 스코프안에 있는 모든 지역 변수들을 environment record 와 outer environment reference가 다음과 같이 관리해주고 있습니다.
LexicalEnvironment = {
EnvironmentRecord : {
x: 10,
y: 20,
result : undefined,
printResult : function
},
OuterEnvironmentReference :
globalEC.lexicalEnvironment
}
즉 렉시컬 환경은 코드에서 변수나 함수 등의 식별자를 정의하고 관리해주는 개념으로 생각하면 됩니다. 클로저는 이러한 렉시컬 환경을 기억하고 있다가 자신이 선언됐을 때의 환경 밖에서 호출되어도 그 환경에 접근할 수 있습니다.
※ 참고로 렉시컬 환경은 개념(컨셉?)이라 Javascript 코드에서 실제로 접근할 순 없습니다.
클로저의 간단한 예제를 살펴보겠습니다.
function outter() {
var text = "Answer";
return function () {
console.log(text);
};
}
var closure = outter();
closure(); // 'Answer'
outter()
는 내부함수를 반환하고 있고, 반환된 내부함수는 외부함수 outter()
에서 선언된 text
라는 지역 변수를 참조하고 있습니다. 함수는 반환되는 순간 생을 마감합니다. 따라서 closure()
을 실행했을 때에 outter()
은 이미 죽은 상태입니다. 하지만 closure()
를 실행해보면 내부함수가 outter()
의 text
를 가지고 와서 실행해주는 것을 볼 수 있습니다. 이는 외부함수 outter()
가 소멸되었어도 내부함수의 외부함수 접근이 가능하다는 것 을 보여줍니다. 이는 클로저의 중요한 특징입니다.
※ 내부함수 : (외부)함수 안에서 선언된 함수를 뜻합니다.
다른 예제를 살펴보겠습니다.
function game(title) {
var gameTitle = title;
return function() {
console.log(gameTitle);
};
}
var overWatch = game('Over Watch');
var lol = game('League Of Legend');
overWatch(); // Over Watch
lol(); // League Of Legend
game
의 지역변수인 gameTitle
이 동적으로 바뀌는 것 처럼 보이지만 그렇지 않습니다. overWatch
와 lol
은 선언 당시의 렉시컬 환경을 각각 저장합니다. 따라서 overWatch
의 gameTitle
과 lol
의 gameTitle
은 각각 따로 생성된 녀석들 입니다. 클로저는 자신이 생성된 시점의 환경을 기억하였다가 호출되었을 때 기억해놓은 환경을 가지고와서 수행해줍니다. 이 또한 클로저의 중요한 특징입니다.
이렇게 생성될 때 마다 렉시컬 환경을 기억하면 메모리 차원에서 손해를 볼 수 있습니다. 그럼에도 클로저를 사용하는 이유를 알아보겠습니다.
OOP의 대표적인 언어인 Java는 변수/함수의 외부 접근을 방지 하기위해 private으로 변수/함수를 선언합니다. 클로저를 사용하면 이와 유사한 기능을 사용할 수 있습니다.
버튼을 클릭할 때 마다 count가 올라가는 기능을 구현한다고 합시다.
<button onclick="countBtnClick()">Click</button>
전역 변수를 선언하여 count를 올려줄 수 있지만 누구나 접근, 변경할 수 있어 의도치 않게 값이 변경될 수 있습니다.
var count = 0;
function countBtnClick() {
count += 1;
}
지역 변수를 사용하면 함수를 호출할 떄 마다 count값을 0으로 초기화하기 때문에 이전 상태를 기억하지 못합니다.
function countBtnClick() {
var count = 0;
count += 1;
}
클로저를 사용해보겠습니다. 즉시실행함수은 호출후에 바로 소멸하지만 즉시실행함수 내부의 함수가 클로저가 되면서 렉시컬 환경(count)을 기억하게 됩니다. 또 즉시실행함수의 지역변수인 count에 접근할 수 있습니다. count는 외부에서 직접 접근이 불가능하게 되고 private 하게 사용할 수 있습니다.
const countBtnClick = (function() {
var count = 0;
return function() {
count += 1;
}
})()
※ 너무나 흔한 예제인 반복문 클로저는 블록레벨스코프를 지원하는 let을 사용하면 해결할 수 있기 때문에 스킵하겠습니다.
function foo() {
var color = 'blue';
function bar() {
console.log(color);
}
bar();
}
foo();
bar
함수는 클로저일까요? bar
는 foo
안에서 정의되고 실행되었지만, foo
밖으로 나오지 않았기 때문에 클로저라고 부를 수 없습니다.
클로저는 각각의 환경을 갖고있기 때문에 메모리를 추가로 소모합니다. 따라서 참조를 제거하여 메모리를 release 시키는 것이 좋습니다.
function game(title) {
var gameTitle = title;
return function() {
console.log(gameTitle);
};
}
var overWatch = game('Over Watch');
var lol = game('League Of Legend');
overWatch(); // Over Watch
lol(); // League Of Legend
overWatch = null;
lol = null;