헤르메스 LIFE

JavaScript 및 Dojo로 인한 브라우저 메모리 누출 발견 및 해결하기 본문

문서

JavaScript 및 Dojo로 인한 브라우저 메모리 누출 발견 및 해결하기

헤르메스의날개 2012. 1. 4. 14:04
728x90
원문 : http://www.ibm.com/developerworks/kr/library/wa-sieve/index.html

 

JavaScript 및 Dojo로 인한 브라우저 메모리 누출 발견 및 해결하기

sIEve로 검사 및 선별

Yi Ming Huang, 소프트웨어 엔지니어, IBM

요약:  독자가 JavaScript와 Ajax 기술을 많이 사용하는 웹 2.0 애플리케이션을 개발하고 있다면, 브라우저 메모리 누출에 봉착할 가능성이 높습니다. 한 페이지 애플리케이션이 있거나 페이지에서 UI 연산을 많이 처리하는 경우 문제가 심각할 수 있습니다. 이 기사에서는 sIEve 도구로 메모리 누출을 발견하여 정정하는 방법을 학습합니다. 메모리 누출 문제의 실질적인 예제와 솔루션도 포함됩니다.

이 기사에 태그:  웹_서비스

기사 게재일:  2011 년 12 월 14 일
난이도: 중급 원문:  보기 PDF:  A4 and Letter (240KB | 15 pages)Get Adobe® Reader®
페이지뷰:  182 회
의견:   0 (보기 | 의견 추가 - 로그인)


소개

일반적으로 브라우저 메모리 누출은 웹 애플리케이션의 문제가 아니다. 사용자들은 페이지 사이에 탐색하고, 각 페이지를 전환하면서 브라우저가 새로 고쳐지게 된다. 한 페이지에 메모리 누출이 있을지라도 이러한 누출은 페이지 전환 이후에 해제된다. 누출의 규모가 작으므로 결과적으로 보통 무시된다.

메모리 누출은 Ajax 기술이 도입되었을 때 한 가지 문제 이상으로 크게 되었다. 웹 2.0 스타일 페이지에서 사용자는 페이지를 자주 새로 고치지 않는다. Ajax 기술은 페이지 컨텐츠를 비동기적으로 업데이트하기 위해 사용된다. 극단적인 시나리오에서는 전체 웹 애플리케이션이 하나의 페이지에서 구성된다. 이 경우에 누출이 축적되어 무시될 수 없다.

이 기사에서는 메모리 누출이 어떻게 발생하는지 그리고 sIEve로 누출의 소스를 찾는 방법을 학습한다. 문제점과 솔루션의 실제적인 예제로 이러한 문제를 살펴보는 데 도움이 된다. 이 기사에서 예제에 대한 소스 코드를 다운로드할 수 있다.

JavaScript 및 Dojo Toolkit의 경험이 있다면 유용하지만 이 기사를 이해하기 위해 이러한 경험이 필수는 아니다.

누출 패턴

웹 개발자가 인식하는 것처럼 Internet Explorer(IE)는 Firefox 및 다른 브라우저와는 다르다. 이 기사에서 논의된 메모리 누출 패턴과 문제는 주로 IE를 대상으로 하지만 이에 국한되지는 않는다. 훌륭한 사례는 모든 브라우저에 적용 가능해야 한다.

IE가 메모리를 관리하는 방법의 논의는 이 기사의 범위를 벗어나지만, 참고자료에 더 많은 정보가 나와 있다.

JavaScript의 특성과 JavaScript 및 DOM 오브젝트의 브라우저 메모리 관리로 인해 부주의하게 코딩된 JavaScript가 브라우저 메모리 누출을 야기한다. 두 가지 잘 알려진 패턴으로 인해 이러한 누출이 나타난다.

순환 참조
순환 참조는 거의 모든 누출의 근본 원인이다. 일반적으로 IE는 순환 참조를 처리하고 JavaScript 영역에서 올바르게 처리할 수 있다. 예외는 DOM 오브젝트가 도입될 때 발생한다. JavaScript 오브젝트가 DOM 요소로 참조를 보유하고 DOM 요소의 특성이 JavaScript 오브젝트를 보유할 때 순환 참조가 일어나 DOM 노드에서 누출이 발생하게 된다. 목록 1은 메모리 누출에 대한 기사에서 이 문제를 시연하는 데 자주 사용되는 코드 샘플을 보여준다.


목록 1. 순환 참조로 누출

      
var obj = document.getElementById("someLeakingDIV");
document.getElementById("someLeakingDiv").expandoProperty = obj;

문제를 해결하기 위해 문서에서 노드를 제거할 준비가 되면 expandoProperty를 명시적으로 널로 설정한다.

