[자바스크립트 닌자] 클로저가 무엇이고 왜 쓰는걸까?(2)

이전 게시글에 이어 클로저를 사용하는 방법들에 대해 추가적으로 알아보자.


즉시 실행 함수에 클로저를 적용하기

이전 글에서 클로저로 private 변수 효과를 내는 법을 배웠었다. 그 효과와 즉시 실행 함수를 결합하면 독립된 공간을 쉽게 구성할 수 있다.

즉시 실행 함수란?
선언하자 마자 실행되고 소멸되는 함수를 말한다.
ex) (()=>{})(); 또는 (function(){})();

우선 일반적인 button 객체에 클릭 이벤트를 붙이는 방식을 살펴보자.

1
2
3
4
5
6
var numClicks = 0;
const btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
	numClicks++;
	console.log(numClicks);
});

이러한 방식으로 상위 변수의 numClicks를 참조하는 이벤트를 붙인다면 numClicks라는 변수는 다른 곳에서도 겹칠 가능성이 존재하게 된다.

1
2
3
4
5
6
7
8
(() => {
	var numClicks = 0;
	const btn = document.querySelector('#btn');
	btn.addEventListener('click', () => {
		numClicks++;
		console.log(numClicks);
	});
})();

하지만 이벤트를 붙이는 함수를 즉시실행 함수 내에 설정해두면 numClicks라는 변수는 클로저에 의해 btn객체만 참조할 수 있으며 다른 곳에서는 참조가 불가능한 독립적인 공간이 생겨서 변수 오염이 줄어들게 된다.

이처럼 이벤트 기능을 변수와 같이 붙일 때 즉시실행함수와 클로저를 결합하면 간결한 코드가 가능해진다.


클로저를 이용해 메모이제이션 기법을 자동 적용하기 (함수 동작 오버라이딩)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Function.prototype.memoized = function (key) {
	this._values = this._values || {}; // _values는 실행된 값들을 저장하는 캐시이다.
	// 만약 해당 인자의 함수 실행값이 없다면 함수를 실행하고 있다면 캐시에서 값만 return한다.
	return this._values[key] !== undefined
		? this._values[key]
		: (this._values[key] = this.apply(this, argument));
};

function isPrime(num) {
	var prime = num != 1;
	for (var i = 2; i < num; i++) {
		if (num % i == 0) {
			prime = false;
			break;
		}
	}
	return prime;
}
// 소수인지 검사하는 함수를 통해 테스트를 해보면 첫 실행만 함수를 실행하고 _values 캐시에 값을 저장한걸 확인할 수 있다.
isPrime.memoized(5);
console.log(isPrime._values[5]);

메모이제이션이란 이전의 연산 결과를 기억하는 함수를 만들어 내서 다시 함수를 실행하는 과정을 줄여주는 성능 최적화 기법이다.

이러한 방식을 구현한 과정을 위의 코드에서 나타냈는데 클로저가 사용되지 않은 방식이다.

여기서 문제점은 사용자가 함수를 실행할때 memoized를 직접 의식하고 실행해야 한다는 것이다. (함수 실행할때마다 이렇게 추가적으로 작성해야하면 코드도 길어지고 힘들어진다)

하지만 클로저를 이용하면 자동으로 메모이제이션을 지원하는 방법을 구현할 수가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Function.prototype.memoized = function (key) {
	this._values = this._values || {};
	return this._values[key] !== undefined
		? this._values[key]
		: (this._values[key] = this.apply(this, argument));
};

Function.prototype.memoize = function () {
	var fn = this;
	return function () {
		return fn.memoized.apply(fn, arguments);
	};
};

const isPrime = function (num) {
	var prime = num != 1;
	for (var i = 2; i < num; i++) {
		if (num % i == 0) {
			prime = false;
			break;
		}
	}
	return prime;
}.memoize();

console.log(isPrime(5));

이전의 코드에서 memoize 를 추가하고 isPrime을 기존함수에 memoize를 적용한것으로 할당하였다. 해당 방식의 원리는 다음과 같다.

  1. memoize 함수 에서 isPrime에 저장한 익명함수를 가리키는 this를 사라지지 않게(this 자체는 클로저에 적용되지 않는다) fn 변수에 할당해서 클로저를 적용시킨다.
  2. isPrime에 메모이제이션 호출 적용함수를 저장한다.
  3. isPrime을 호출하면 function () { return fn.memoized.apply(fn, arguments); }; 이 실행되고 fn은 클로저를 통해 소멸되지 않았으므로 정상적으로 실행된다.

이렇게 하면 함수를 호출할 때마다 사용자는 메모이제이션을 의식하지 않고 자동으로 사용할 수 있게 된다.


클로저를 사용하면 지금까지 설명한 방식들 뿐만 아니라 함수 context 바인딩(내장된 bind기능과 유사한)을 구현할 수도 있으며 react hook에서 제공하는 useState 와 같은 기능들을 구현할수도 있다(실제로 useState를 구현하는데 클로저를 이용했기 때문).

어떻게 보면 다른 언어들과 크게 차이나는 기능은 아닌것처럼 보일수도 있지만 함수형 언어에서 매우 중요한 기능이다.