메모리 문제 해결

Kayce Basques
Kayce Basques

Chrome 및 DevTools를 사용하여 메모리 누수, 메모리 팽창, 빈번한 가비지 컬렉션 등 페이지 성능에 영향을 미치는 메모리 문제를 찾는 방법을 알아봅니다.

요약

  • Chrome 작업 관리자를 사용하여 페이지에서 사용하는 메모리 양을 확인합니다.
  • 타임라인 기록을 사용하여 시간 경과에 따른 메모리 사용량을 시각화합니다.
  • 힙 스냅샷을 사용하여 분리된 DOM 트리(메모리 누수의 일반적인 원인)를 식별합니다.
  • 할당 타임라인 기록을 사용하여 JS 힙에 새 메모리가 할당되는 시점을 확인합니다.
  • JavaScript 참조에 의해 유지되는 분리된 요소를 식별합니다.

개요

RAIL 성능 모델의 정신에 따라 성능 노력의 초점은 사용자가 되어야 합니다.

메모리 문제는 사용자가 인식할 수 있는 경우가 많기 때문에 중요합니다. 사용자는 다음과 같은 방식으로 메모리 문제를 인식할 수 있습니다.

  • 시간이 지남에 따라 페이지 성능이 점차 저하됩니다. 이는 메모리 누수의 증상일 수 있습니다. 메모리 누수는 페이지의 버그로 인해 페이지가 시간이 지남에 따라 점점 더 많은 메모리를 사용하는 경우입니다.
  • 페이지의 실적이 지속적으로 저조합니다. 이는 메모리 비대화의 증상일 수 있습니다. 메모리 비대화는 페이지가 최적의 페이지 속도에 필요한 것보다 더 많은 메모리를 사용하는 경우입니다.
  • 페이지 성능이 지연되거나 자주 일시중지되는 것처럼 보입니다. 이는 잦은 가비지 컬렉션의 증상일 수 있습니다. 가비지 컬렉션은 브라우저가 메모리를 회수하는 것입니다. 브라우저가 이 시점을 결정합니다. 수집 중에는 모든 스크립트 실행이 일시중지됩니다. 따라서 브라우저가 가비지 컬렉션을 많이 수행하면 스크립트 실행이 많이 일시중지됩니다.

메모리 비대화: '과도한' 정도는 어느 정도인가요?

메모리 누수는 정의하기 쉽습니다. 사이트에서 메모리를 점점 더 많이 사용한다면 누수가 있는 것입니다. 하지만 메모리 비대화는 정확히 파악하기가 조금 더 어렵습니다. '메모리를 너무 많이 사용'하는 것으로 간주되는 기준은 무엇인가요?

기기와 브라우저마다 기능이 다르기 때문에 정확한 수치는 없습니다. 고급형 스마트폰에서 원활하게 실행되는 페이지가 저사양 스마트폰에서는 비정상 종료될 수 있습니다.

여기서 핵심은 RAIL 모델을 사용하고 사용자에 집중하는 것입니다. 사용자에게 인기 있는 기기를 파악한 다음 해당 기기에서 페이지를 테스트합니다. 사용자 환경이 지속적으로 좋지 않은 경우 페이지가 해당 기기의 메모리 기능을 초과하는 것일 수 있습니다.

Chrome 작업 관리자로 실시간 메모리 사용량 모니터링

Chrome 작업 관리자를 메모리 문제 조사의 시작점으로 사용합니다. 작업 관리자는 페이지에서 사용 중인 메모리 양을 알려주는 실시간 모니터입니다.

  1. Shift+Esc를 누르거나 Chrome 기본 메뉴로 이동하여 도구 더보기 > 작업 관리자를 선택하여 작업 관리자를 엽니다.

    작업 관리자를 엽니다.

  2. 작업 관리자의 표 헤더를 마우스 오른쪽 버튼으로 클릭하고 JavaScript 메모리를 사용 설정합니다.

    작업 관리자 헤더에서 JS 메모리를 사용 설정합니다.

이 두 열은 페이지에서 메모리를 사용하는 방식에 대해 서로 다른 정보를 제공합니다.

  • 메모리 사용량 열은 OS 메모리를 나타냅니다. DOM 노드는 OS 메모리에 저장됩니다. 이 값이 증가하면 DOM 노드가 생성됩니다.
  • JavaScript 메모리 열은 JS 힙을 나타냅니다. 이 열에는 두 개의 값이 포함됩니다. 관심 있는 값은 라이브 숫자(괄호 안의 숫자)입니다. 라이브 숫자는 페이지의 연결 가능한 객체가 사용 중인 메모리 양을 나타냅니다. 이 숫자가 증가하면 새 객체가 생성되거나 기존 객체가 증가하는 것입니다.

    JavaScript 메모리 헤더가 사용 설정된 작업 관리자

