광고 차단 프로그램이 감지되었습니다

이 사이트는 광고 수익을 통해 무료로 콘텐츠와 서비스를 제공하고 있습니다.

더 나은 서비스를 위해 광고 차단 프로그램을 비활성화 해주세요.

광고 차단 해제 방법 보기
Loading...

웹페이지 메모리누수 자원관리

웹페이지 메모리누수 자원관리에 대한 img

문제 해결 보고서:

이 해결과정은 보고 계신 웹사이트의 실제 코드의 문제 해결과정입니다.


커서AI 문제 해결 보고서

1. 문제 상황

  • 발생한 현상: 블로그 웹사이트를 계속 사용하거나 페이지를 이동할수록 웹브라우저가 점점 느려지고 메모리 사용량이 지속적으로 증가함
  • 사용자 의도: 블로그 포스트 페이지에 목차 버튼과 맨 위로/맨 아래로 스크롤 버튼을 추가하여 사용자 경험 개선
  • 오류 메시지: 직접적인 오류 메시지는 없으나 "메모리 누수가 생긴 것 같다"는 사용자 관찰이 있었음
  • 예상 vs 실제: 버튼들이 정상적으로 작동하면서 메모리 사용량이 일정하게 유지되어야 했으나, 실제로는 페이지 사용 시간이 길어질수록 메모리 사용량이 계속 증가함


2. 원인 분석

  • 기술적 원인: JavaScript 코드에서 이벤트 리스너가 중복 등록되고, 타이머 자원이 적절히 정리되지 않으며, DOM 요소가 계속 새로 생성되는 문제가 있었음
  • 쉬운 설명: 웹페이지에서 특정 작업(클릭, 스크롤 등)을 감지하는 "감시자"들이 계속 새로 추가되었는데, 기존 "감시자"는 제거되지 않아 같은 일을 하는 감시자가 점점 늘어남
  • 일상 비유: 집에 도둑 감시용 CCTV를 설치했는데, 매일 기존 카메라는 끄지 않고 새 카메라만 추가하는 상황. 시간이 지날수록 불필요한 카메라가 늘어나 전력과 저장공간을 낭비하는 것과 같음


3. 해결 과정 (단계별)

  1. 1단계: 이벤트 리스너 관리 시스템 구축
  • 무엇을 하는가: 이벤트 리스너를 변수에 저장하고 필요할 때 제거할 수 있는 구조 만들기
  • 왜 필요한가: 이벤트 리스너가 중복으로 추가되는 것을 방지하기 위함
  • 어떻게 하는가: 전역 변수를 만들어 이벤트 리스너 참조를 저장하고, 정리 함수 추가
  • 코드 변경:
// 변경 전 코드
document.addEventListener('tocGenerated', function() {
    initFloatingButtons();
});

document.addEventListener('click', function(e) {
    const navLink = e.target.closest('.post-nav-link');
    if (navLink) {
        setTimeout(function() {
            initFloatingButtons();
        }, 1000);
    }
});
// 변경 후 코드
// 전역 이벤트 리스너 관리
let tocGeneratedListener = null;
let navClickListener = null;

function initFloatingButtons() {
    // 이전 이벤트 리스너 제거
    cleanupEventListeners();
    
    // 새 이벤트 리스너 등록
    tocGeneratedListener = function() {
        initFloatingButtons();
    };
    document.addEventListener('tocGenerated', tocGeneratedListener);
    
    navClickListener = function(e) {
        const navLink = e.target.closest('.post-nav-link');
        if (navLink) {
            // 로직...
        }
    };
    document.addEventListener('click', navClickListener);
}

function cleanupEventListeners() {
    if (tocGeneratedListener) {
        document.removeEventListener('tocGenerated', tocGeneratedListener);
        tocGeneratedListener = null;
    }
    
    if (navClickListener) {
        document.removeEventListener('click', navClickListener);
        navClickListener = null;
    }
}
  • 변경 설명: 이벤트 리스너를 변수(tocGeneratedListener, navClickListener)에 저장해서 나중에 제거할 수 있게 했습니다. 새 리스너를 등록하기 전에 기존 리스너를 정리하는 함수(cleanupEventListeners)를 호출해 중복 등록을 방지합니다.
  1. 2단계: 타이머 자원 관리 개선
  • 무엇을 하는가: setTimeout으로 만든 타이머를 변수에 저장하고 필요할 때 취소
  • 왜 필요한가: 실행 중인 타이머가 중복으로 생성되는 것을 방지하기 위함
  • 어떻게 하는가: 전역 변수에 타이머 ID를 저장하고, 새 타이머 생성 전에 기존 타이머 취소
  • 코드 변경:
// 변경 전 코드
setTimeout(function() {
    initFloatingButtons();
}, 1000);
// 변경 후 코드
let initializationTimer = null;

if (initializationTimer) {
    clearTimeout(initializationTimer);
}

