IBM - Javascript의 메모리 누수 패턴과 핸들링 방법(Memory leak patterns in JS)

김규빈·2021년 11월 13일
3

본 글은 IBM developer article을 보고 작성한 글 입니다
[참고]

사실 이 아티클을 처음 읽어본지는 꽤 오래전인데, 아티클이 작성한지 오래되기도 했고(2007년) 내용중 그렇다 할 획기적인 핸들링 방법 또한 없어서 읽고 넘겼던 아티클이다. 어쩌다 메모리 누수 아티클을 최근에 다시 보게 됬는데 기본을 놓치고 있는 내 코드를 보면서 다시 한번 정리해본다.

서론

2Paragraph를 보면 js와 dom 요소를 사용을 익숙한 개발자라고 가정하는데, 최근 효율적인 web트랜드 기술은 가상 돔을 이용한 SPA 구조의 웹개발을 하기 때문에 불가피한 상황이 아니라면 dom요소를 직접 제어하는 방법은 지양해야겠다.(2007년도 글)

JS는 가비지컬렉터 언어이다. JS의 클로저 속성을 바라보고 가비지컬렉터에 의해 메모리가 할당된 객체를 회수하는데, 이 회수하는 과정에서 올바른 코드를 작성하지 않으면 사각지대(엄밀히 말하면 의도치 않은 참조)가 발생하여 가비지 컬렉터가 회수하지 못하게 된다. 이러한 틈으로 메모리 누수가 발생하게 된다. 가비지컬텍터의 개념이 부족하다면 coreJS-클로저의 글을 읽고 본다면 좀 더 쉬운 글이 된다

본론

case1.순환 참조를 통한 메모리 누수

<html>
     <body>
     <script type="text/javascript">
     document.write("Circular references between JavaScript and DOM!");
     var obj;
     window.onload = function(){
 obj=document.getElementById("DivElement");
            document.getElementById("DivElement").expandoProperty=obj;
            obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
            };
     </script>
     <div id="DivElement">Div Element</div>
     </body>
     </html>

위의 코드에서 객체 obj에는 DivElement가 나타내는 DOM 객체에 대해 참조를 하고 있다. DOM 개체에는 expandoProperty를 통해 obj객체를 참조하면서, obj 객체와 DOM 객체 사이에는 순환 참조를 하고있다. DOM 객체는 참조 카운팅을 통해 가비지 컬렉터에 수집되기 때문에 어느 객체도 수집되지 않고 메모리 할당으로 이어진다.

case2.외부 함수 호출로 인한 메모리 누수

<html>
<head>
<script type="text/javascript">
document.write("Circular references between JavaScript and DOM!");
function myFunction(element)
{
 this.elementReference = element;
 // This code forms a circular reference here
  //by DOM-->JS-->DOM
 element.expandoProperty = this;
}
function Leak() {
 //This code will leak
 new myFunction(document.getElementById("myDiv"));
}
</script>
</head>
<body onload="Leak()">
<div id="myDiv"></div>
</body>
</html>

외부 함수 myFunction을 호출하여 순환 참조가 생성된다. 다시 서로 참조를 통해 메모리 누수로 이어지고 있다. 이 두 코드 샘플에서 볼 수 있듯이 순환 참조를 하고 있는 구조는 쉽게 발생되기 때문에 이러한 코드 작성은 피해야 된다.

case3.내부 함수를 통한 메모리 누수

function parentFunction(paramA)
{
     var a = paramA;
     function childFunction()
     {
		return a + 2; 
     }
     return childFunction();
}

내부 함수 선언은 특히나 순환 참조 구조가 되기 쉽다. a의 객체는 내부 함수 childFunction를 통해 다시 참조가 되기 때문에 가비지컬렉팅이 되지 않는다.

case4.클로저 작동

<html>
<body>
<script type="text/javascript">
document.write("Closure Demo!!");
window.onload=
function  closureParent(paramA)
{
    var a = paramA;
    return function closureInner(paramB)
    {
       alert( a +" "+ paramB);
    };
};
var x = closureParent("outer x");
x("inner x");
</script>
</body>
</html>

