1. js는 자동으로 메모리를 관리한다
- 자바스크립트는 변수를 선언하거나 객체를 생성할 때, 데이터를 위한 메모리 공간을 확보한다.(메모리 할당)
const num = 101; // 선언시 num을 위한 메모리 할당됨
const str = "hello"; // 선언시 str을 위한 메모리 할당됨
- 그리고 데이터가 더 이상 필요 없으면, 가비지 콜렉션이라는 메모리 관리 기술을 이용해 메모리 공간을 비운다.(메모리 해제)
- 이를 통해 더 이상 사용하지 않는 데이터가 메모리 공간을 차지하지 않도록 보장해준다.
- 자바스크립트는 메모리 할당과 해제를 자동으로 관리한다. 그래서 우리가 자바스크립트를 사용할 때 메모리 관리에 대한 고민을 크게 하지 않게 된다.
💡 어? 그런데 자바스크립트에서 메모리 누수가 발생하는 경우가 있는데 왜 그런 걸까?
자동으로 메모리를 해제하는데??
- 분명 자바스크립트는 메모리를 자동으로 관리한다고 했다.
- 하지만 때때로 데이터를 사용하지 않지만 그 데이터의 메모리 공간이 유지되는, 메모리 누수가 발생한다!
- 이번 시간에는 메모리 라이프 사이클, 그리고 메모리 누수가 발생하는 원인에 대해 알아보았다.
2. 메모리 라이프 사이클
- JavaScript 메모리 라이프 사이클은 크게 할당, 사용, 해제로 나눌 수 있다.
단계 | 설명 |
---|---|
1. 메모리 할당(메모리 공간 확보) | - 변수를 선언할 때나 객체, 배열 등을 생성할 때 메모리 할당을 한다. |
2. 메모리 사용 | - 할당된 메모리를 읽거나 쓰는 단계이다. - 변수에 새로운 값을 할당할 때, 객체의 속성을 읽을 때 메모리를 사용한다. |
3. 메모리 해제(메모리 공간 비우기) | - 객체가 더 이상 필요하지 않게 되면, 메모리에서 해제한다. |
2-1. 메모리 할당하기
- 변수를 선언하거나 객체, 함수 등을 생성할 때 메모리가 자동으로 할당된다.
let name = 'lulu'; // name을 위한 메모리 공간 확보
const age = 28; // age을 위한 메모리 공간 확보
name = 'gicomong'; // name을 위한 메모리 공간 재확보
- 여기서 주의할 점은 데이터 유형에 따라 사용하는 메모리 형태가 다르다는 것이다.
number
,boolean
와 같은 기본 유형은 스택 메모리를 사용하고, 객체, 배열, 함수와 같은 참조 유형은 힙 메모리를 사용한다.
2-1-1. 기본 유형, 스택 메모리를 사용한다.
- 스택은 LIFO(후입선출) 방식으로 데이터를 저장하는 구조이다.
- 자바스크립트는 기본 유형을 선언하면, 스택 메모리에 해당 데이터의 변수명과 값을 저장한다.
- 스택 형식이기 때문에,
pop()
,push()
로 빠르게 데이터에 접근할 수 있는 장점이 있다. - 단, 스택은 크기가 제한되어 있다. 그래서 너무 많은 변수가 저장되면 오버플로우가 발생할 수 있다.
- 가장 최근에 선언된 변수는 스택 메모리의 맨 위에 배치된다.
2-1-2. 참조유형, 힙 메모리를 사용한다.
- 객체, 배열, 함수와 같은 참조 유형은 힙 메모리를 사용한다.
- 힙 메모리는 동적 메모리 할당 방식으로, 스택과 달리 크기가 유동적이며 용량이 크다.
- 참조 유형은 아래 이미지와 같이, 참조 주소가 스택 메모리에 저장되고 실제 데이터가 힙 메모리에 저장된다.
2-2. 메모리 사용하기
- 선언된 데이터에 접근한다는 건, 할당된 메모리를 읽고 쓰는 것을 의미한다.
- 아래는 메모리 사용 예시로,
n
이 저장된 메모리 위치를 조회하고 해당 값을 검색한다.
const n = 12;
console.log(n); // 12
2-3. 메모리 해제하기
- 선언된 데이터가 더 이상 필요하지 않으면 메모리 공간을 비운다.(메모리 해제)
- JavaScript는 가비지 콜렉션이라는 자동 메모리 관리 방법을 사용한다.
- 가비지 콜렉션은 메모리 할당을 추적하고 할당된 메모리 블록이 필요하지 않는지 판단해 회수한다.
- 하지만 가비지 콜렉션이 완벽하게 메모리를 자동 해제하지 못한다.
- 그 이유는 할당된 메모리가 더 이상 필요없는지 여부를 알기 어렵기 때문이다.
2-4. 가비지 켈렉션으로 메모리를 자동 관리한다.
- 앞서 언급했듯이, 가비지 컬렉션은 자동으로 메모리를 관리하는 기법이다.
- 가비지 콜렉터는 메모리 블록이 더 이상 필요하지 않는지 판단하여 회수한다.
- 가바지 콜렉터는 두 가지 알고리즘 방식이 있는데, 간단히 알아보자.
2-4-1. reference counting 알고리즘
💡 이 알고리즘은 '어떤 곳에서도 참조하지 않는 객체'를 '더 이상 필요 없는 객체'라고 간주한다.
참조되지 않는 객체를 "가비지"라 지칭하고, 가비지에 대한 메모리를 해제한다.
- reference counting 알고리즘 작동 방식은 아래와 같다.
let objectA = { value: "Hello" }; // 1
let objectB = objectA; // 2
objectA = null; // 3
objectB = null; // 4
{ value: "Hello" }
객체가 생성되고,objectA
은 이 객체를 참조한다. 객체의 참조 횟수는 1이다.objectB
는objectA
가 참조하는 동일한 객체를 참조한다. 객체의 참조 횟수는 2로 증가한다.- 객체에 대한 참조가 제거된다. 객체의 참조 횟수는 1로 감소한다.
- 마지막으로
objectB
를null
로 설정하면 객체의 참조 횟수가 0이 된다. 그러면, 자바스크립트 엔진은 객체가 더 이상 필요하지 않다 판단해, 해당 객체에 대한 메모리를 비운다.
2-4-2. Mark and Swap 알고리즘
💡 이 알고리즘은 "도달할 수 없는 객체"를 "더 이상 필요없는 객체"로 정의하여 메모리를 관리한다.
최신 엔진은 Mark and Swap 알고리즘을 사용한다.
- Mark and Swap 알고리즘은 root(루트)라는 개념이 있는데, 루트는 전역 객체를 가리킨다.
- 가비지 콜렉터는 root부터 root가 참조하는 객체, root가 참조하는 객체가 참조하는 객체 등을 찾는다.
- 그리고 접근 가능한 객체를
active
로 표시한다. - 그 다음, 가비지 콜렉터은
active
표시가 없는 모든 객체를 가비지로 간주해 메모리를 해제한다.
💡 자바스크립트 엔진은 최신인지에 따라 두 알고리즘 중 하나를 이용해 메모리를 관리한다.
하지만, 두 알고리즘을 사용해도 메모리 누수를 완전히 막을 수 없다ㅠ
그렇다면 어떤 경우에 메무리 누수가 발생할까?
3. JS의 메모리 누수가 발생하는 이유
3-1. 전역 변수를 선언할 때
3-1-1. 전역 변수 예시
- 전역 변수는 전역에서 접근할 수 있어 데이터가 필요하지 않을 때를 판단하기 어렵다.
- 그래서, 참조를 끊어도 메모리가 유지되는 문제가 발생한다.
// 전역변수 선언
let obj = { ... };
// obj를 null로 초기화해서 참조 끊기?
myData = null;
3-1-2. 메모리 누수를 막는 법
- 이 경우, 함수 스코프 단위에서 변수를 선언해, 함수 내에서만 변수 접근이 가능하도록 제한하는 방법이 있다.
- 함수 내에 변수를 선언하면, 함수가 필요하지 않을 때 변수들이 자동으로 가비지 처리가 된다.
function func() {
let obj = { ... };
}
func();
3-2. setTimeout, setInterval을 초기화하지 않았을 때
3-2-1. setTimeout, setInterval 예시
setTimeout
과setInterval
은 일정 시간에 특정 로직을 실행시킬 수 있는 스케쥴링 함수이다.setTimeout
함수는 시간이 경과하면 로직을 실행하며,setInterval
은 특정 시간 간격으로 로직을 반복 실행한다.
// 1초 후에 콘솔 출력
setTimeout(() => {
console.log("나는 1초 후에 실행해");
}, 1000);
// 1초에 한 번씩 콘솔 출력
setInterval(() => {
console.log("나는 1초마다 실행해");
}, 1000);
3-2-2. 메모리 누수를 막는 법
- 스케쥴링 함수는 초기화하지 않으면 계속 메모리를 차지한다.
- 그래서 각각의 스케쥴링 함수는 아래와 같은 방식으로 스케쥴링을 제거 해줘야 좋다.
// setTimeout을 별도 변수에 담고, clearTimeout로 제거하기
const timeoutId = setTimeout(() => {
console.log("나는 1초 후에 실행해");
}, 1000);
clearTimeout(timeoutId);
// setInterval을 별도 변수에 담고, clearInterval로 제거하기
const timer = setInterval(() => {
console.log("나는 1초마다 실행해");
}, 1000);
clearInterval(timer);
3-3. eventListener를 초기화하지 않았을 때
3-3-1. eventListener 예시
eventListener
는 요소에 이벤트를 등록할 수 있는 리스너이다.- 만약 확인 버튼(
confirmBtn
)이 클릭할 때마다 콘솔을 출력한다고 가정하자. - 그리고 특정 상황에서 확인 버튼이 제거된다면, 이벤트 리스너는 메모리에서 제거될까? 아니다ㅠ
const confirmBtn = document.getElementById("confirmBtn");
// confirmBtn에 클릭이벤트 등록하기
confirmBtn.addEventListener("click", onClickConfrim);
function onClickConfrim() {
console.log("확인 버튼이 클릭됐따!");
}
3-3-2. 메모리 누수를 막는 법
- 이벤트 리스너는 여전히 버튼 요소를 참조하고 있기에, 가비지 대상이 되지 않는다.
- 그래서 요소가 필요 없으면,
removeEventListener
로 이벤트 리스너를 제거해야한다.
const confirmBtn = document.getElementById("confirmBtn");
confirmBtn.removeEventListener("click", onClickConfrim);
function onClickConfrim() {
console.log("확인 버튼이 클릭됐따!");
}
3-4. 사용하지 않는 값을 가진 클로저
3-4-1. 클로저란?
- 클로저는 중첩 함수에서 외부 함수가 종료되어도 내부 함수가 외부 함수에 접근할 수 있는 환경이다.
- 내부 함수는 함수 내부에 선언된 함수를 가리키고, 외부 함수는 내부 함수를 포함하는 함수를 지칭한다.
- 아래의 예시에서
outer()
는 외부 함수이며,inner()
는 내부 함수이다. - 클로저는 로직의 응집성을 높인다는 장점이 있지만, 잘 못 사용하면 메모리 누수를 일으킨다.
function outer(){
const inner = function() {
console.log('haha');
}
inner();
}
inner(); // Error: inner is not defined
3-4-2. 클로저에서 메모리 누수를 막는 법
- 아래 코드를 보면, 외부 스코프의 변수(
arr
)는 사용되지 않지만 선언되어 있다. - 그리고 내부 스코프에서는 이 변수를 사용하고 있어 참조가 유지되고 있다.
function outer(){
const arr = []; // 사용하지 않는 변수(원인)
return function inner(v){
arr.push(v);
}
}
// 내부 함수에 접근하기
const appendNum = outer();
// 내부 함수 여러번 호출하기
for (let i=0; i< 999999; i++){
appendNum(i); // 반복 호출하면서 arr변수의 메모리 증가!
}
- 함수 스코프 변수는 함수의 호출이 완료되면 메모리에서 정리된다.
- 하지만 클로저에서는 외부 스코프 변수를 내부 함수에서 참조하면, 참조가 유지된다.
- 그래서 리턴되지 않는 변수(
arr
)를 외부 스코프에 선언하고 내부 함수에서 사용하면, 참조가 유지되어 누수가 발생한다. - 이 경우, 클로저 사용시 외부 범위의 변수를 사용하거나 내부 변수를 반환하도록 수정해야 한다.
4. 메모리 누수 측정하는 법
💡 메모리 누수가 발생하는 상황을 알아봤는데, 어떻게 해야 메모리 누수가 발생하는 지 알 수 있을까? Chrome에서 확인할 수 있다!
4-1. 성능 패널 이용하기
- 성능 패널에서 기록을 하면, JS Heap 영역을 확인할 수 있다.
- 기록 결과창에서 선 그래프의 JS Heap은 힙 메모리 사용량을 나타낸다.
- 선 그래프가 상승 추세라면 프로그램이 지속적으로 메모리를 소비하고 있다는 말로, 메모리 누수가 있을 가능성이 있다.
4-2. 메모리 패널 이용하기
- 메모리 패널은 실시간으로 메모리 사용량을 볼 수 있다.
- 아래와 같은 순서로 [타임라인 할당 계측]을 ON을 하고 기록을 해보자!
- 녹화를 하면, 그래프에서 파란색 히스토그램이 생성되는 것을 볼 수 있다.
- 이 파란색 히스토그램은 현재 메모리 양을 나타낸다.
- 히스토그램에서 회색은 이전에 차지하던 메모리 공간이 해제되었음을 나타낸다.
- 만약 사이트에서 파란색 히스토그램만 생성되고 회색으로 변하지 않는다면, 메모리가 해제되지 않는다는 말로, 프로그램에 메모리 누수가 있을 수 있다.
5. 마치며…
- 이번시간에는 자바스크립트의 메모리 라이프 사이클과 메모리 누수 원인 등을 알아보았다.
- 메모리 누수는 주로 전역 변수 선언,
setTimeout
/setInterval
초기화 누락 등에 의해 발생한다. - 이를 방지하기 위해 전역 변수는 함수 스코프 단위에서 선언하며,
setTimeout
은clearTimeout
로 제거하는 등의 방법이 있었다. - 만약 서비스에서 메모리 누수를 측정하고 싶다면, 크롬의 성능 패널과 메모리 패널을 이용해서 해결해보자.
출처
- The Secrets of Memory Leaks in JavaScript You Don’t Know
- JavaScript의 메모리 관리
- JavaScript Memory Management: How to Avoid Common Memory Leaks and Improve Performance
- How to escape from memory leaks in JavaScript
반응형
'개발 기술 > 개발 이야기' 카테고리의 다른 글
Vue 3.4 변경점 파헤치기 (0) | 2024.02.29 |
---|---|
feature flag로 지속적 배포하기(with. postHog, react) (0) | 2024.01.20 |
구글 검색 엔진과 SEO을 알아보자!(with. SEO 측정 방법) (2) | 2023.11.12 |
[Vue] 컴포저블에서 props의 반응성 유지하기(feat. toRef, unref) (0) | 2023.10.29 |
[Vue] prop drilling을 예방하는 Provider Pattern 알아보기 (2) | 2023.09.30 |
댓글