메모리 누수는 React 앱에서 스크롤이 버벅거리거나, 탭 전환 후 메모리가 계속 증가하는 증상으로 나타납니다. 실무에서는 Chrome DevTools와 React DevTools로 누수를 확인하고, useEffect 정리(cleanup) 누락, 이벤트 리스너, 타이머, 네트워크 요청 미중단 등 흔한 패턴을 점검해 해결합니다.
1. 누수 징후 빠르게 파악하기
1) 페이지를 몇 번 이동하거나 컴포넌트를 반복 마운트/언마운트 했을 때 메모리가 계속 증가합니다. 2) 콘솔에 "Can't perform a React state update on an unmounted component" 경고가 보입니다. 3) DevTools Memory 탭에서 Detached DOM 노드가 증가합니다.
2. DevTools로 근거 수집하기
1) Performance 탭에서 "Record" 후 사용자 흐름(페이지 전환/리스트 열기/닫기)을 재현합니다. Allocation Instrumentation을 활성화하여 객체 할당 타이밍을 확인합니다. 2) Memory 탭의 Heap snapshot으로 Retainers에서 누수를 일으키는 참조(예: Detached HTMLDivElement, 이벤트 리스너 클로저, 큰 배열을 잡고 있는 ref)를 찾습니다. 3) React DevTools Profiler로 커밋당 메모리와 렌더 횟수를 확인하고, 특정 컴포넌트 언마운트 후에도 참조가 남는지 봅니다.
3. 재현용 누수 예시와 고치기
아래 컴포넌트는 타이머, 윈도우 이벤트, fetch 정리 누락으로 누수가 발생합니다.
import React, { useEffect, useState, useCallback } from "react";
function LeakyWidget() {
const [tick, setTick] = useState(0);
const onResize = useCallback(() => {
// 크기 변경 처리
}, []);
useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), 1000);
window.addEventListener("resize", onResize);
// fetch 중단 처리 없음, 언마운트 후 setState 시도 가능
fetch("/api/data")
.then(r => r.json())
.then(() => setTick(t => t + 1));
// cleanup 누락으로 누수 발생
}, [onResize]);
return <div>Tick: {tick}</div>;
}
export default LeakyWidget;정상적인 정리(cleanup)와 AbortController 적용으로 누수를 방지합니다.
import React, { useEffect, useState, useCallback } from "react";
function SafeWidget() {
const [tick, setTick] = useState(0);
const onResize = useCallback(() => {
// 크기 변경 처리
}, []);
useEffect(() => {
const controller = new AbortController();
const id = setInterval(() => setTick(t => t + 1), 1000);
window.addEventListener("resize", onResize);
fetch("/api/data", { signal: controller.signal })
.then(r => r.json())
.then(() => setTick(t => t + 1))
.catch(err => {
if (err.name !== "AbortError") console.error(err);
});
return () => {
clearInterval(id);
window.removeEventListener("resize", onResize);
controller.abort();
};
}, [onResize]);
return <div>Tick: {tick}</div>;
}
export default SafeWidget;4. 흔한 누수 패턴 체크리스트
1) 타이머/지연 작업: setInterval, setTimeout은 cleanup에서 clearInterval/clearTimeout을 호출합니다. 2) 이벤트 리스너: window/document/DOM 노드에 addEventListener 후 반드시 removeEventListener 합니다. 3) 네트워크 요청: AbortController로 언마운트 시 controller.abort()를 호출합니다. 4) 구독/소켓/옵저버: RxJS subscribe, WebSocket, IntersectionObserver, MutationObserver는 cleanup에서 unsubscribe/close/disconnect 합니다. 5) Ref에 큰 데이터 유지: useRef에 배열/맵을 계속 push하면 Retainers로 남습니다. 언마운트 시 ref.current를 초기화하고, 필요한 데이터만 보관합니다.
5. React 18 StrictMode 유의사항
개발 모드 StrictMode는 효과를 두 번 호출해 정리 로직을 검증합니다. 이로 인해 타이머 중복 생성이나 이벤트 리스너 중복 등록이 드러납니다. effect는 반드시 idempotent하게 작성하고, cleanup에서 생성한 모든 리소스를 정확히 해제합니다.
6. 진단 절차를 자동화하는 작은 팁
1) Heap snapshot을 두 번 이상 찍고, 동일한 탐색 흐름 후 비교하여 Detached 노드와 클로저 수가 줄었는지 확인합니다. 2) 컴포넌트 언마운트 후에도 fetch 응답이 setState를 호출하지 않는지 콘솔 경고를 모니터링합니다. 3) eslint-plugin-react-hooks로 의존성 배열을 정확히 유지하고, cleanup 누락을 린트 규칙으로 점검합니다.
7. 유용한 커스텀 훅 예시
AbortController를 반복적으로 쓰는 경우 커스텀 훅으로 패턴을 표준화합니다.
import { useEffect, useState } from "react";
export function useAbortableFetch(url, options) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch(url, { ...options, signal })
.then(r => r.json())
.then(setData)
.catch(e => {
if (e.name !== "AbortError") setError(e);
});
return () => controller.abort();
}, [url]);
return { data, error };
}8. 데이터/렌더링 전략으로 메모리 관리 강화
1) 큰 리스트는 react-window 등으로 가상화하여 DOM 노드 수를 제한합니다. 2) 무거운 객체를 전역 상태에 불필요하게 유지하지 말고, 필요 시 파생 데이터만 메모이제이션해 사용합니다. 3) 이미지/버퍼 등 대용량 리소스는 컴포넌트 생명주기에 맞춰 해제합니다.
9. 최종 확인 루틴
1) 동일한 사용자 시나리오를 5~10회 반복 후 Heap snapshot 용량이 안정적인지 확인합니다. 2) Memory 탭에서 Detached HTMLDivElement가 남아있지 않은지 검사합니다. 3) Profiler에서 언마운트 후 관련 컴포넌트가 더 이상 커밋되지 않는지 확인합니다.
핵심은 생성한 리소스를 동일한 곳에서 해제하는 일관된 cleanup입니다. DevTools로 근거를 확보하고, useEffect 정리와 요청 중단, 구독 해제, 타이머 제거를 습관화하면 실무에서 대부분의 React 메모리 누수를 빠르게 해결할 수 있습니다.
'React' 카테고리의 다른 글
| React 앱에 서버리스 함수(Function) 연동하기 (0) | 2026.06.05 |
|---|---|
| React에서 Chart.js를 사용한 동적 차트 생성 (0) | 2026.06.05 |
| React에서 스타일링을 위한 CSS Modules 활용하기 (0) | 2026.06.04 |
| React 앱에서 Web Share API 사용하여 콘텐츠 공유하기 (0) | 2026.06.04 |
| React에서 Drag-and-Drop API로 파일 순차 업로드 구현하기 (1) | 2026.06.04 |