[250324] TIL

오늘 한 일

서론

전통적인 다중 페이지 애플리케이션에서는 페이지 전환 시 브라우저가 전체 페이지를 언로드하고 새로운 페이지를 로드하면서 자연스럽게 메모리가 정리되지만, SPA에서는 페이지 전환이 JavaScript를 통해 DOM 조작으로 이루어지므로 메모리 정리가 명시적으로 수행되어야 함

JavaScript의 메모리 관리 모델

JavaScript는 가비지 컬렉션(Garbage Collection, GC)을 통해 메모리를 자동으로 관리

가비지 컬렉터는 더 이상 접근할 수 없는(unreachable) 객체를 식별하고 해당 메모리를 회수

  • 표시-소거(Mark-and-Sweep): 루트(전역 객체, 현재 실행 중인 함수의 지역 변수 등)에서 시작하여 도달 가능한 모든 객체를 표시하고, 표시되지 않은 객체를 메모리에서 제거합니다.
  • 참조 카운팅(Reference Counting): 각 객체에 대한 참조 수를 추적하고, 참조 수가 0이 되면 해당 객체를 메모리에서 제거합니다.

그러나 이러한 자동 메모리 관리에도 불구하고, 특정 패턴은 가비지 컬렉터가 메모리를 회수하지 못하게 하여 메모리 누수를 발생시킵니다.

발생 원인

순환 참조

<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 는 전역 변수이므로 페이지가 존재하는 한 계속 유지
  • obj 가 DOM 요소 참조
  • DOM 요소의 expandoProperty가 다시 obj 참조
  • 함수가 종료되어도 참조 계속 유지된다.
  • 단일 페이지 애플리케이션에서는 (SPA) 에서는 이런 누수가 누적되면 심각하다.

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

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

a 의 객체는 내부함수 childFunction을 통해 다시 참조가 되기 때문에 가비지 컬렉팅이 되지 않음

클로저 작동

<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>

객체 a는 가비지 컬렉팅이 되지 않음

불필요한 클로저

// 잘못된 방식 (메모리 누수 가능성)
function createHeavyObject() {
  const heavyData = new Array(10000).fill('🐘');
  
  return function() {
    console.log(heavyData.length); // heavyData를 계속 참조
  };
}
  • 그 외, 이벤트 리스너, 전역 변수 남용, 타이머 남용 등이 있다.

해결 방법

순환 참조 제거

window.onload = function(){
    var obj = document.getElementById("DivElement");
    obj.expandoProperty = obj; // 순환 참조 생성
    obj.bigString = new Array(1000).join(new Array(2000).join("XXXXX"));
    
    // 페이지 언로드 시 참조 정리
    window.addEventListener('beforeunload', function() {
        obj.expandoProperty = null;
        obj = null;
    });
};

이벤트 리스너 정리

// React 예시
useEffect(() => {
  window.addEventListener('resize', handleResize);
  
  // 컴포넌트 언마운트 시 정리
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

불필요한 클로저 제거

// 개선된 방식
function createLightFunction() {
  const size = new Array(10000).fill('🐘').length;
  
  return function() {
    console.log(size); // 필요한 값만 참조
  };
}

전역 변수 최소화

타이머 함수 해제

useEffect(() => {
  const timer = setInterval(() => {
    console.log('polling data...');
  }, 1000);
  
  return () => clearInterval(timer);
}, []);

Categories:

Updated:

Leave a comment