
👩🏻💻 JavaScript의 메모리 누수의 발생과 해결방법
C언어에서의 메모리 누수를 알아보기 전 메모리 구조를 뜯어봤듯이, JavaScript 에서의 메모리 누수도 먼저 메모리의 전체적인 구조를 더 정확히 짚고 넘어가야할 것 같아서 공부를 한 이후에 메모리 누수를 해결할 방법에 대해서 찾아보고 또 생각을 해봤다.
🖐🏻 JavaScript 콜 스택, 메모리 힙
- 자바스크립트는 콜스택과 메모리힙이라는 메모리 힙이라는 메모리 구조를 통해 데이터 및 코드실행을 관리함
🥞 콜 스택
원시타입(숫자, 문자열 등) 데이터가 저장됨
실행 컨텍스트(Execution Context)를 통해 아래의 역할을 수행
변수 식별자(이름) 저장
스코프 체인 및 this 관리
코드 실행 순서 관리
🧳 메모리 힙
- 참조타입(객체 등) 데이터가 저장됨
🙋🏻♀️ 콜스택, 메모리 힙의 데이터 구조
- 자바스크립트는 콜스택과 메모리힙이라는 메모리 구조를 통해 데이터 및 코드 실행을 관리함
[ 예제 1 ]
const pokemon = 10;
const pikachu = { name : "피카츄"};

[ 예제 2 ]
const name = "Harry Potter";
const freinds = ["Ron", "Dobby", "James"];
const harry = {
name : "Harry Potter",
job : "auror"
}
const dobby = () => {
console.log("is Free!");
}

🥞 원시타입 데이터
name"Harry Potter"라는 값 자체는 원시타입이므로 콜 스택에 저장됨변수
name에는"Harry Potter"가 저장된 콜스택 메모리의 주솟값이 저장됨- 변수 식별자
name자체는 콜스택 상의 실행컨텍스트 렉시컬 환경 이라는 곳에 저장됨
- 변수 식별자
🧳 참조타입 데이터
freinds,harry,dobby배열, 객체, 함수 등은 참조타입이므로 메모리 힙에 저장됨
메모리 힙의 주소값이 저장된 콜스택의 주소값은 각각 변수
freinds,harry,dobby에 저장됨- 변수 식별자
freinds,harry,dobby이름 자체는 콜스택 상의 실행컨텍스트 렉시컬 환경에 저장됨
- 변수 식별자
🙋🏻♀️ 더 자세하게 봅시다!
🥞 원시타입의 변수 데이터
원시타입 변수 생성
원시타입의 데이터 값은 콜스택에 저장
데이터 값이 저장된 콜스택의 주소값은 변수에 각각 저장됨
변수의 각 식별자 자체는 실행 컨텍스트의 렉시컬 환경에 저장됨
let dobby = "is Free!";
let leetrue = "is NOT FREE!";

[ 원시타입 재할당 - Case 1 ]
let dobby = "is Free!";
let leetrue = "is NOT FREE!";
dobby = "is NOT FREE!";
변수
dobby에"is NOT FREE!"를 재할당 하면,본인의 메모리에 있는 값을 변경하는 것이 아닌, 기존에
"is NOT FREE!"를 저장하고 있던 메모리의 주소값으로 교체함dobby에 저장된 주소값은 주소값"is NOT FREE!"에 저장된 주소값과 동일해짐

[ 원시타입 재할당 - Case 2 ]
let dobby = "is Free!";
let leetrue = "is NOT FREE!";
dobby = "is NOT FREE!";
leetrue = "is wizard";
- 변수
leetrue에 새로운“is wizard”를 재할당하면 새로운 메모리를 확보해“is wizard”를 저장하고, 변수leetrue에 저장된 주소값을 해당 주소값으로 교체

[ 가비지 컬렉터 ]
let dobby = "is Free!"; // garbage collector
let leetrue = "is NOT FREE!";
dobby = "is NOT FREE!";
leetrue = "is wizard";
- 더이상 참조되지 않는 데이터는 가비지 컬렉터에 의해 적절한 시점에 메모리에서 해제됨