initializationTimer = setTimeout(function() {
    initFloatingButtons();
    initializationTimer = null;
}, 1000);
  • 변경 설명: 타이머 ID를 변수(initializationTimer)에 저장하고, 새 타이머를 만들기 전에 기존 타이머를 취소(clearTimeout)합니다. 타이머 작업이 끝나면 변수를 null로 설정해 정리합니다.
  1. 3단계: DOM 요소 재활용 및 이벤트 핸들러 정리
  • 무엇을 하는가: DOM 요소를 매번 새로 만들지 않고 가능한 재활용하기
  • 왜 필요한가: 불필요한 DOM 조작을 줄이고 이벤트 핸들러 메모리 누수 방지
  • 어떻게 하는가: 기존 요소가 있으면 내용만 비우고 재사용, 이벤트 핸들러도 정리
  • 코드 변경:
// 변경 전 코드
function createFloatingButtonsContainer() {
    const existingContainer = document.querySelector('.floating-buttons-container');
    if (existingContainer) {
        existingContainer.remove();
    }
    
    const container = document.createElement('div');
    container.className = 'floating-buttons-container';
    document.body.appendChild(container);
    
    // 버튼 생성...
}
// 변경 후 코드
function createFloatingButtonsContainer() {
    let container = document.querySelector('.floating-buttons-container');
    
    if (container) {
        // 기존 버튼의 이벤트 리스너 정리
        const buttons = container.querySelectorAll('button');
        buttons.forEach(button => {
            if (button.clickHandler) {
                button.removeEventListener('click', button.clickHandler);
            }
        });
        
        // 내용만 비우고 재활용
        container.innerHTML = '';
    } else {
        // 컨테이너 새로 생성
        container = document.createElement('div');
        container.className = 'floating-buttons-container';
        document.body.appendChild(container);
    }
    
    // 버튼 생성...
}
  • 변경 설명: 기존 컨테이너를 삭제하고 새로 만드는 대신, 있으면 내용만 비우고 재사용합니다. 또한 버튼에 연결된 이벤트 핸들러를 저장하고(button.clickHandler) 명시적으로 제거하여 메모리 누수를 방지합니다.
  1. 4단계: 광고 로드 로직 최적화
  • 무엇을 하는가: 광고 초기화 로직을 배치 처리 방식으로 변경
  • 왜 필요한가: 한 번에 많은 광고를 로드하면 성능 저하 및 메모리 사용량 증가
  • 어떻게 하는가: 광고를 소량씩 나누어 시간차를 두고 초기화
  • 코드 변경:
// 변경 전 코드
try {
    const adElements = document.querySelectorAll('.ad-container .adsbygoogle');
    console.log(`${adElements.length}개 광고 초기화 시작`);
    
    adElements.forEach((ad, index) => {
        if (!ad.getAttribute('data-adsbygoogle-status')) {
            try {
                (adsbygoogle = window.adsbygoogle || []).push({});
                console.log(`광고 ${index + 1} 초기화 완료`);
            } catch (e) {
                console.warn(`광고 초기화 실패:`, e);
            }
        }
    });
} catch (e) {
    console.error("광고 초기화 중 오류 발생:", e);
}
// 변경 후 코드
// 인아티클(본문 중간) 광고 찾기
const inArticleAds = document.querySelectorAll('.in-article-ad .adsbygoogle');
const allAds = [...inArticleAds, newAd];

// 배치당 최대 광고 수 (성능 최적화)
const BATCH_SIZE = 2;
let processedCount = 0;

const initializeAdBatch = () => {
    const batch = allAds.slice(processedCount, processedCount + BATCH_SIZE);
    
    if (batch.length === 0) {
        console.log('모든 광고 초기화 완료');
        return;
    }
    
    batch.forEach(ad => {
        // 이미 초기화된 광고는 건너뛰기
        if (!ad.getAttribute('data-adsbygoogle-status')) {
            try {
                console.log(`광고 배치 초기화 (${processedCount + 1}/${allAds.length})`);
                (adsbygoogle = window.adsbygoogle || []).push({});
            } catch (e) {
                console.warn(`광고 초기화 실패:`, e);
            }
        }
    });
    
    processedCount += batch.length;
    
    // 다음 배치 처리 (지연 로드)
    if (processedCount < allAds.length) {
        const nextTimer = setTimeout(initializeAdBatch, 300);
        this.adTimers.push(nextTimer);
    }
};

// 첫 배치 시작
const initialTimer = setTimeout(initializeAdBatch, 300);
this.adTimers.push(initialTimer);
  • 변경 설명: 모든 광고를 한 번에 초기화하는 대신, 최대 2개씩 나누어 시간차(300ms)를 두고 초기화합니다. 이렇게 하면 브라우저가 한 번에 과도한 작업을 하지 않아 성능이 개선됩니다.
  1. 5단계: 페이지 언로드 시 정리 로직 추가
  • 무엇을 하는가: 페이지를 떠날 때 모든 리소스를 정리하는 코드 추가
  • 왜 필요한가: 다른 페이지로 이동해도 메모리가 계속 점유되는 것을 방지
  • 어떻게 하는가: beforeunload 이벤트에 정리 함수 연결
  • 코드 변경:
