![](https://velog.velcdn.com/images/woodong/post/b5299aa2-fa83-48b5-81d7-1b34fc486816/image.png)
- 웹 어플리케이션 내부에서 메모리 누수는 범위가 크고 추적하기가 어려워서 나중에 대응하는데 어려움이 존재합니다.
- 그래서 가비지 컬렉터가 어떻게 동작하는지 이해한다면 충분히 도움이 될 수 있을 것이라고 생각합니다!
객체 삭제 감지
- 자바스크립트는 객체가 가비지 컬렉터에 수집되는 시점을 감지하는 FinalizationRegistry API를 제공합니다.
- 실제로는 가비지 컬렉터가 수집을 해도 즉시 폐기하지는 않고 더 중요한 작업을 먼저 수행한 다음 나중에 일괄 처리한다고 합니다.
const registry = new FinalizationRegistry((message) => console.log(message));
function removeObject() {
const x = {};
registry.register(x, "x has been collected");
}
removeObject();
- 만약 강제로 빠르게 처리하고 싶으면 개발자 도구 > 메모리 > 휴지통 아이콘 클릭을 통해 제거할 수 있습니다.
![](https://velog.velcdn.com/images/woodong/post/bc0e550a-3ed3-45e1-b1ee-a4e0240c1911/image.png)
시나리오
1. 중첩된 객체
const registry = new FinalizationRegistry((message) => console.log(message));
function nestedObject() {
const x = {};
const y = {};
const z = { x, y };
registry.register(x, "x has been collected");
registry.register(y, "y has been collected");
registry.register(z, "z has been collected");
window.temp = x;
}
nestedObject();
결과
window.temp
가 참조하고 있는 x
는 가비지 컬렉터에 수집되지 않으며 z
와 y
만 참조하는 값이 없음으로 가비지 컬렉터에게 수집됩니다.
- 즉 알아서 최적화를 해주고 있다는 것을 볼 수 있습니다!
- 그리고 다시
window.temp = undefined
를 실행하면 x
도 가비지 컬렉터의 수집 대상으로 변경됩니다.
2. 클로저
const registry = new FinalizationRegistry((message) => console.log(message));
function closer() {
const x = {};
const y = {};
const z = { x, y };
registry.register(x, "x has been collected");
registry.register(y, "y has been collected");
registry.register(z, "z has been collected");
window.temp = () => z.x;
}
closer();
결과
- 당연히
window.temp
에 저장되어 있는 x
는 가바지 컬렉터에게 수집 대상에서 제외됩니다.
- 하지만 더 이상 접근할 수 없는
y
와 z
는 마찬가지로 가비지 컬렉터에게 대상에서 제외됩니다.
- 자바스크립트 엔진은
z.x
로 접근하게 되면 x
에 대한 직접 참조가 아닌 z
를 통해 참조하는 것으로 판단하여 z
에 대한 참조를 유지해야 하고 결국 y
의 참조도 유지하게 되는 것입니다.
- 또한
window.temp = () => z;
이렇게 z
를 클로저로 참조할 경우 마찬가지로 모두 수집하지 않습니다.
- 정리하자면 가바지 컬렉터는 참조하고 있는 객체를 판별할 때 대체할 수 있는 참조값을 모른다는 것이고 해당 기능을 수행하지 않는다는 것을 알 수 있습니다.
3. Eval
eval
은 자바스크립트에서 제공하는 함수로 문자로 표현된 자바스크립트 코드를 실행하는 함수이빈다.
console.log(eval('2 + 2'));
const registry = new FinalizationRegistry((message) => console.log(message));
function executionEval() {
const x = {};
registry.register(x, "x has been collected");
window.temp = (string) => eval(string);
}
executionEval();
결과
x
를 참조하는 곳이 없더라도 가비지 컬렉터는 x
를 수집하지 않습니다.
- 그러다 보니
windonw.temp('x')
를 통해 x
를 접근할 수 있게 됩니다.
- 이런 현상이 발생한 이유는 내부에
eval
함수가 렉시컬 스코프에 존재할 경우 어떤 객체도 수집하지 않습니다.
eval을 사용할 때 가비지 컬렉터가 동작하게 할 수 있을까?
eval
의 특징을 사용하면 가능합니다.
- 자바스크립트의
eval
은 direct eval
과 indirect eval
로 구분됩니다.
direct eval
: 직접 eval
을 호출, 지역 스코프를 참조
indirect eval
: 다른 변수에 담아서 eval
을 참조 형태로 호출, 무조건 글로벌 스코프를 참조
const registry = new FinalizationRegistry((message) => console.log(message));
function executionEval() {
const x = {};
registry.register(x, "x has been collected");
const evalFunction = eval
window.temp = (string) => evalFunction(string);
}
executionEval();
4. DOM 엘리먼트
const registry = new FinalizationRegistry((message) => console.log(message));
function domElement() {
const x = document.createElement("div");
const y = document.createElement("div");
const z = document.createElement("div");
z.append(x);
z.append(y);
registry.register(x, "x has been collected");
registry.register(y, "y has been collected");
registry.register(z, "z has been collected");
window.temp = x;
}
domElement();
결과
- 일반적인 객체와 다르게 부모인
z
엘리먼트와 자식인 x
와 y
는 서로를 참조하고 연결되어 있기 때문에 모두 가비지 컬렉터에 수집되지 않습니다.
- 이유는 당연히
z
엘리먼트에서 x
와 y
를 접근할 수 있기 때문입니다.
- 만약에
window.temp.remove()
를 통해 부모를 제거한다면 바로 x
와 y
는 가비지 컬렉터가 수집합니다.
5. 프로미스
const registry = new FinalizationRegistry((message) => console.log(message));
function asyncOperation() {
return new Promise((resolve, reject) => {
});
}
function promise() {
const x = {};
registry.register(x, "x has been collected");
asyncOperation().then(() => console.log(x));
}
promise();
결과
- 앞에서 가바지 컬렉터는 정밀하게 분석을 하지 않는다는 것을 알게 되었는데 여기서는 신기하게도
x
를 가비지 컬렉터가 수집합니다.
만약 resolve에 대한 참조를 가지고 있다면?
- 여기서는 신기하게도
resolve
를 실행하지 않더라도 a
를 가비지 컬렉터가 수집하지 않습니다.
window.temp()
를 실행하게 되면 x
가 출력되는 것을 볼 수 있습니다.
- 즉 정리하자면
Promise
가 이행되거나 가비지 컬렉터가 더 이상 프로미스의 resolve
및 reject
에 대한 경로를 추적할 수 없을 때 수집합니다.
const registry = new FinalizationRegistry((message) => console.log(message));
function asyncOperation() {
return new Promise((resolve, reject) => {
window.temp = resolve;
});
}
function promise() {
const x = {};
registry.register(x, "x has been collected");
asyncOperation().then(() => console.log(x));
}
promise();
React에서의 안티패턴
- 리액트에서 명시하고 있는 안티 패턴에 대한 내용과도 연관이 있습니다.
- 무언가가 계속
resolve
를 참조하고 있다면 컴포넌트가 마운트 해제된 이후에도 useEffect
와 렉시컬 스코프에 있는 모든 것들이 가비지 컬렉터에 수집되지 않고 살아있게 되면서 메모리 누수가 발생합니다.
- 즉
asyncOperation
가 이행되지 않는다면 컴포넌트가 unMounted 되더라도 isMounted
와 setStatus
는 계속 유지됩니다.
function MyComponent() {
const isMounted = useIsMounted();
const [status, setStatus] = useState('');
useEffect(async () => {
await asyncOperation();
if (isMounted()) {
setStatus('Great success');
}
}, []);
return <div>{status}</div>;
}
정리하기
- 가비지 컬렉터가 메모리를 회수하는 과정에서 여러 시나리오들을 이해하고 사전에 방지한다면 웹 어플리케이션에 대한 성능을 더 끌어올릴 수 있을 것입니다.
- 그리고 자바스크립트 엔진마다 또는 다른 버전마다 가비지 컬렉터가 수행하는 시나리오가 다를 수 있습니다. 하지만 여기서 소개된 시나리오는 V8(Chrome), JavaScriptCore(Safari), Gecko(Firefox)에서 동일하게 작동합니다!
참고