성능 기록으로 메모리 누수 시각화

성능 패널을 조사의 또 다른 시작점으로 사용할 수도 있습니다. Performance 패널을 사용하면 시간 경과에 따른 페이지의 메모리 사용량을 시각화할 수 있습니다.

  1. DevTools에서 성능 패널을 엽니다.
  2. 메모리 체크박스를 사용 설정합니다.
  3. 녹화하기

성능 메모리 기록을 보여주기 위해 다음 코드를 살펴보겠습니다.

var x = [];

function grow() {
  for (var i = 0; i < 10000; i++) {
    document.body.appendChild(document.createElement('div'));
  }
  x.push(new Array(1000000).join('x'));
}

document.getElementById('grow').addEventListener('click', grow);

코드에서 참조된 버튼을 누를 때마다 10,000개의 div 노드가 문서 본문에 추가되고 100만 개의 x 문자열이 x 배열에 푸시됩니다. 이 코드를 실행하면 다음 스크린샷과 같은 타임라인 기록이 생성됩니다.

간단한 성장 예시

먼저 사용자 인터페이스에 대한 설명입니다. Overview 창의 HEAP 그래프(NET 아래)는 JS 힙을 나타냅니다. 개요 창 아래에는 카운터 창이 있습니다. 여기에서 JS 힙(개요 창의 HEAP 그래프와 동일), 문서, DOM 노드, 리스너, GPU 메모리별로 분류된 메모리 사용량을 확인할 수 있습니다. 체크박스를 사용 중지하면 그래프에서 숨겨집니다.

이제 스크린샷과 비교하여 코드를 분석합니다. 노드 카운터(녹색 그래프)를 보면 코드와 깔끔하게 일치하는 것을 확인할 수 있습니다. 노드 수는 개별 단계에서 증가합니다. 노드 수가 증가할 때마다 grow()에 대한 호출이라고 가정할 수 있습니다. JS 힙 그래프(파란색 그래프)는 간단하지 않습니다. 권장사항에 따라 첫 번째 하락은 실제로 강제 가비지 컬렉션입니다(collect garbage 버튼을 눌러 실행). 기록이 진행됨에 따라 JS 힙 크기가 급증하는 것을 볼 수 있습니다. 이는 자연스럽고 예상되는 현상입니다. JavaScript 코드는 버튼을 클릭할 때마다 DOM 노드를 만들고 100만 자의 문자열을 만들 때 많은 작업을 수행합니다. 여기서 중요한 점은 JS 힙이 시작할 때보다 높게 끝난다는 것입니다(여기서 '시작'은 강제 가비지 컬렉션 이후의 지점). 실제 환경에서 JS 힙 크기 또는 노드 크기가 증가하는 패턴이 보이면 메모리 누수가 발생할 수 있습니다.

힙 스냅샷으로 분리된 DOM 트리 메모리 누수 발견하기

DOM 노드는 페이지의 DOM 트리 또는 JavaScript 코드에서 참조가 없는 경우에만 가비지 컬렉션될 수 있습니다. 노드가 DOM 트리에서 삭제되었지만 일부 JavaScript에서 여전히 참조하는 경우 노드가 '분리'되었다고 합니다. 분리된 DOM 노드는 메모리 누수의 일반적인 원인입니다. 이 섹션에서는 DevTools의 힙 프로파일러를 사용하여 분리된 노드를 식별하는 방법을 설명합니다.

다음은 분리된 DOM 노드의 간단한 예입니다.

var detachedTree;

function create() {
  var ul = document.createElement('ul');
  for (var i = 0; i < 10; i++) {
    var li = document.createElement('li');
    ul.appendChild(li);
  }
  detachedTree = ul;
}

document.getElementById('create').addEventListener('click', create);

코드에서 참조된 버튼을 클릭하면 10개의 li 하위 요소가 있는 ul 노드가 생성됩니다. 이러한 노드는 코드에서 참조되지만 DOM 트리에 존재하지 않으므로 분리됩니다.

힙 스냅샷은 분리된 노드를 식별하는 한 가지 방법입니다. 이름에서 알 수 있듯이 힙 스냅샷은 스냅샷 시점에 페이지의 JS 객체와 DOM 노드 간에 메모리가 어떻게 분산되는지 보여줍니다.

스냅샷을 만들려면 DevTools를 열고 Memory 패널로 이동하여 힙 스냅샷 라디오 버튼을 선택한 다음 Take snapshot 버튼을 누릅니다.