🧳 참조타입의 변수 데이터
참조타입 변수 생성
참조타입 데이터는 메모리 힙에 저장됨
메모리 힙의 주소값은 콜 스택에 저장되고 각 변수에는 해당 콜스택의 주소값이 각각 저장됨
const gen1 = ["이상해씨", "파이리", "꼬부기"];
let gen2 = ["치코리타", "브케인", "리아코"];

[ 참조타입 데이터의 값 변경 ]
const gen1 = ["이상해씨", "파이리", "꼬부기"];
let gen2 = ["치코리타", "브케인", "리아코"];
gen1.push("피카츄"); // 여기!
gen2.push("피츄"); // 여기!
변수에 값을 재할당한 것이 아니라, 변수에 저장된 데이터를 수정한 것
메모리힙에 저장된 배열의 값을 변경하더라도, 배열이 저장된 메모리힙의 주소는 그대로임
각 변수 입장에서는 사실상 변한 것이 없음
- 따라서 재할당이 필요없는 경우, 참조타입은 const 로 선언

[ let으로 선언된 참조타입의 재할당 ]
const gen1 = ["이상해씨", "파이리", "꼬부기"];
let gen2 = ["치코리타", "브케인", "리아코"];
gen2 = ["피츄", "메리프", "꼬지모"]; // 여기!
메모리 힙에 새로운 주소가 확보되고 새로운 배열이 저장됨
콜스택에 저장되는 메모리 힙의 주소값도 새로운 주소로 변경이 필요함
이 때, 콜스택에 기존에 저장되어 있던 주소값을 바꾸는 것이 아닌, 새로운 메모리를 확보해 새로운 메모리힙의 주소값을 저장함
따라서, 변수
gen2에 저장되는 콜스택의 주소값도 변경됨
참조타입은 동일한 구성의 객체를 생성하더라도, 매번 새 메모리를 확보해 새 객체를 생성함

[ const로 선언된 참조타입의 재할당 ]
const gen1 = ["이상해씨", "파이리", "꼬부기"];
let gen2 = ["치코리타", "브케인", "리아코"];
gen1 = ["피츄", "메리프", "꼬지모"]; // TYPE ERROR : ASSIGNMENT TO CONSTANT VARIABLE
const 로 선언된 변수에 다른 배열을 할당하면 에러가 남
메모리 힙에 새로운 공간을 확보해 다른 배열을 저장하려면, 콜스택에서도 새로운 메모리를 확보해 해당 메모리힙의 주소를 저장해야하는데, 이는 결과적으로 const 로 선언된 변수에 저장되어있는 주소값도 변경하게 만듦
const 로 선언된 변수는 본인에게 할당된 콜스택 주소값의 변경을 허용하지 않음
따라서 위 코드는 실행이 불가

[ 동일한 구성의 참조타입 데이터 ]
const pokemon = {
name : "피카츄",
type : "전기"
};
const pikachu = {
name : "피카츄",
type : "전기"
};
객체와 같은 참조타입 데이터는 동적으로 내부 요소가 변경될 수 있는 관계로
동일한 요소의 객체를 생성하더라도 메모리힙에 각각 별ㄷ로의 메모리 공간을 확보해 저장됨
따라서
pokemon !== pikachu

