프론트엔드 개발자

JavaScript (7) - 가비지 컬렉션(GC)란?

2025. 11. 30. 20:58front-end/JavaScript

728x90

JavaScript 가비지 컬렉션(GC)

더 이상 사용되지 않는 메모리를 자동으로 해제해주는 엔진 내부 기능.
자바스크립트는 개발자가 malloc() 같은 걸 직접 쓰지 않기 때문에, 대신 엔진(V8 등)이 알아서 사용 여부를 판단하고 메모리를 해제함

 

 

GC는 “어떤 메모리를 버릴지” 어떻게 판단할까?

핵심 개념: Reachability(도달 가능성)

  • 어딘가에서 접근 가능한 값이면 = 살아있음 (해제 X)
  • 어디에서도 접근할 수 없게 되면 = 죽음 (해제 O)

도달 가능한 시작점(roots):

  • 전역 객체 (window, global)

window 전역객체

  • 현재 실행 중인 함수 스코프
  • 클로저에 묶인 변수
  • call stack에 존재하는 변수들

예제 1

let a = { name: '홍소영' };
let b = a;

// 이제 a, b 둘 다 이 객체를 참조함.
a = null;

// 그래도 b가 객체를 가지고 있으므로 GC 대상 아님.
b = null;

// 이제 어떤 곳에서도 접근 불가 → GC가 해제

 

예제 2 - 함수 스코프

function test() {
  const temp = { x: 1 };
  return temp.x;
}

test();  // temp 객체는 사용 후 어디에서도 접근 불가 → GC 해제

 

예제 3 - 클로저 때문에 GC 안되는 경우

function outer() {
  const big = new Array(100000).fill('*'); // 큰 메모리
  return function inner() {
    console.log(big[0]);
  };
}

const fn = outer();
fn();

 

big은 inner()가 참조하기 때문에 outer()가 끝나도 메모리가 해제되지 않음.
→ 클로저는 GC에서 자주 발생하는 메모리 유지 원인

 

React / Next.js 에서 자주 발생하는 메모리 누수 원인

예제 1 - 이벤트 리스너 제거 안함

useEffect(() => { window.addEventListener('resize', handler); return () => window.removeEventListener('resize', handler); }, []);

 

 

예제 2 - setInterval / setTimeout 클리어 안함

useEffect(() => { const id = setInterval(...); return () => clearInterval(id); }, []);
예제 3 - WebSocket / SSE 연결 종료 안함
 
useEffect(() => { const ws = new WebSocket(...); return () => ws.close(); }, []);

 

 GC는 “즉시” 동작하지 않음

  • 일정한 타이밍에 검사하고 정리함
  • 브라우저나 Node가 바쁠 때는 뒤로 밀릴 수 있음
  • 전체 힙 크기가 커지면 GC 횟수가 증가 → 성능 저하 가능

GC 알고리즘 (간단 버전)

1) Mark-and-Sweep (표준)

  • root부터 도달 가능한 객체에 마크(mark)
  • 마크 안 된 객체는 모두 삭제(sweep)

https://ko.javascript.info/garbage-collection

 

2) Generational GC (V8)

'대부분의 객체는 금방 죽는다. 오래 살아남은 객체만 따로 관리하자!' V8엔진이 효율적으로 데이터를 관리하기 위한 핵심전략

  • 새로 생성된 객체는 ‘young generation’
  • 일정 시간 살아남으면 ‘old generation’ 이동
  • young GC는 매우 빠름

 

  • Young Generation (새 객체가 젤 처음 생성되는 구역)
    • 대부분의 객체는 매우 짧은 시간만 존재한다.
    • 그래서 Young 영역은 매우 자주 가비지 컬렉션을 한다.
    • 이 청소를 Scavenge(스캐빈지) 라고 부른다.
    • 빠르게 처리되도록 크기가 작고 정리 알고리즘도 간단하게 되어 있음.
  • Young GC 시나리오
    1. 새로 만든 객체 = Young 영역에 저장
    2. 조금 있다가 GC 실행
    3. 아직 살아있는 객체만 → Old 영역으로 이동(=promotion)
    4. 죽은 객체는 Young에서 즉시 제거
  • Old Generation (오래 살아남은 객체들이 저장되는 구역)
    • Young GC에서 살아남아서 promotion된 객체들이 있는 곳
    • 여기 있는 객체들은 생명주기가 긴 경우가 많음
    • 크기가 크고, GC 빈도는 Young보다 훨씬 적음
    • 대신 Mark-Sweep / Mark-Compact 같은 무거운 알고리즘 사용

 

왜 이렇게 나누는 걸까?

  • JS의 대부분 객체는 금방 사라진다
    함수 스코프 안에서 빠르게 만들고 금방 버려짐
    → Young Generation에서 빠르게 정리하는 게 효율적
  • 오래 살아남는 객체는 비용 비싸도 천천히 검사해도 된다
    예: 전역 상태, 싱글톤 객체 등
  • 메모리 정리 속도가 훨씬 빨라짐
728x90