힙 스냅샷 가져오기 라디오 버튼이 선택되어 있습니다.

스냅샷을 처리하고 로드하는 데 시간이 걸릴 수 있습니다. 완료되면 왼쪽 패널(힙 스냅샷)에서 선택합니다.

클래스 필터 입력란에 Detached를 입력하여 분리된 DOM 트리를 검색합니다.

분리된 노드를 필터링합니다.

캐럿을 펼쳐 분리된 트리를 조사합니다.

분리된 트리 조사

노드를 클릭하여 자세히 조사합니다. Objects 창에서 이를 참조하는 코드에 관한 자세한 정보를 확인할 수 있습니다. 예를 들어 다음 스크린샷에서 detachedTree 변수가 노드를 참조하는 것을 확인할 수 있습니다. 이 특정 메모리 누수를 해결하려면 detachedTree를 사용하는 코드를 검토하고 더 이상 필요하지 않은 노드에 대한 참조를 삭제해야 합니다.

분리된 노드 조사

할당 타임라인으로 JS 힙 메모리 누수 식별

할당 타임라인은 JS 힙의 메모리 누수를 추적하는 데 도움이 되는 또 다른 도구입니다.

Allocation Timeline을 보여주기 위해 다음 코드를 살펴보겠습니다.

var x = [];

function grow() {
  x.push(new Array(1000000).join('x'));
}

document.getElementById('grow').addEventListener('click', grow);

코드에서 참조된 버튼을 누를 때마다 100만 자의 문자열이 x 배열에 추가됩니다.

할당 타임라인을 기록하려면 DevTools를 열고 메모리 패널로 이동하여 타임라인의 할당 라디오 버튼을 선택하고 기록 버튼을 누른 다음 메모리 누수를 일으키는 것으로 의심되는 작업을 실행하고 완료되면 기록 중지 버튼을 누릅니다.

기록하는 동안 다음 스크린샷과 같이 Allocation Timeline에 파란색 막대가 표시되는지 확인합니다.

성능 타임라인의 새로운 할당

파란색 막대는 새 메모리 할당을 나타냅니다. 이러한 새 메모리 할당은 메모리 누수 후보입니다. 막대를 확대하여 지정된 기간에 할당된 객체만 표시하도록 Constructor 창을 필터링할 수 있습니다.

확대된 할당 타임라인

객체를 펼치고 값을 클릭하여 객체 창에서 객체에 관한 세부정보를 확인합니다. 예를 들어 아래 스크린샷에서 새로 할당된 객체의 세부정보를 보면 Window 범위의 x 변수에 할당된 것을 확인할 수 있습니다.

문자열 배열의 객체 세부정보입니다.

함수별 메모리 할당 조사

메모리 패널의 할당 샘플링 프로필 유형을 사용하여 JavaScript 함수별 메모리 할당을 확인합니다.

메모리 패널의 할당 샘플링 프로파일러

  1. 할당 샘플링 라디오 버튼을 선택합니다. 페이지에 작업자가 있으면 JavaScript VM 인스턴스 선택 창에서 이를 프로파일링 대상으로 선택할 수 있습니다.
  2. 시작 버튼을 누릅니다.
  3. 조사하려는 페이지에서 작업을 실행합니다.
  4. 모든 작업을 완료하면 중지 버튼을 누릅니다.

DevTools에 함수별 메모리 할당 분석이 표시됩니다. 기본 뷰는 Heavy (Bottom Up)이며, 메모리를 가장 많이 할당한 함수가 상단에 표시됩니다.

할당 프로필 결과 페이지

JS 참조에 의해 유지되는 객체 식별

분리된 요소 프로필에는 JavaScript 코드에서 참조되어 유지되는 분리된 요소가 표시됩니다.

분리된 요소 프로필을 기록하여 정확한 HTML 노드와 노드 수를 확인합니다.

분리된 요소 프로필 예

잦은 가비지 컬렉션 발견

페이지가 자주 일시중지되는 것처럼 보이면 가비지 컬렉션 문제가 있을 수 있습니다.

Chrome 작업 관리자 또는 타임라인 메모리 기록을 사용하여 빈번한 가비지 컬렉션을 발견할 수 있습니다. 작업 관리자에서 메모리 또는 자바스크립트 메모리 값이 자주 오르내리면 가비지 컬렉션이 자주 발생한다는 의미입니다. 타임라인 기록에서 JS 힙 또는 노드 수 그래프가 자주 오르내리면 가비지 컬렉션이 자주 발생한다는 의미입니다.

문제를 식별한 후에는 할당 타임라인 기록을 사용하여 메모리가 할당되는 위치와 할당을 유발하는 함수를 찾을 수 있습니다.