클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.
아래 코드에서 init()은 지역 변수 name과 함수 displayName()을 생성한다.
displayName()은 init() 안에 정의된 내부 함수이며 init() 함수 본문에서만 사용할 수 있다.
function init() {
var name = "Mozilla"; // name은 init에 의해 생성된 지역 변수
function displayName() {
alert(name); // 부모 함수에서 선언된 변수를 사용
}
displayName();
}
init();
여기서 주의할 점은 displayName() 내부엔 자신만의 지역 변수가 없다는 점이다. 그런데 함수 내부에서 외부 함수의 변수에 접근할 수 있기 때문에 displayName() 역시 부모 함수 init()에서 선언된 변수 name에 접근할 수 있다.
이 예시를 통해 함수가 중첩된 상황에서 파서가 어떻게 변수를 처리하는지 알 수 있다. "lexical"이란, lexical scoping 과정에서 변수가 어디에서 사용 가능한지 알기 위해 그 변수가 소스코드 내 어디에서 선언되었는지 고려한다는 것을 의미한다. 중첩된 함수는 외부 scope에서 선언한 변수에도 접근할 수 있다.
ES6 이전 JavaScript에서 var로 선언한 변수는 함수 내부 또는 외부에서 선언되었는지에 따라 함수 스코프 또는 전역 스코프 두 가지만 가졌기 때문에 중괄호로 표시된 블록이 스코프를 생성하지 않는다는 점에서 혼란을 일으켰다.
ES6부터 블록이 스코프로 취급되기 시작했지만, 이는 let과 const로 변수를 선언했을 때만 유효하다. 또한, ES6에서 모듈을 도입하면서 또 다른 스코프를 제공하게 되었다. 클로저는 이 모든 스코프의 변수를 캡처할 수 있다.
클로저는 함수와 함수가 선언된 어휘적 환경(코드가 어디서 실행되며 주변에 어떤 코드가 있는지, lexical 환경)의 조합이다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.
클로저(closure)란?
- 함수와 함수가 선언된 어휘적 환경(코드가 어디서 실행되며 주변에 어떤 코드가 있는지, lexical 환경)의 조합을 말한다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.
- 중첩함수이고 리턴된 함수가 외부함수의 변수를 참조하면 클로저이다.
1) 클로저의 개념 :
function makeFunc() {
var name = "KIHUN";
function displayName() {
alert(name);
}
return displayName; // 중첩함수 이고 리턴된 함수 (클로저)
}
var myFunc = makeFunc(); //myFunc변수에 displayName을 리턴 (유효범위의 어휘적 환경을 유지)
myFunc(); //리턴된 displayName 함수를 실행(name 변수에 접근)
위 예제에서 외부함수(makeFunc)은 내부함수(displayName)를 반환하고 생을 마감했다. 함수 안의 지역 변수들은 그 함수와 함께 생명주기를 마감하기 때문에 외부함수의 실행이 끝나면 name 변수에 더 이상 접근할 수 없어 보인다.
외부함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다.
그러나 외부함수의 렉시컬 환경은 내부함수에 의해 참조되고 있고, 내부함수는 내부함수의 인스턴스인 myFunc에 의해 참조를 유지하여 가비지 컬렉션의 대상이 되지 않는다. 따라서 외부함수의 생명주기가 끝났음에도 이 외부함수의 변수에 접근할 수 있다.
function makeAdder(x) {
var y = 1;
return function(z) {
y = 100;
return x + y + z;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
//클로저에 x와 y의 환경이 저장됨
console.log(add5(2)); // 107 (x:5 + y:100 + z:2)
console.log(add10(2)); // 112 (x:10 + y:100 + z:2)
//함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산
위 예제에서 add5와 add10은 같은 함수 본문 정의를 공유하지만 서로 다른 맥락(어휘)적 환경을 저장한다. 함수 실행 시 add5의 맥락적 환경에서 클로저 내부의 x는 5 이지만 add10의 맥락적 환경에서 x는 10이다. 또한 리턴되는 함수에서 초기값이 1로 할당된 y에 접근하여 y값을 100으로 변경한 것을 볼 수 있다. 이는 클로저가 리턴된 후에도 외부함수의 변수들에 접근 가능하다며 클로저에 단순히 값 형태로 전달되는것이 아니다.
클로저는 함수 실행이 끝나고 나면 함수 내부의 변수를 사용할 수 없는 일반적인 함수와는 달리, 외부 함수의 실행이 끝나더라도 외부 함수 내 변수가 메모리 상에 저장된다.(어휘적 환경을 메모리에 저장)
스코프로 변수값을 감싸지 않았다면, 이 값은 전역 변수여야 한다. 그러나 클로저가 이 변수값을 보존하고 있기 때문에 전역 변수를 만들 필요가 없다.
side effect
- side effect : 다른 함수 혹은 로직 등에 의해 의도하지 않은 변경을 초래하는 것
- 클로저를 통해 불필요한 전역 변수 사용을 줄이고, 스코프를 이용해 값을 보다 안전하게 다룰 수 있다.
// 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
const counter = (function () {
// 카운트 상태를 유지하기 위한 자유 변수
let counter = 0;
// 함수를 인수로 전달받는 클로저를 반환
return function (aux) {
// 인수로 전달받은 보조 함수에 상태 변경을 위임한다.
counter = aux(counter);
return counter;
};
}());
// 보조 함수
function increase(n) {
return ++n;
}
// 보조 함수
function decrease(n) {
return --n;
}
// 보조 함수를 전달하여 호출
console.log(counter(increase)); // 1
console.log(counter(increase)); // 2
// 자유 변수를 공유한다.
console.log(counter(decrease)); // 1
console.log(counter(decrease)); // 0
모듈화란 함수 재사용성을 극대화하여, 함수 하나를 완전히 독립적인 부품 형태로 분리하는 것을 말한다.
클로저를 통해 데이터와 메서드를 같이 묶어서 다룰 수 있다.
const makeCounter = () => {
let value = 0;
return {
increase: () => {
value = value + 1
},
decrease: () => {
value = value - 1
},
getValue: () => value
}
}
const counter1 = makeCounter();
counter1.increase(); // 1
counter1.increase(); // 2
counter1.getValue(); // 2
// getValue 메서드는 외부 함수에 선언된 value 값을 리턴하는 함수
const counter2 = makeCounter();
counter2.decrease(); // -1
counter2.decrease(); // -2
counter2.getValue(); // -2