🙋🏻♀️ 자바스크립트의 소스코드 실행 방식
자바스크립트의 메모리 구조와 그 할당방식까지만 이해하면 될 것이라 생각했지만, 이 참에 소스코드가 실행되는 방식에서의 단계도 확실히 이해를 하고 있어야 조금 더 내가 원하는 답에 다가갈 수 있을 것 같아서 추가적으로 공부를 해봤다.
소스 코드 실행 단계
1단계 : 선언문들을 찾아서 변수 등을 실행 컨텍스트의 렉시컬 환경에 등록
2단계 : 소스코드를 첫 줄부터 실행하며 값을 할당하고 참조함
1️⃣ 1단계 : 생성단계 Creation Phase
자바스크립트는 소스코드 실행 전 소스코드 전체를 훑으며 오류 여부를 체크하는 과정을 거침
이 때, 실행 컨텍스트를 생성하고 변수 및 함수를 등록해둠
위 과정을 소스코드 평가 과정, 소스코드 스캔 과정이라고도 함
실행컨텍스트란?
자바스크립트가 코드를 실행하기 위해 관리하는 스택 구조의 실행환경
자바스크립트는 실행컨텍스트라는 구조를 통해
- 실행 시점의 변수, 함수, this, 스코프 등을 관리하며 명령어를 실행해 나감
렉시컬 환경이란?
ECMAScript 명세서에서 “자바스크립트 엔진이 변수를 어떻게 관리해야 하는지” 기술하기 위해 사용된 명칭
실행컨텍스트의 컴포넌트로 변수 및 스코프, this 등을 관리하기 위한 추상적인 이론상의 개념이며 구체적인 객체를 의미하는 것은 아님
자바스크립트 엔진들은 각기 다른 방식으로 ECMAScript 의 렉시컬 환경에 사항을 준수하며 변수 환경을 관리함
[ 자바스크립트 소스코드 실행 방식 1단계(생성 단계) 예시 ]

// 변수 선언문
var a = 10;
let b = 20;
const c = 30;
// 함수 선언문
function func_1(arg){
return arg;
}
// 함수 표현식(var)
var func_2 = function(arg){
return arg;
}
// 함수 표현식(const/let)
const func_3= function(arg){
return arg;
}
👉🏻 1단계 : 생성단계 Creation Phase 에서의 변수
var
var a코드를 발견하면실행 컨텍스트의 렉시컬 환경에 변수를 등록하고
콜 스택에 데이터 저장을 위한 메모리를 할당하고
해당 콜스텍 메모리 주소값을 변수에 저장하고
확보된 콜스택 메모리에는
undefined를 할당(초기화) 해둠
따라서 var 로 선언된 변수는 선언문 앞에서도 참조할 수 있게 되며, 이것이 호이스팅의 원리임
const , let
let b,const c코드를 발견하면실행 컨텍스트의 렉시컬 환경에 변수를 등록하고 끝이남
따라서, 이 상태에서 변수를 참조하면 ReferenceError 가 발생
- Temporary Dead Zone 발생
이것이
let /const로 선언된 변수가 절반만 호이스팅된다고 말하는 이유
var 와 const / let 변수가 저장되는 위치
var 로 선언된 변수는 렉시컬 환경 내부의 객체 환경 레코드에 등록됨
- 전역 객체 window.a 등으로 참조 가능
전역에서 const / let 으로 선언된 변수는 렉시컬 환경 내부의 선언적 환경 레코드에 등록됨
- 전역 객체에서 참조할 수 없음
👉🏻 1단계 : 생성단계 Creation Phase 에서의 함수
함수 선언문 (function 으로 생성)
function func_1코드를 발견하면실행 컨텍스트의 렉시컬 환경에 함수 식별자 (
func_1)를 등록메모리 힙에 공간을 확보
콜스택에 메모리 힙의 주소를 저장
함수 변수 (
func_1)에 콜스택의 주소를 저장메모리힙에 함수 내용을 저장
완전 호이스팅되어 함수 선언문 앞에서도 함수를 호출할 수 있음
함수 표현식 (var, let / const)로 생성
변수와 동일하게 작동
var, let/const 에 따라 다름
var 로 선언된 함수 :
undefined할당let / const 로 선언된 함수 : 참조 불가능
2️⃣ 2단계 : 실행단계 Execution Phase
- 코드를 한 줄씩 해석하며 실행해나가는 단계 → 런타임 (Runtime)