클로저
클로저는 인식하지 못하고 순환 참조를 작성하기 때문에 메모리 누출을 야기한다. 상위 함수의 변수는 클로저가 살아있는 한 보유될 것이다. 해당 라이프사이클은 함수 범위를 넘어가며, 이는 신중하게 처리되지 않으면 누출을 초래할 것이다. 목록 2는 클로저로 야기된 누출을 보여주며, 이는 JavaScript에서 일반적인 코딩 스타일이다.


목록 2. 누출된 클로저

         <html>
<head>
<script type="text/javascript">
window.onload = function() {
    var obj = document.getElementById("element");
    // this creates a closure over "element"
    // and will leak if not handled properly.
    obj.onclick = function(evt) {
        alert("leak the element DIV");
    };
};
</script>
</head>
<body>
<div id="element">Leaking DIV</div>
</body>
</html>

sIEve를 사용하면—orphan 노드와 메모리 누출을 발견하는 도구—요소 DIV가 두 번 참조되었다는 것을 인식할 것이다. 참조 중 하나는 클로저가 보유하고(onclick 이벤트에 지정된 익명 함수) 노드를 제거할지라도 발견될 수 없다. 애플리케이션이 element 노드를 나중에 제거하는 경우, JavaScript 참조는 여전히 orphan 노드를 보유할 것이다. 이러한 orphan 노드가 메모리 누출을 야기할 것이다.

클로저가 순환 참조를 작성하는 이유를 이해하는 것은 중요하다. "Memory Leakage in Internet Explorer - revisited" 기사의 다이어그램은 해당 문제를 분명히 시연하며, 이는 그림 1에 표시된다.

문제를 수정하는 한 가지 방법은 클로저를 제거하는 것이다.


그림 1. DOM과 JavaScript 사이에 순환 참조를 작성하는 클로저

sIEve 소개하기

sIEve는 메모리 누출을 발견하는 데 유용한 도구이다. 참고자료에서 slEve를 다운로드하고 문서에 액세스할 수 있다. 기본 sIEve 창이 그림 2에 표시된다.


그림 2. sIEve 기본 창

해당 도구는 Show in use를 클릭하면 특히 유용하다. orphan 노드와 DOM 노드로 늘어나거나 줄어든 참조를 비롯하여 사용 중인 DOM 노드가 모두 표시될 것이다.

그림 3은 샘플 보기를 보여준다. 누출의 원인은 다음과 같다.

  • Orphan 열에서 Yes로 표시된 Orphan 노드.
  • 파란색으로 된 DOM 노드로 잘못 늘어난 참조.

sIEve를 사용하여 누출하는 노드를 찾고 수정하는 코드를 검토한다.


그림 3. sIEve: 사용 중인 DOM 노드

sIEve로 누출하는 노드 찾기

다음 단계를 사용하여 누출하는 노드를 발견한다.

  1. 웹 애플리케이션의 URL로 sIEve를 실행한다.
  2. Scan Now를 클릭하여 현재 문서에서 사용 중인 모든 DOM 노드를 찾는다(선택적).
  3. Show in use를 클릭하여 모든 DOM 노드를 확인한다. 이제 방금 시작했으므로 모든 노드가 빨간색(새 항목)이 될 것이다.
  4. 웹 애플리케이션에서 일부 조치를 취하여 누출이 있는지 여부를 테스트한다.
  5. Scan Now를 클릭하여 사용 중인 DOM 노드를 새로 고친다(선택적).
  6. Show in use를 클릭한다. 이제 해당 보기에 일부 흥미로운 정보가 포함된다. Orphan 노드를 찾을 수 있거나 특정 DOM 노드로 예상하지 못한 참조가 늘어날 수 있다.
  7. 보고서를 분석하고 코드를 검토한다.
  8. 필요하면 4-8단계를 반복한다.

sIEve는 애플리케이션의 누출을 모두 찾을 수는 없지만, orphan 노드로 야기된 누출을 찾을 것이다. ID와 outerHTML과 같은 추가 정보는 누출하는 노드를 식별하는 데 도움을 줄 수 있다. 누출하는 노드를 조작하는 코드를 검토하고 이에 따라 변경한다.

실질적인 예제

이 섹션에 메모리 누출을 초래할 수 있는 조건의 예제가 더 들어 있다. 샘플 및 우수 사례는 Dojo 툴킷을 기반으로 하지만, 대부분의 예제는 일반 JavaScript 프로그래밍에서 유효하다.

정리할 때, 일반적인 사례는 DOM을 삭제하고 JavaScript 오브젝트를 삭제하여 메모리 누출을 방지하는 것이다. 하지만, 다른 내용이 더 있다. 이 섹션의 나머지 부분에서는 이전에 소개한 패턴에 대해 빌드한다.

