GC는 느리지 않다 — 프론트엔드가 메모리를 차지하고 있을 뿐
Source: Dev.to
Garbage Collection (GC)은 프론트엔드 엔지니어가 존재한다는 건 알지만 거의 생각하지 않는 주제 중 하나입니다—하지만 무언가가 끊기거나, 멈추거나, 신비롭게 느려질 때까지는 말이죠.
성능 문제가 발생하면, GC가 흔히 기본 용의자가 됩니다:
“가비지 컬렉터가 실행되고 있는 것 같아.”
때때로 그것이 맞을 수도 있습니다. 하지만 대부분은 그렇지 않습니다.
이 글에서는 프론트엔드 애플리케이션에서 흔히 떠돌아다니는 가비지 컬렉션에 대한 오해를 풀어보고, 브라우저가 실제로 하는 일과 성능 문제가 더 가능성이 높은 원인이 어디인지 살펴보겠습니다.
Myth 1: “Garbage Collection Is Random”
GC는 무작위가 아닙니다.
현대 JavaScript 엔진(V8, SpiderMonkey, JavaScriptCore)은 결정론적이며 휴리스틱 기반의 컬렉터를 실행합니다. 컬렉션 시점은 다음을 기준으로 결정됩니다:
- 할당 속도
- 힙 크기
- 메모리 압박
- 이전 GC 동작
GC가 실행되는 이유는 메모리를 할당했기 때문이며, 브라우저가 기분이 안 좋아서가 아닙니다.
자주 GC 일시정지를 경험한다면 보통 다음과 같은 상황을 의미합니다:
- 할당량이 너무 많음
- 할당 속도가 너무 빠름
- 예상보다 오래 메모리를 보유하고 있음
Myth 2: “Garbage Collection Only Happens When Memory Is Full”
이것은 가장 오래 지속되는 오해 중 하나입니다.
가비지 컬렉션은 메모리가 소진되기 전에 자주 실행됩니다.
왜일까요? 힙이 무한히 커지는 것을 허용하면 다음과 같은 문제가 발생합니다:
- 캐시 지역성이 악화됨
- 이후에 GC 일시정지가 길어짐
- 탭 간 메모리 압력이 증가함
현대 엔진은 드물게 발생하는 대규모 컬렉션보다 빈번하고 점진적인 컬렉션을 선호합니다.
실제로는 다음과 같은 의미입니다:
- 충분한 메모리가 남아 있어도 작은 GC 일시정지가 발생함
- 메모리가 “가득 찰 때”까지 기다리는 것은 훨씬 더 나쁜 결과를 초래함
신화 3: “GC 일시정지는 항상 길고 눈에 띈다”
This used to be true. It isn’t anymore.
Modern browsers use:
- 세대별 GC (young vs. old objects)
- 증분 GC (work spread over time)
- 동시 GC (running off the main thread where possible)
Most garbage collections today are:
- 짧다
- 증분
- 사용자에게 보이지 않는다
If users notice GC, it usually means:
- 너무 많은 객체를 오래된 세대로 승격시켰다
- 중요한 UI 단계에서 메모리 압박을 발생시켰다
Myth 4: “Creating Objects Is Expensive Because of GC”
Object creation is usually cheap. Holding onto objects is expensive.
The real GC cost comes from:
- Long‑lived objects
- Retained closures
- Detached DOM nodes
- Global caches that never shrink
A fast allocation followed by fast reclamation is ideal.
Problems arise when:
- Temporary objects accidentally become long‑lived
- References are kept in unexpected places
GC doesn’t punish allocation—it punishes retention.
신화 5: “변수를 수동으로 null 로 설정하면 GC에 도움이 된다”
변수를 null 로 설정하는 것이 도움이 되는 경우는 드뭅니다.
왜일까요? JavaScript 엔진은 변수 이름이 아니라 도달 가능성을 추적합니다. 객체가 도달 불가능해지면, 수동으로 null 로 설정했든 아니든 가비지 컬렉터가 수집합니다.
수동으로 변수를 null 로 설정하는 것이 도움이 되는 경우는 다음과 같습니다:
- 참조 체인을 끊을 때
- 스코프가 종료되기 전에 큰 구조를 미리 해제할 때
무분별하게 변수를 null 로 설정하면 보통:
- 불필요한 잡음이 생깁니다
- 가독성이 떨어집니다
- 측정 가능한 이점이 없습니다
// Example: breaking a reference chain
let largeData = fetchHugePayload();
process(largeData);
largeData = null; // only useful if you need to free it before the function ends
신화 6: “메모리 누수는 항상 명백하다”
대부분의 프론트엔드 메모리 누수는 미묘합니다.
실제 사례에서 흔히 볼 수 있는 누수는 다음과 같습니다:
- 이벤트 리스너를 제거하지 않음
- 클로저가 큰 객체를 캡처함
- DOM 노드가 화면에서 제거되었지만 여전히 참조됨
- 캐시 키가 잘못 지정됨
이러한 누수는 즉시 메모리를 폭발시키지 않습니다. 힙 사용량이 서서히 증가하여 가비지 컬렉터(GC)가 정상적으로 처리하지 못할 때까지 계속됩니다. GC가 눈에 보이게 될 때쯤이면, 누수는 이미 오랫동안 존재해 온 경우가 대부분입니다.
신화 7: “프레임워크가 GC를 처리한다”
프레임워크는 도움이 되지만, GC를 사라지게 하지는 않는다.
They can:
- 우발적인 메모리 누수를 줄인다
- 예측 가능한 라이프사이클을 장려한다
They cannot:
- 논리적 유지 버그를 방지하지 못한다
- 클로저 오용을 수정하지 못한다
- 커스텀 이벤트 시스템을 자동으로 정리하지 못한다
자바스크립트를 작성한다면, 프레임워크와 관계없이 메모리 동작에 대한 책임은 여러분에게 있다.
GC가 실제로 프론트엔드 앱에 문제를 일으키는 경우
GC 문제는 다음 상황에서 나타나는 경우가 많습니다:
- 초기 페이지 로드
- 대규모 재렌더링
- 애니메이션이 많은 인터랙션
- 빠른 상태 업데이트
문제는 드물게 “GC 자체”가 아닙니다. 보통은 다음과 같습니다:
- 루프 안에서 과도한 할당
- 레이아웃 + 할당이 동시에 발생
- 중요한 렌더링 단계에서 메모리 churn
프론트엔드 엔지니어가 GC를 생각하는 방법
Instead of fearing GC, adopt better mental models:
- Allocate freely, retain carefully
자유롭게 할당하고, 신중하게 유지하세요 - Avoid unnecessary long‑lived references
불필요한 장기 레퍼런스를 피하세요 - Clean up subscriptions and listeners
구독 및 리스너를 정리하세요 - Measure memory, not just performance
성능뿐 아니라 메모리도 측정하세요
GC is not your enemy. Unintended memory retention is.
GC는 당신의 적이 아닙니다. 의도치 않은 메모리 유지가 적입니다.
최종 생각
프런트엔드 성능이 저하될 때, 가비지 컬렉션을 탓하는 것은 쉽다. 이를 이해하는 것은 더 어렵지만 훨씬 더 유용하다.
대부분의 성능 향상은 “GC를 피하는 것”에서 오는 것이 아니다. 브라우저가 이미 효율적으로 메모리를 관리하는 방식에 코드를 맞추는 데서 온다. 그렇게 하면 GC는 배경으로 사라져—제자리인 배경에 머무르게 된다.