👉🏻 2단계 : 실행단계 Execution Phase 에서의 변수
var
a = 10;코드를 만나면콜스택에 확보되어 있던 메모리(
undefined)말고, 새로운 메모리를 다시 확보해서 실제 값(10) 저장변수
a에도 새로운 콜스택 주소값을 저장
let & const
let b = 20; const c = 30;코드를 만나면let b, const c부분에서 메모리를 확보해undefined를 할당,undefined가 저장된 콜스택 메모리 주소값을 변수에 저장다음으로
b = 20; c = 20;부분에서 다시 새로운 메모리를 확보해 실제값(b=20; c=30;)을 저장새로운 콜스택 메모리의 주소값을 변수 b, c 에 다시 저장(교체)
👉🏻 2단계 : 실행단계 Execution Phase 에서의 함수
함수 선언문
- 1단계 (생성 단계) 에서 이미 함수 식별자 및 구현부까지 메모리에 할당이 완료된 상태
함수 표현식
- 변수와 동일하게 작동
🧹 가비지 컬렉터
사실 가비지 컬렉터는 다음에 공부해보려 했지만.. 메모리 누수에 있어서 꼭 필요한 부분이라는 생각이 들어서 간단하게나마.. ^0^/ 이 부분에서의 핵심은 자바스크립트의 가비지 컬렉터 알고리즘인 것 같다.
👉🏻 가비지 컬렉션 알고리즘의 핵심개념 “ 참조 “
A 라는 메모리를 통해 B 라는 메모리에 접근이 된다면, “B는 A에 참조된다” 라고 함
참조 세기 (Reference-counting) 가비지 컬렉션
더 이상 필요없는 오브젝트를 어떤 다른 오브젝트도 참조하지 않는 오브젝트로 정의하는 알고리즘
이런 오브젝트를 “가비지”라고 부르며 이를 참조하는 다른 오브젝트가 하나도 없을 때 수집이 가능
let pokemon = {
type: {
watery : "꼬부기"
}
};
// 2개의 오브젝트가 생성되었고
// 하나의 오브젝트는 다른 오브젝트의 속성으로 참조되었으며
// 나머지 하나는 pokemon 이라는 변수에 할당
// 가비지 컬렉션이 수행될 메모리는 하나도 없음
let myPokemon = pokemon; // myPokemon 변수는 위의 오브젝트를 참조하는 변수
pokemon = "갓겜"; // myPokemon 변수가 위의 오브젝트를 참조하는 유일한 변수가 됨
let myWateryPokemon = myPokemon.type;
// 위의 오브젝트의 type 속성을 참조함
// myPokemon 은 두 개의 참조를 가짐
// myPokemon 은 속성으로 참조하고 myWateryPokemon 이라는 변수가 참조함
myPokemon = "피카츄";
// 제일 처음 myPokemon 변수가 참조했던 오브젝트를 참조하는 오브젝트는 없음
// 하지만 가비지 컬렉션은 수행될 수 없음
// 오브젝트 type 의 속성이 여전히 myWateryPokemon 에 의해 참조되므로 메모리 해제가 안됨
myWateryPokemon = "따라큐";
// myWateryPokemon 변수에 다른 값을 할당함
// 이제 제일 처음 pokemon 의 변수가 참조했던 오브젝트를 참조하는
// 다른 변수는 없으므로 가비지 컬렉션이 수행됨
순환 참조의 한계
- 두 객체가 서로 참조하는 속성으로 순환 구조가 생성되는 경우에 발생
function recursion() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1는 obj2를 참조
obj2.a = obj1; // obj2는 obj1을 참조
return "ref each other";
}
recursion();
위 코드에 따르면 함수 내에서 선언된 두
obj1,obj2변수는 해당 함수의 지역변수이기 때문에 함수 실행 이후 메모리가 바로 해제되어야 하지만, 실제 메모리 상에서는 두 객체가 서로 참조하므로 해제가 되지 않음recursion()함수가 호출될 때마다 해당 객체들 만큼의 메모리가 할당이 되고 해제되지 않으므로 그만큼 메모리가 낭비됨
👉🏻 표시하고 쓸기 (Mark-and-sweep) 알고리즘
가비지 컬렉터는 주기적으로 roots 로 시작해 이것이 참조하는 오브젝트들, 참조되는 오브젝트가 참조하는 오브젝트로 이동하며 접근할 수 있는 오브젝트를 표시함
그리고 접근할 수 없는 오브젝트들에 대해 가비지 컬렉션을 수행
접근 가능한 오브젝트의 기준?
원래 접근 가능한 오브젝트 root
root 가 참조하는 값이나 루트에서 참조할 수 있는 값
[ Mark-and-sweep 예제 ]
let object = { name : "leetrue" };
let pointer = object;
object = null;
console.log(pointer); // { name : "leetrue" }
roots-pointer-object로 접근 가능하므로 해제되지 않음
🙋🏻♀️ 자바스크립트에서 메모리 누수가 발생하는 이유
전역변수
자바스크립트의 전역 변수는 루트 노드를 참조하기 때문에 (
window,global) 애플리케이션의 생명주기 동안 절대로 가비지 콜렉팅이 되지 않아 계속해서 메모리를 점유함글로벌변수를 참조하고 있는 객체 또한 가비지 컬렉팅의 대상이 되지 않음
동시 참조
- 하나의 동일한 객체가 다양한 객체에서 참조될 때, 하나의 참조가 잘못된다면 전체 객체에서 메모리 누수 발생 가능
클로저
- 클로저가 힙의 큰 객체의 클로저를 참조하고 있다면, 클로저가 사용될 때까지 그 객체는 메모리에 남아 있음
타이머 & 이벤트
setTimeout,setInterval,Observer이벤트 리스너 등의 콜백이 적절한 조치없이 무거운 객체의 참조를 가지고 있을 경우 메모리 누수가 발생
👩🏻💻 그래서 메모리 누수를 어떻게 관리하겠다는 건데?
👀 메모리 누수 방지를 위해 기본적으로 알아야할 것 들을 간단히 정리하면,
1️⃣ C 언어 vs Javascript
C언어는 메모리 관리를 위해
malloc()과free()를 이용해서 메모리를 해제해준다자바스크립트는 객체가 생성되었을 때 자동으로 메모리를 할당하고 더이상 사용하지 않을 때 자동으로 해제
- 자동으로 해제한다고 해서 개발자가 메모리 관리를 고민할 필요가 없는 것은 절대 아님
2️⃣ 자바스크립트에서 메모리 할당
값 초기화
- 자바스크립트는 값을 선언할 때 자동으로 메모리를 할당
값 사용
할당된 메모리를 읽고 쓰는 것
변수나 객체 속성의 값을 읽고 쓰거나 함수 호출 시 함수에 인수를 전달해서 수행
할당된 메모리가 더 이상 필요가 없을 때 해제
- 가비지 컬렉션(GC)이라는 자동 메모리 관리 방법을 사용함
3️⃣ 메모리 누수란?
애플리케이션에서 더 이상 사용하지 않는 메모리가 힙에서 계속 남아 있어서 메모리에서 쓸모 없는 블록으로 남게 됨
이러한 블록이 계속해서 생기게 되면 애플리케이션에서는 더 이상 사용할 메모리가 존재하지 않게되고 OS 또한 할당할 메모리가 남지 않아 애플리케이션이 느려지거나 크래쉬가 날 수 있음
4️⃣ 가비지 컬렉터
- 가비지 컬렉션 알고리즘의 핵심개념 “ 참조 “
👀 따라서 메모리 누수 방지하는 방법은!
1️⃣ 전역 변수의 사용을 줄이자
- 전역 변수의 경우에는 root 가 참조하고 있기 때문에, 절대로 가비지 컬렉팅이 되지 않음
2️⃣ 글로벌 스코프 사용을 줄이자
함수의 지역 스코프를 사용해 가비지 컬렉터가 원할 때 메모리를 수집할 수 있게 해야함
특별한 제한으로 인해 전역을 쓴다면 더 이상 사용하지 않을 때
null을 넣어줘야함전역 변수의 경우는 오직 상수, 캐시 또는 재사용할 싱글턴 패턴에만 사용해야함
큰 객체를 전역 변수에 저장하지 말아야하며 꼭 저장해야한다면 더 이상 사용하지 않을 때
null처리를 해줘야함캐시 객체의 경우에는 이 객체가 점점 커지는 것을 방지해야함
3️⃣ 스택 메모리를 잘 활용해야함
스택 접근은 힙 접근 보다 성능적으로도 우월하고, 메모리의 효율성도 높으므로 가능한 스택 변수를 많이 활용해야함
실무에서 스택을 더 효율적으로 사용하려면,
스택 변수로부터 힙 객체를 참조하는 것을 가능한 피해야함
사용하지 않는 변수는 그냥 두면 안됨
객체나 배열 내부의 값을 넘길 때는 전체 객체를 통으로 넘기기보다 이를 분해해서 필요한 것만 넘기기
객체 내부의 값은 대부분 원시값으로!
4️⃣ 힙 메모리를 효율적으로 활용하기
참조를 넘기는 대신 객체를 복사해서 넘기기
- 참조를 넘기는 것은 객체가 크거나 복사하는 비용이 클 때 활용
객체의 변이를 가능한 피하고 전개 연산자를 사용하거나
Object.assign으로 복사하는 것이 좋음하나의 객체에 여러가지 참조를 만드는 것 피하기
- 대신 객체를 복사!
수명이 짧은 변수 활용하기
큰 객체 트리 만드는 것을 피하기
- 만약 불가하다면, 지역변수 내에서 보관
🌷 결론
이 하나의 답을 얻기 위해 아침부터 정말 많은 것들을 후벼 팠구나… 사실 더 명확한 답을 내기까지는 더 봐야할 것들이 많겠지만 그러기엔 너무 방대해질 것 같아서.. 흑흑 우선 메모리와 포인터의 관점에서 메모리 누수를 방지할 수 있는 방법에 대해서 어느정도의 답을 낼 수는 있을 것 같다.
어쨌든 C언어에서 포인터에서의 메모리 누수가 잦은 이유를 들여다보면, 포인터는 동적으로 할당된 데이터의 주솟값을 가리키고 있는데 만약 이 부분에 대한 메모리가 해제되어 사라진다면, 존재하지 않는 메모리 주소를 가리키고 있는 또 하나의 포인터 변수가 남아있기 때문이 아닐까? 그리고 이것은 또 댕글링 포인터의 문제까지로도 이어지기도 한다.
이러한 관점에서 메모리 누수를 방지하기 위한 방법을 찾는다면 더이상 사용하지 않는 것들에 대해서 클린없을 해주고 “참조” 고리도 끊어주는 것이 하나의 방법이라는 생각이 든다. 그래서 포인터에서 여기까지 오게 된 것인가..! 하지만 가비지 컬렉터의 알고리즘을 보면 결론적으로는 이 참조의 고리가 끊기지 않으면 결국은 가비지 컬렉팅이 되지 않아 낭비되는 메모리가 발생하므로, 앞으로 코드를 작성해나갈 때 이런 부분들에 대해서 잘 고려해야겠다는 생각이 든다.
![[월간 이트루] 3월의 회고.](https://cdn.hashnode.com/res/hashnode/image/upload/v1711895194426/f6a22b40-ffd3-4b11-a25b-e1b040b11853.png)
![[월간 이트루] 2월의 회고.](https://cdn.hashnode.com/res/hashnode/image/upload/v1709129901980/5a2af3c5-8f6e-4f07-9c4f-1b43bef27ae9.png)


![[월간 이트루] 1월의 회고.](https://cdn.hashnode.com/res/hashnode/image/upload/v1706708547437/51af577d-21f9-4256-8939-95c577a58abc.jpeg)