// 변경 전 코드
// 페이지 언로드 시 정리 코드 없음
// 변경 후 코드
// 페이지 언로드 시 정리
window.addEventListener('beforeunload', function() {
    cleanupResources();
});

function cleanupResources() {
    cleanupEventListeners();
    
    if (initializationTimer) {
        clearTimeout(initializationTimer);
        initializationTimer = null;
    }
    
    if (window.BlogAds && window.BlogAds.cleanup) {
        window.BlogAds.cleanup();
    }
}
  • 변경 설명: 페이지를 떠나기 전에 모든 이벤트 리스너, 타이머 등의 리소스를 정리하는 함수를 실행합니다. 이렇게 하면 다른 페이지로 이동한 후에도 불필요한 메모리 점유를 방지할 수 있습니다.


4. 핵심 개념 설명

  • 개념 1: 메모리 누수(Memory Leak)
  • 쉬운 정의: 프로그램이 더 이상 필요하지 않은 데이터를 메모리에서 제거하지 않아 메모리 사용량이 계속 증가하는 현상
  • 일상 비유: 쓰레기를 계속 모으기만 하고 버리지 않아 집이 점점 좁아지는 상황과 같습니다. 결국 집이 쓰레기로 가득 차 움직일 수 없게 됩니다.
  • 왜 중요한가: 메모리 누수가 지속되면 웹사이트가 점점 느려지고, 결국에는 브라우저가 멈추거나 충돌할 수 있습니다.
  • 개념 2: 이벤트 리스너(Event Listener)
  • 쉬운 정의: 웹페이지에서 특정 사건(클릭, 스크롤 등)이 발생했을 때 실행될 동작을 지정하는 기능
  • 일상 비유: 초인종이 울리면 문을 열어보는 것처럼, "클릭"이라는 초인종이 울리면 "메뉴 열기"라는 동작을 수행합니다.
  • 왜 중요한가: 웹페이지와 사용자 간의 상호작용을 가능하게 하지만, 제대로 관리하지 않으면 메모리 누수의 주요 원인이 됩니다.
  • 개념 3: 가비지 컬렉션(Garbage Collection)
  • 쉬운 정의: 자바스크립트가 더 이상 필요하지 않은 데이터를 자동으로 찾아내 메모리에서 제거하는 과정
  • 일상 비유: 쓰레기 수거차가 주기적으로 와서 더 이상 필요하지 않은 쓰레기를 수거해가는 것과 같습니다.
  • 왜 중요한가: 메모리를 효율적으로 관리하지만, 특정 상황(중복 이벤트 리스너, 타이머 등)에서는 제대로 작동하지 않아 수동 정리가 필요합니다.
  • 개념 4: 배치 처리(Batch Processing)
  • 쉬운 정의: 많은 작업을 한 번에 처리하지 않고 작은 그룹으로 나누어 순차적으로 처리하는 방법
  • 일상 비유: 100개의 상자를 한 번에 들지 않고, 10개씩 나누어 10번에 걸쳐 운반하는 것과 같습니다.
  • 왜 중요한가: 브라우저가 한 번에 과도한 작업을 수행하지 않게 하여 웹페이지의 응답성을 유지하고 성능을 개선합니다.


5. 학습 및 예방

  • 배운 교훈: 웹 개발에서는 자원(이벤트 리스너, 타이머, DOM 요소 등)을 명시적으로 정리하는 습관이 중요합니다. "쓰레기는 만든 사람이 책임지고 치워야 한다"는 원칙이 웹 개발에도 적용됩니다.
  • 예방 방법:
  1. 이벤트 리스너는 변수에 저장하고 더 이상 필요하지 않을 때 removeEventListener로 제거
  2. setTimeout은 변수에 ID를 저장하고 필요할 때 clearTimeout으로 취소
  3. 새로운 DOM 요소를 만들기 전에 기존 요소를 재활용할 수 있는지 검토
  4. 페이지 언로드 시 모든 자원을 정리하는 코드 추가
  5. 대규모 작업은 작은 배치로 나누어 처리
  • 추천 학습 자료:
  1. "자바스크립트 메모리 관리 이해하기" - MDN 웹 문서
  2. "웹 성능 최적화 기초" - 구글 웹 개발자 가이드
  3. "이벤트 리스너와 메모리 관리" - 자바스크립트 입문 가이드


6. 요약

블로그 웹사이트에 목차 버튼과 스크롤 버튼을 추가했더니 사용 시간이 길어질수록 메모리가 계속 증가하는 문제가 발생했습니다. 원인은 이벤트 리스너와 타이머가 계속 중복 생성되고 정리되지 않는 것이었습니다. 이벤트 리스너를 변수에 저장하고 정리하는 시스템을 구축하고, 타이머를 관리하며, DOM 요소를 재활용하는 방식으로 해결했습니다. 또한 광고 로딩을 배치 처리 방식으로 개선하고 페이지를 떠날 때 모든 자원을 정리하는 코드를 추가하여 메모리 사용량을 안정화했습니다.

목차
목차를 불러오는 중...

댓글

Loading...

댓글 로딩 중...

구글 검색