위의 목록에서 closureInner 상위 함수 closureParent 내에 정의된 내부 함수이다. 외부 변수 x를 사용하여 closureParent 호출하면 외부 함수 변수 a에 외부 x 값이 할당 된다. 함수는 x에 포함된 내부 함수 closureInner 대한 alert가 반환될 것이다.
외부 함수가 반환된 후에도 외부 함수 closureParent 지역 변수가 존재한다는 점을 유의 해야된다. JavaScript에서 closureParent 호출되는 순간 a객체가 생성되고, 이 속성에는 "outer x"라고도 하는 paramA 값이 포함된다. 마찬가지로 closureParent 반환되면 변수 x에 포함된 내부 함수 closureInner 반환하게 된다.
내부 함수는 외부 함수의 변수에 대한 참조를 보유하기 때문에 객체 a는 가비지 컬렉팅 되지 않는다.

case5.이벤트 핸들링 메모리 누수

<html>
<body>
<script type="text/javascript">
document.write("Program to illustrate memory leak via closure");
window.onload=function outerFunction(){
 var obj = document.getElementById("element");
 obj.onclick=function innerFunction(){
 alert("Hi! I will leak");
 };
 obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
 // This is used to make the leak significant
};
</script>
<button id="element">Click Me</button>
</body>
</html>

객체 obj에 DOM 객체(id "element"로 참조됨)에 대한 참조가 포함된 클로저이다. DOM 요소에는 obj에 대한 참조를 하게되고, Obj 객체와 DOM 객체 간의 순환 참조로 인해 메모리 누수가 발생된다.

case6. 참조를 끊어 누수 방지 하기

<html>
<body>
<script type="text/javascript">
document.write("Avoiding memory leak via closure by breaking the circular
   reference");
 window.onload=function outerFunction(){
 var obj = document.getElementById("element");
 obj.onclick=function innerFunction()
 {
  alert("Hi! I have avoided the leak");
  // Some logic here
 };
 obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
 obj = null; //This breaks the circular reference
 };
</script>
<button id="element">"Click Here"</button>
</body>
</html>

case.5의 메모리 누수에 대한 핸들링 방법은 obj를 null로 만들어 순환 참조를 명시적으로 중단하는 것이다. null로 더이상 dom객체를 참조하지 않기 때문에 가비지 컬렉터에 의해 수집된다.

case7. 다른 함수를 통해 클로저를 회피하여 누수 방지.

<html>
<body>
<script type="text/javascript">
document.write("Avoiding a memory leak by adding another closure");
 window.onload=function outerFunction(){
var anotherObj = function innerFunction()
   {
   // Some logic here
   alert("Hi! I have avoided the leak");
     };
  (function anotherInnerFunction(){
  var obj =  document.getElementById("element");
  obj.onclick=anotherObj })();
}; </script>
<button id="element">"Click Here"</button>
</body>
</html>

다른 클로저를 추가하여 JavaScript 객체와 DOM 객체 간의 순환 참조를 방지한다.

case8.

<html>
<head>
<script type="text/javascript">
document.write("Avoid leaks by avoiding closures!");
window.onload=function()
{
 var obj = document.getElementById("element");
 obj.onclick = doesNotLeak;
}
function doesNotLeak()
{
 //Your Logic here
 alert("Hi! I have avoided the leak");
}
</script>
</head>
<body>
<button id="element">"Click Here"</button>
</body>
</html>

다른 함수를 추가하여 클로저 자체를 피함으로써 누수를 방지한다.

결론

사실 IBM의 이 아티클은 클로저 속성을 잘 사용하라는 내용이다. 기본적인 내용이지만 기본이 가장 중요하다. 당신의 프로젝트에서 얼마나 메모리 할당 회수를 하고 있는지, 굳이 필요없는 변수 객체에 메모리가 할당 중이진 않는지, state값 선언시 참조값을 끊어 컬렉터 수집이 잘 이루어지고 있는지 생각해 보면 좋을 것 같다.

profile
FrontEnd Developer

1개의 댓글

comment-user-thumbnail
2021년 11월 22일

깨...달았습니다...!

답글 달기