다음 예제는 작성할 수 있는 사이트를 포함한다. 페이지에서부터 웹 위젯도 삭제할 것이다. 해당 조치는 페이지 새로 고침 없이 단일 페이지에서 수행된다. 목록 3은 이 기사의 나머지 부분에서 점진적으로 향상될 Dojo 클래스에서 정의한 위젯(디짓이 아님)을 보여준다.


목록 3. MyWidget 클래스
            
           
dojo.declare("leak.sample.MyWidget", null, {
	constructor: function(container) {
		this.container = container;
		this.ID = dojox.uuid.generateRandomUuid();
		this.domNode = dojo.create("DIV", {id: this.ID, 
			innerHTML: "MyWidget "+this.ID}, this.container);
	},
	destroy: function() {
		this.container.removeChild(dojo.byId(this.ID));
	}
});

목록 4는 위젯을 조작하는 기본 페이지를 보여준다.


목록 4. 사이트 HTML
            
<html>
<head>
<title>Dojo Memory Leak Sample</title>
<script type="text/javascript" src="js/dojo/dojo/dojo.js"></script>
<script type="text/javascript">
dojo.registerModulePath("leak.sample", "../../leak/sample");
dojo.require("leak.sample.MyWidget");

widgetArray = [];

function createWidget() {
	var container = dojo.byId("widgetContainer");
	var widget = new leak.sample.MyWidget(container);
	widgetArray.push(widget);
}
function removeWidget() {
	var widget = widgetArray.pop();
	widget.destroy();
}
</script>
</head>
<body>
	<button onclick="createWidget()">Create Widget</button>
	<button onclick="removeWidget()">Remove Widget</button>
	<div id="widgetContainer"></div>
</body>
</html>

dojo.destroy() 또는 dojo.empty() 사용하기

처음에 보면 이 문제는 문제가 될 것처럼 보이지는 않는다. 위젯이 작성되어 배열에 저장된다. 이는 배열에서 팝업되어 제거된다. DOM 노드도 문서에서부터 분리된다. 하지만 sIEve를 사용하여 create widgetremove widget 조치 사이에 차이점을 추적하는 경우 위젯 노드가 orphan이 될 때마다 메모리 누출을 수반할 수 있음을 알게 될 것이다. 그림 4는 위젯 작성 및 제거의 예제를 두 번 보여준다.


그림 4. 위젯 노드에 대한 누출

이 상황은 IE 버그일 수도 있다. 요소를 작성하고 이를 문서에 연결한 다음 parentNode.removeChild()로 즉시 제거할 지라도, orphan 노드는 여전히 존재할 것이다.

DOM 노드를 지우기 위해 dojo.destroy() 또는 dojo.empty()를 사용할 수 있다. Dojo는 삭제된 노드를 다른 어딘가로 이동한 다음 영구 삭제하기 위해 dojo.destroy(<domNode>)를 구현했다. 이는 가비지 콜렉션의 해당하는 종류에 대한 노드를 작성할 것이다. 삭제하려는 노드는 제거되었다. (구현 세부사항은 Dojo 소스 코드를 참조한다.) 목록 5는 문제를 수정하는 방법을 보여준다.


목록 5. DOM 노드를 제거하기 위해 dojo.destroy() 사용하기
           
                   
## change the destroy() method of MyWidget.js
destroy: function() {
	dojo.destroy(dojo.byId(this.ID));
}

확인을 위해 sIEve를 사용할 때, 최초로 위젯을 제거하면 Dojo가 빈 DIV(가비지)를 작성한다는 것을 알게 될 것이다. 이후의 추가와 제거에서 어느 DOM 노드도 orphan이 되지 않을 것이므로 누출이 발생하지 않을 것이다.

DOM 노드로 JavaScript 참조 무효화(nullify)하기

이는 정리를 수행할 때 DOM 노드로 JavaScript 참조를 무효화하는 훌륭한 사례이다. 목록 3에서 destroy 메소드는 DOM 노드로 JavaScript 참조를 무효화하지 않는다(this.domNode, this.container). 대부분의 경우에 이 상황은 메모리 누출을 초래하지 않지만 다른 오브젝트가 위젯으로 참조를 보유하는 더 복잡한 애플리케이션에서 작업하고 있을 때 문제점이 발생할 수 있다.

알지 못하는 다른 저장소가 사용 가능하고 위젯으로 참조를 보유하고, 어떠한 이유로 이는 정리될 수 없다고 가정한다. 위젯을 제거하면 여기로 참조된 DOM 노드가 orphan 상태가 될 것이다. 목록 6은 해당 변경을 보여준다.


목록 6. 사이트 HTML: 오브젝트(widgetRepo)를 하나 더 추가하여 위젯 보유하기
           
           
widgetArray = [];
widgetRepo = {};

function createWidget() {
	var container = dojo.byId("widgetContainer");
	var widget = new leak.sample.MyWidget(container);
	widgetArray.push(widget);
	widgetRepo[widget.ID] = widget;
}

