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

클로저란 자바스크립트의 특징적인 기능으로 함수와 긴밀한 관계를 지니고 있다.

클로저를 활용하면 페이지에 필요한 js소스코드의 양과 복잡함을 줄일 수 있다. 또한, 클로저 없이는 다룰 수 없는 문제, 또는 구현하기 까다로운 문제들도 다룰 수 있게 되므로 꼭 알아야 하는 개념이다.

클로저(Closure)란?

예시 1

우선 간단한 예제 코드를 통해 클로저를 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function assert(value, text) {
	value ? console.log(text) : console.log('error');
}

var outerValue = 'ninja';
var later;

function outerFunction() {
	var innerValue = 'samurai';

	function innerFunction() {
		assert(outerValue, 'Inner can see the ninja'); // 전역 변수를 참조하는건 당연히 가능하다.
		assert(innerValue, 'Inner can see the samurai'); // 하지만 outerFunction 함수가 끝나도 innerValue를 참조하는게 가능할까?
	}
	later = innerFunction; // 전역 변수에 innerFunction을 대입.
}
outerFunction();
later(); // outerFunction의 실행이 끝나고 later 함수 실행.

위의 코드를 보면 later를 호출할 때 outerValue의 값은 전역변수 이므로 assert의 value가 존재하는게 확실하다. 하지만 later를 실행할 때 outerFunction 내부의 유효범위는 이미 사라진 뒤에 innerValue의 값을 호출하는 것은 가능할까?

클로저가 없었다면 안되는게 정상이지만 자바스크립트에선 innerValue 의 값을 확인할 수 있다.

이러한 이유는 innerFunction을 클로저라는 보호막이 감싸고 있으며, innerFunction()의 클로저는 해당 함수가 존재하는 한, 함수의 유효 범위와 관계된 모든 변수를 가비지 컬렉션으로부터 보호하기 때문이다.

js-ninja

이처럼 책에서는 클로저를 보호막으로 표현하고 있다.

함수의 유효범위에 대해 잘 모른다면 링크를 참고하자 https://medium.com/@yeon22/javascript-%EC%8A%A4%EC%BD%94%ED%94%84-scope-%EB%9E%80-bc761cba1023

예시 2

다음은 클로저의 추가적인 특징도 포함한 예제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function assert(value, text) {
	value ? console.log(text) : console.log('error');
}

var outerValue = 'ninja';
var later;

function outerFunction() {
	var innerValue = 'samurai';

	function innerFunction(paramValue) {
		assert(outerValue, 'Inner can see the ninja');
		assert(innerValue, 'Inner can see the samurai');
		// 함수의 인자도 클로저에 포함될까?
		assert(paramValue, 'Inner can see the wakizashi');
		// 함수 선언 뒤에 선언된 변수도 참조가 가능한가?
		assert(tooLate, 'Inner can see the ronin');
	}
	later = innerFunction;
}
assert(!tooLate, "Outer can't see the ronin");
outerFunction();
var tooLate = 'ronin';
later('wakizashi');

위의 예제는 함수의 인자를 참조하는 assert와, tooLate라는 전역 변수를 참조하는 assert가 추가되었다.

클로저를 통해서 위의 테스트 assert도 모두 통과하게 된다.

이러한 예시를 통해서 클로저를 정리해보자

정리

  • 클로저는 해당 함수가 존재하는 한, 함수의 유효 범위와 관계된 모든 변수를 가비지 컬렉션으로부터 보호하는 일종의 보호막 개념이다.

  • 이러한 클로저는 여러가지 특징을 보유한다.

    • 함수 매개변수는 함수의 클로저에 포함되어 있다.
    • 함수 밖 유효 범위에 속한 모든 변수는, 심지어 함수를 선언한 뒤에 선언된 변수일지라도, 모두 클로저에 포함된다.
    • 하지만 같은 유효 범위에 속한 변수라 하더라도, 선언에 앞서 참조하는 것은 불가능하다.

이러한 클로저의 개념을 통해서 어떻게 적용할 수 있는지 방법들을 알아보자.

클로저로 Private 변수 효과 내기

클로저를 사용하는 한가지 경우는 변수의 유효범위를 제한하려는 용도로 사용하는 것이다. 자바스크립트로 객체 지향 코드를 작성할 때는 전통적인 형태의 private 변수를 사용할 수 없는데, 클로저를 이용하면 비슷한 기능을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function assert(value, text) {
	value ? console.log(text) : console.log('error');
}

function Ninja() {
	var feints = 0;

	this.getFeints = () => feints;
	this.feint = () => {
		feints++;
	};
}

var ninja = new Ninja(); // 함수를 new로 생성하면 함수내 this는 ninja context를 참조한다.
ninja.feint();

assert(ninja.getFeints() == 1, '생성자 함수 내부에 있는 feint 변수의 값은 얻어올 수 있다.');
assert(ninja.feints === undefined, '하지만 private변수에 직접 접근할 수 없다.');

위의 예시를 실행하면 assert테스트가 모두 통과한다. 생성자 외부에서는 생성자 함수 내에 변수를 접근할 수 없지만 함수 내의 메서드들은 클로저를 통해 변수에 접근할 수 있기 때문에, 메서드를 호출하는 사용자에게는 변수를 직접 노출하지 않으면서 메서드 내에서는 객체의 상태를 관리하는 것이 가능해진다.


클로저로 콜백과 타이머 다루기

우선 타이머함수를 다루는 예제를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function assert(value, text) {
	value ? console.log(text) : console.log('error');
}

function animateIt() {
	let tick = 0;
	var timer = setInterval(() => {
		if (tick < 100) {
			tick++;
		} else {
			clearInterval(timer);
			assert(tick == 100, '클로저를 통해 tick변수에 접근한다.');
			assert(timer, 'timer 변수에 대한 참조 역시 클로저를 통해 얻을 수 있다.');
		}
	}, 10);
}
animateIt();

위의 예시를 실행하면 1초 뒤에 assert의 결과가 모두 통과하게 된다.

여기서 확인할 수 있는 것은 setInterval 내의 익명함수에서 유효범위에 속한 tick 과 timer변수를 클로저를 통해 참조할 수 있다는 것이다.

tick과 timer를 굳이 전역변수로 설정해서 사용할 수도 있겠지만 이러면 함수를 여러번 사용할수록 새로운 전역변수를 생성해야하므로 변수 관리의 어려움을 겪게된다. 이러한 문제를 해결하기 위해 타이머 함수에서 클로저를 활용하며, 콜백 함수도 같은원리로 적용할 수 있다.


클로저를 활용한 추가적인 활용법은 다음 글에서 알아보자.