메모리 누수 위험

>> 예시 출력란



자바스크립트는 가비지 컬렉션이라고 알려진 기술을 이용해 자신의 메모리를 관리한다. 가비지 컬렉션은 직접 메모리 블록을 관리하고 사용하던 메모리가 더는 필요 없을 때 프로그래머가 그것을 직접 해제하던 낮은 수준의 C와 같은 언어와 상당히 대조된다. Objective-C 같은 언어는 참조 카운트 시스템을 구현하고 있어서 개발자에 도움을 주고 있다. 참조 카운트는 사용자가 특정 메모리가 현재 얼마나 많은 부분에서 참조하고 있는지 알 수 있게 함으로써 사용자가 더 이상 필요없는 메모리를 제거할 수 있게 해준다. 이와 대조적으로 자바스크립트는 고수준의 언어이며 내부적으로 스스로 메모리를 관리한다.

자바스크립트에서는 객체든 아니면 새로 등장하는 함수든 메모리에 저장되는 아이템이 나타나면 그 아이템을 위한 메모리 공간이 마련된다. 객체는 함수로 전달되기도하고 변수에 할당되기도 하므로 객체를 가리키는 코드들은 점점 더 많아지게 된다. 자바스크립트는 이런 포인터를 추적하면서 마지막 참조가 사라지게 되면 객체를 메모리에서 해제한다. 다음 도표와 같은 포인터의 체인을 생각해 보자.

여기서 A 객체는 B를 가리키는 속성이 있고, B는 C를 가리키는 포인터가 있다. 여기서 A 객체가 현재 범위에서 이용되는 단 하나의 변수라고 해도 나머지 두 개도 참조되고 있으므로 세 개의 모든 객체는 여전히 메모리에 남아 있어야만 한다. 하지만 (A가 선언되어 있던 함수가 끝나는 경우와 같이) A 객체가 쓸모가 없어지면 가비지 컬렉터에 의해 제거된다. 그러면 이제 B 또한 그것을 가리키는 것이 없으므로 메모리에서 해제되고 마지막으로 C 또한 해제된다.

더 복잡하게 나열된 참조는 훨씬 더 다루기 어렵다.

이제 C 객체에 역으로 B를 참조할 수 있게 속성을 추가했다. 이때는 A가 메모리에서 해제될 때도 B는 여전히 C로부터 참조되고 있게 된다. 이런 순환 참조(reference loop)는 자바스크립트 엔진에 의해 별도로 처리해야 하는데, 엔진에서 참조 루프가 변수 범위에 존재하는 변수들부터 고립됐다는 사실을 감지할 수 있어야 한다.




우연한 순환 참조들

클로저는 의도치 않게 순환 참조를 만들 수도 있다. 함수는 메모리에 존재해야 하는 객체지만 그 함수의 닫힌 환경에 있는 변수도 마찬가지로 메모리에 존재해야 한다.

							function outerFn() {
								var outerVar = {};
								function innerFn () {
									$.print(outerVar);
								}

								outerVar.fn = innerFn;
								return innerFn;
							}
						

여기서는 outerVar 객체를 생성해 내부 함수 innerFn()에서 참조하고 있다. 그 다음 outerVar에 innerFn()을 가리키는 속성을 만들고 innerFn()을 반환한다. 이는 innerFn() 함수에 대한 클로저를 생성하고 그 클로저는 outerVar을 참조한다. 그리고 다시 outerVar은 innerFn()을 참조하고 있다.

하지만 실제 잘못된 반복의 형태는 이보다 더 찾기 어려울 정도로 교활하게 숨겨질 수도 있다.

							function outerFn() {
								var outerVar = {};
								function innerFn () {
									$.print('hello');
								}

								outerVar.fn = innerFn;
								return innerFn;
							}
						

이 코드에서는 더는 outerVar를 가리키지 않게 innerFn()을 변경했다. 하지만 이런 방식으로는 순환고리를 끊지 못한다. 심지어 outerVar가 innerFn()으로부터 전혀 참조되지 않는다고 해도 innerFn()의 닫힌 환경에 여전히 남아있을 것이다. outerFn()함수의 범위에 있는 모든 변수는 클로저의 특성상 innerFn()에 의해 묵시적으로 참조된다. 그러므로 클로저는 이런 의도치 않은 순환고리가 만들어지기 쉽다.



인터넷 익스플로러 메모리 누수 문제

보통 이 모든 것이 문제가 되지는 않는다. 자바스크립트는 이런 순환고리를 찾아내고 고립이 되면 순환고리를 지울 수 있다. 하지만 몇몇 인터넷 익스플로러 버전에서는 특별한 형태의 순환 참조를 제대로 처리하지 못한다. 반복문이 DOM 요소와 일반적인 자바스크립트 객체 모두를 포함할 때 인터넷 익스플로러는 아무것 하나도 메모리에서 해제할 수 없다. 왜냐하면 서로 다른 메모리 관리자를 사용하기 때문이다. 이 루프는 브라우저가 닫히기 전까지는 결고 메모리에서 해제될 수 없으므로 시간이 지날수록 더 많은 메모리를 낭비하게 된다. 이런 루프의 일반적인 원인은 이벤트 핸들러다.

							$(document).ready(function(){
								var button = document.getElementById('button-1');
								button.onclick = function () {
									$.print('hello');
									return false;
								};
							});
						

click 핸들러를 할당할 때 button을 포함한 클로저 하나를 생성하게 된다. 하지만 여기서 button은 onclick 프로퍼티로 클로저를 역으로 참조하고 있다. 여기서 만들어진 루프는 인터넷 익스플로러에 의해 제거될 수 없고 다른 페이지로 이동하더라도 메모리에 남아있게 된다.

메모리를 해제하려면 위도우 창이 닫히기 전에 onclick 프로퍼티를 제거해 이 루프를 끊어야 한다. 이때 window와 onunload 핸들러 사이에 새로운 루프를 만들지 않게 주의한다. 또 다른 방법으로 클로저가 만들어지는 것을 피하려면 다음과 같이 코드를 다시 작성할 수도 있다.

							function hello () {
								$.print('hello');
								return false;
							}

							$(document).ready(function(){
								var button = document.getElementById('button-1');
								button.onclick = hello;
							});
						

hello() 함수는 더는 button과 같은 영역에서 선언되지 않으므로 참조는 한 방향(button에서 hello로)으로만 되고, 루프도 없고 메모리 누수도 없다.




좋은 소식

이번에는 같은 코드이지만 jQuery를 사용해 구성해 보자.

							$(document).ready(function(){
								var $button = $("#button-1");
								$button.click(function () {
									$.print('hello');
									return false;
								})
							});
						

비록 이전과 같이 루프의 원이 될 수 있는 클로저가 생성됐더라도 이 코드가 수행되는 인터넷 익스플로러에는 메모리 누수가 발생하지 않는다. 다행히 jQuery는 잠재적인 누수에 대한 대비책이 있다. jQuery는 할당했던 모든 이벤트 핸들러를 일일이 해제한다. 그러므로 jQuery를 사용해 이벤트 바인딩 메서드를 추가한다면 이런 특수한 상황에서도 메모리 누수를 별로 걱정할 필요가 없다.

그렇다고 문제가 완전히 해결된 건 아니다. DOM 요소로 작업할 때는 항상 주의를 기울여야 한다. 인터넷 인스플로러에서 자바스크립트 객체를 DOM요소에 연결하면 여전히 메모리 누수가 생긴다. jQuery는 단지 메모리 누수가 좀 더 드물게 발생하게 도울 뿐이다.