이제 위젯을 추가하고 제거하려고 시도한 다음에 sIEve를 사용하여 메모리 누출을 발견한다. 그림 5는 위젯 DIV에 대한 orphan 노드와 widgetContainer DIV로의 늘어난 참조를 보여준다. Refs 열에서 widgetContainer DIV는 문서에서 하나의 참조만 있어야 한다.


그림 5. Orphan 노드

솔루션은 정리 도중에 DOM 노드 참조를 무효화하는 것이며, 이는 목록 7에 표시된다. 원래 함수를 해치지 않을 것이므로 가능할 때 이러한 무효화 명령문을 추가하는 것은 훌륭한 사례라고 여겨진다.


목록 7. DOM 참조 무효화하기
   
## the destroy method of MyWidget class
destroy: function() {
	dojo.destroy(dojo.byId(this.ID));
	this.domNode = null;
	this.container = null;
}

이벤트 연결 끊기 및 주제 구독 취소

Dojo를 통해 메모리 누출을 방지하는 또 다른 훌륭한 사례는 연결한 이벤트의 연결을 끊고 구독한 주제의 구독을 취소하는 것이다. 목록 8은 이벤트를 연결하고 연결을 끊는 예제를 보여준다.

JavaScript 프로그래밍을 통해 문서에서 DOM 노드에 대한 이벤트를 제거하기 전에 연결을 끊는 것이 일반적으로 권장된다. 다음 API를 사용하여 다른 브라우저에서 이벤트를 연결하고 연결을 끊는다.

  • IE의 경우: attachEventdetachEvent
  • 다른 브라우저의 경우: addEventListenerremoveEventListener

목록 8. Dojo.connect 및 dojo.disconnect
            
## the constructor method of MyWidget class
constructor: function(container) {
	// … old code here	
	this.clickHandler = dojo.connect(
	this.domNode, "click", this, "onNodeClick");
}

## the destroy method of MyWidget class
destroy: function() {
	// … old code here
	dojo.disconnect(this.clickHandler);
}

주제를 구독하고 공개하여 Dojo에서 컴포넌트들 사이에 연결을 설정할 수도 있다. 이는 Observer 패턴으로 구현된다. 이 경우에 훌륭한 사례는 메모리 누출을 방지하기 위해 정리를 수행할 때 주제를 구독 취소하는 것이다. 두 가지 메소드에 다음 API를 사용한다.

  • dojo.subscribe(/*string*/topic, /*function*/function)
  • dojo.unsubscribe(/*string*/topic)

innerHTML 설정하기

IE 메모리 누출은 JavaScript로 innerHTML을 설정하는 방법에 신중하지 않으면 나타날 수 있다. (세부사항은 참고자료를 참조한다.) 목록 9는 IE 메모리 누출을 초래할 수 있는 시나리오를 보여준다.


목록 9. IE에서 innerHTML 누출
// 1. An orphan node should be in the document
var elem = document.createElement(“DIV”);

// 2. Set the node’s innerHTML with an DOM 0 event wired
elem.innerHTML = “<a onclick=’alert(1)’>leak</a>”;

// 3. Attach the orphan node to the document
document.body.appendChild(elem);

위에 나타난 코드 유형은 웹 2.0 애플리케이션에서 일반적이므로 신중하게 진행한다. 솔루션은 innerHTML을 설정하기 전에 노드가 orphan이 아니게 하는 것이다. 목록 10은 목록 9에서 코드의 수정사항을 보여준다.


목록 10. innerHTML 누출에 대한 수정사항
            
var elem = document.createElement(“DIV”);

// now the node is not orphan anymore
document.body.appendChild(elem);

elem.innerHTML = “<a onclick=’alert(1)’>no leak</a>”;


결론

브라우저 메모리 누출을 야기하는 패턴을 식별하는 것은 상대적으로 간편하다. 애플리케이션 소스 코드에서 문제의 소스를 찾는 것은 더 어려울 수 있다. sIEve는 orphan 노드로 야기된 대부분의 누출을 찾는 데 도움을 줄 수 있다. 이 기사에서는 메모리 누출이 부주의한 아주 작은 JavaScript만으로 어떻게 발생할 수 있는지 설명했다. 이 기사에서 개괄한 우수 사례는 누출 발생을 방지하는 데 도움을 줄 수 있다.


다운로드 하십시오

설명이름크기다운로드 방식
Source code for this article MyWidget.zip 1KB HTTP

다운로드 방식에 대한 정보


참고자료

교육

제품 및 기술

토론

필자소개

Yi Ming Huang은 소프트웨어 엔지니어로 China Development Lab에서 Lotus ActiveInsight를 담당하고 있다. 그는 Portlet/Widget 관련 웹 개발에 참여했으며 REST, OSGi 및 Spring 기술에 관심을 갖고 있다

728x90