클로저에서 자바스크립트의 메모리 관리까지 토끼굴 파기

dev_dam·2024년 3월 15일
20

토끼굴

목록 보기
1/3
post-thumbnail

이 글은 이전 블로그 클로저의 다양한 활용과 메모이제이션을 공부하다가 메모리 관리까지 토끼굴 판 과정에 대해서 기록한 내용입니다.
최대한 간략하게 적으려고 노력했지만 그럼에도 불구하고 내용이 꽤 길 수 있습니다.

빠른 요약

  • Map, Set, WeakMap, WeakSet은 자바스크립트의 메모리 관리를 돕는 데이터 구조입니다.
  • 가비지 컬렉션(V8 기준)은 참조 카운팅 및 Mark-and-sweep 알고리즘과 Tri-color 알고리즘을 사용하여 메모리를 관리합니다.
  • WeakRefFinalizationRegistry를 통해 장시간 실행되는 프로그램의 메모리 사용 최적화를 할 수 있지만, 브라우저 엔진마다 가비지 컬렉션의 동작 방식이 다르며 어느 시점에 호출될지 반드시 호출될지 보장할 수 없기 때문에 WeakRef와 FinalizationRegistry의 사용은 매우 신중하게 고려되어 사용해야 합니다.
  • 자바스크립트의 Symbol 타입의 고유한 특징은 메타 프로그래밍을 가능하게 해주며 이런 기법을 활용하여 자바스크립트의 기본 동작을 확장하거나 변경할 수 있습니다.

들어가며,

이전 블로그 클로저의 다양한 활용과 메모이제이션 글에서 사실 궁금한 점이 있었습니다.
클로저의 가장 큰 문제점이 메모리 누수인데, 과연 메모리 누수가 제대로 해결되는 것인가?에 대한 의문이었습니다.
제가 의문을 갖게 된 예제 코드를 살펴보겠습니다 (참고 : You Don’t Know JS Yet)

function manageStudentGrades(studentRecords) {
  var grades = studentRecords.map(getGrade);
  // 클로저로 인한 불필요한 메모리 차지를 방지하기 위해 studentRecords를 해제
	studentRecords = null
  return addGrade;
  
  function addGrade(newGrade) {
    grades.push(newGrade);
    sortAndTrimGradesList();
    return grades;
  }
}

위 예제 코드를 보시면 studentRecords = null 을 할당해 불필요한 메모리는 해제했지만 studentRecords.map(getGrade)의 값은 var grades에 저장되고 클로저로 인해 이후 계속 메모리를 차지하여 사용할 수 있습니다.
위 코드를 살펴보다가 메모리 효율이 얼마나 좋을지에 대해서 의문을 갖게 되었고 문득 저는 자바스크립트가 어떤 식으로 메모리를 관리하는지 제대로 알지 못한다는 것을 깨달았습니다.
자바스크립트는 가비지 컬렉션에 의해서 자동으로 메모리를 수거해간다고 익히 알려져 있습니다. 그렇다고 해서 메모리에 대해서 공부를 안 하고 넘어간다고 한다면 저는 이후에 분명히 메모리 누수에 막히는 일이 발생할 것이고, 그 원인을 몰라 헤매게 될 것 같았습니다.
미래에 있을 불행한(?) 일들이 발생했을 때 제대로 원인 파악을 하기 위해서 자바스크립트의 메모리 관리에 대해서 토끼굴을 파야겠다고 생각했습니다.

자바스크립트의 메모리 관리를 돕는 데이터 구조

자바스크립트 메모리 관리를 공부하기 위해, 그전에 자바스크립트에서 메모리 관리를 돕는 데이터 구조가 있습니다.
바로 Map, Set, WeakMap, WeakSet입니다.

객체(object)와 배열(array)의 한계

자바스크립트에서는 객체와 배열을 사용해서 데이터를 관리했지만 특정 상황에서 데이터 관리의 복잡성과 한계가 나타났습니다.
예를 들어 배열에서 중복 값을 관리할 때 배열 내에 이미 같은 값이 존재하는지 매번 확인해야 했는데, 대규모 데이터를 다룰 때 이런 검사는 비효율적이고 성능 저하를 일으킬 수 있었습니다.
객체의 경우 key는 문자열 또는 Symbol이어야 했는데 이는 숫자나 객체 등 다른 타입을 키로 사용하고자 할 때 제한적이었습니다. 객체의 key가 문자열로 암묵적 타입 변환되어 예상치 못한 방식으로 데이터를 저장하거나 접근하는 문제가 발생할 수 있었습니다.
이러한 한계점을 극복하고 데이터를 보다 효과적으로 관리할 수 있는 방법을 제공하기 위해서 MapSet이 등장했습니다.

Map, Set 에 간략히 살펴보기

자세한 내용은 MDN , 맵과 셋 을 확인해주세요

Map

Mapkeyvalue 쌍이 있는 데이터를 저장한다는 점에서 객체와 유사합니다. 하지만 key에 다양한 자료형을 사용할 수 있다는 점이 객체(object)와 가장 큰 차이점입니다.
Map의 중요 특징은 열거 가능한 메서드를 제공해 준다는 점과 Key를 비교하는 방식입니다.
Map의 key는 중복이 불가능하기 때문에 NaNkey로 등록했다면 이후 NaN을 새로운 key로 등록해도 동일하다고 판단되어 value만 업데이트됩니다

Map을 객체로 변경하기, 객체를 Map으로 변경하기

// 객체를 Map으로 만들수 있습니다.
const obj = { apple: "🍎", banana: "🍌", orange: "🍊" };
const mapObj = new Map(Object.entries(obj));

for (const o of mapObj) {
  console.log("객체를 Map으로 만들어서 순회가 가능합니다. ", o);
}

// Map으로 만든 객체를 일반 객체로 변경할 수 있습니다.
const newObj = Object.fromEntries(mapObj);
console.log("newObj", newObj);

Set

Set은 배열과 비슷하게 여러 값을 저장할 수 있지만 값이 중복될 수 없다는 특징이 있습니다.
어떤 값이 Set에 추가된다면, 그 값은 Set 내에서 유일해야 하며 이미 존재하는 값과 중복될 수 없기 때문에 중복 없이 값을 관리해야 할 경우 유용합니다.

Set의 다양한 활용

Set은 데이터의 중복을 방지해야 할 때 유용합니다. 예를 들어 사용자의 입력값에서 중복을 제거해야 하는 경우나, 어떤 컬렉션에서 고유한 값들만을 추출해야 할 때 Set을 사용할 수 있습니다.
만약 방문자 방명록을 만든다고 할 때 한 방문자가 여러 번 방문해도 방문자를 중복해서 기록하고 싶지 않을 때 사용할 수 있습니다.

const users = [
  { name: "이다미" },
  { name: "차은우" },
  { name: "카리나" },
  { name: "차은우" },
  { name: "카리나" },
];

const findUser = (array) => {
  let set = new Set();
  for (const user of array) {
    set.add(user.name);
  }
  console.log(set);
};

findUser(users);

이렇게 Set은 값의 유일함을 확인하는데 최적화되어 있으며, Set을 사용 안 하고 중복 값을 찾아야 할 때는 더 복잡한 로직을 구현해야 합니다.

WeakMap, WeakSet의 등장 배경

MapSet 은 강력하고 유용한 데이터 구조이지만, 특정 상황에서 메모리 누수(memory leaks)를 일으킬 수 있는 가능성이 있습니다.
예를 들어 Map의 키로 사용된 객체에 대한 다른 참조가 없어져도 해당 객체는 Map 내부에서 여전히 살아 있으므로 가비지 컬렉션의 수거 대상이 되지 않았습니다. 대규모 애플리케이션에서 문제가 될 수 있고 메모리 사용량을 비효율적으로 만들 수 있다는 문제가 있습니다.
이를 해결하기 위해 WeakMapWeakSet 이 도입되었으며, MapSet 의 "약한" 버전입니다.
이들은 주로 메모리 관리 측면에서의 문제를 해결하기 위해 고안되었으며, WeakMapWeakSet의 "약함(weakness)"은 객체에 대한 참조가 가비지 컬렉션 대상 결정에 영향을 미치지 않는다는 것을 의미합니다.

let dami = { name: "이다미" };

const map = new Map();
map.set(dami, "developer");
console.log(map); // Map(1) { { name: '이다미' } => 'developer' }

dami = null; // 변수의 참조값을 null로 변경함

// dami를 나타내는 객체는 map에 의해 여전히 참조되고 있다.
// map.keys()를 이용하면 해당 객체에 접근 가능하다.
for (const obj of map.keys()) {
  console.log(obj); // key값은 여전히 객체로 살아있다. { name: '이다미' }
}
console.log(map.size); // 1

즉, 메모리에서 해지하기 위해서는 let dami 변수와 Map으로 저장한 값 모두 해지해 줘야 한다는 번거로움이 있습니다.
이러한 메모리를 효율적으로 관리하기 위한 데이터 구조로 WeakMapWeakSet이 도입되었습니다.

WeakMap, WeakSet 간략히 살펴보기

WeakMap, WeakSet에 대한 자세한 내용은 MDN를 확인해 주세요

WeakMap

WeakMap은 자바스크립트의 내장 객체로 key-value 쌍을 저장하는 컬렉션이며 Map 객체와는 달리 WeapMapkey는 반드시 객체여야 한다는 점입니다.
즉 원시 값은 WeakMap의 key로 사용할 수 없으며 key객체만 허용하고 이 객체에 대한 참조가 약한 참조(weak reference)라는 것입니다.
이러한 약한 참조의 특성으로 인해, WeakMap 내부의 key-value 쌍은 가비지 컬렉션의 대상이 될 수 있습니다.

let dami = { name: "이다미" };

const weakMap = new WeakMap([[dami, "value"]]);

console.log(weakMap); // WeakMap {{…} => 'value'}
dami = null; // **dami 변수의 객체 참조를 끊습니다.**

WeakMapkey로 사용된 객체를 참조하는 것이 아무것도 없다면, 해당 객체는 WeakMap메모리에서 자동으로 삭제됩니다.
즉, WeakMap에서 따로 참조를 끊지 않아도 알아서 참조가 끊어집니다.
또 다른 차이점으로는 WeakMap, WeakSet은 순회 가능한 반복 작업인 keys(), values(), entries() 메서드를 지원하지 않습니다.

WeakMap의 활용

WeakMap은 메모이제이션에서 유용하게 사용할 수 있습니다.

const memo = new WeakMap();

function memoization(key, value) {
  if (memo.has(key)) memo.get(key);
  return memo.set(key, value);
}

let dami = { name: "이다미" };
let eunwoo = { name: "차은우" };

memoization(dami, "hello");

// 동일한 객체를 key값으로 전달하면 
// 연산을 수행하지 않고 key값으로 저장된 객체를 가져옵니다.
memoization(dami, "key만 업데이트 됩니다.");

// 참조 끊기
dami = null;

dami = null; 로 참조가 끊기게 되면 WeakMap에 저장된 dami도 가비지 컬렉션이 실행되면 자동으로 삭제됩니다.

Map 사용했을 때와 WeakMap 사용했을 때 메모리 변화

  • Map을 사용했을 때 크롬 개발자 도구의 memory
  • 동일 코드를 WeakMap을 사용했을 때 크롬 개발자 도구의 memory
    • 파란색 그래프의 막대기가 회색으로 변하는 것을 알 수 있습니다.
    • 관련 내용은 WeakMap이 알고싶다를 참고해주세요

WeakSet

WeakSetset과 유사한데 객체만 저장 할 수 있다는 점이 다르며, WeakMap과 마찬가지로 객체들에 대한 참조가 약한 참조(weak reference)라는 점입니다.
이로 인해 WeakSet의 내부의 객체들은 가비지 컬렉션의 대상이 될 수 있으며 이들 객체에 대한 다른 참조가 없어지면 자동으로 메모리에서 해제됩니다.

WeakSet 활용

WeakSet은 복잡한 데이터를 저장하지 않고 간단한 답변을 얻는 용도로 사용하기 때문에 사용자가 사이트를 방문 했는지의 여부를 추적하는 용도로 사용할 수 있습니다.

const weakset = new WeakSet();

let dami = { name: "이다미" };
let eunwoo = { name: "차은우" };
let karina = { name: "카리나" };

weakset.add(dami).add(eunwoo).add(karina);
console.log(weakset);

// **dami가 사이트를 방문했는지 확인하기**
console.log(weakset.has(dami))

dami = null

이렇게 간단한 여부를 확인할 때 WeakSet을 사용할 수 있으며, 객체의 참조를 끊는다면 가비지 컬렉션에 의해 자동으로 메모리에서 해지됩니다.

WeakMap, WeakSet 과 가비지 컬렉션

아래 예제 코드를 직접 따라 치신분들이 있다면 저와 같은 의문을 만날 수 있습니다.

let dami = { name: "이다미" };

const weakMap = new WeakMap([[dami, "value"]]);

console.log(weakMap); // WeakMap {{…} => 'value'}
dami = null; // **dami 변수의 객체 참조를 끊습니다.**

console.log(weakMap);
setTimeout(() => {
  console.log(weakMap)
},3000)

분명 dami = null로 참조를 끊으면 WeakMap은 자동으로 가비지 컬렉션에 의해 수거돼서 참조가 끊긴다고 설명했습니다. 하지만 console.log(weakMap); 를 찍으면 여전히 참조되며 setTimeout()으로 호출해도 여전히 참조됨을 알 수 있습니다.
저는 여기서 왜 참조되는 걸까? 라는 의문이 생겼고 그 이유는 가비지 컬렉션의 동작 방식 때문이라는 점을 알 수 있었습니다.
앞서, WeakMapWeakSetMap, Set과 달리 순회 가능한 메서드를 제공하지 않는다고 했습니다.
그 이유는 가비지 컬렉션의 동작 시점을 정확히 알 수 없기 때문입니다.

가비지 컬렉션이 일어나는 시점은 자바스크립트 엔진이 결정합니다.
객체는 모든 참조를 잃었을 때 그 즉시 메모리에서 삭제될 수도 있고, 다른 삭제 작업이 있을 때까지 대기하다가 함께 삭제될 수도 있습니다.
즉, 현재 WeakMap에 요소가 몇 개 있는지 정확히 파악하는 것 자체가 불가능합니다.
가비지 컬렉터가 한 번에 메모리를 청소할 수도 있고, 부분 부분 메모리를 청소할 수도 있음으로 WeakMap의 요소(key, value) 전체를 대상으로 무언가를 하는 메서드는 동작 자체가 불가능합니다.

가비지 컬렉션

가비지 컬렉션에 대한 자세한 내용은 가비지 컬렉션은 MDN 문서자바스크립트 V8 엔진의 가비지 컬렉션 동작 방식 블로그를 확인해 주세요

자바스크립트와 같은 고수준 언어들은 가비지 컬렉션(GC)이라는 자동 메모리 관리 방법을 사용합니다.
가비지 컬렉터의 목적은 메모리 할당을 추적하고 할당된 메모리 블록이 더 이상 필요하지 않게 되었는지를 판단하여 회수하는 것입니다.
V8 엔진의 메모리 구조의 힙 영역은 New space, Old space, Large Object space, 코드 space, 셀 space, 속성 space, 맵 space로 이루어져 있습니다.
가비지 컬렉션이 일어나는 부분은 New spaceOld space이며 메이저 GC인 Old space에 있는 객체들이 어떻게 가비지 컬렉션이 되는지에 대해서만 살펴보겠습니다.

Mark-and-sweep 알고리즘과 Tri-color 알고리즘

기본적인 알고리즘 로직은 “더 이상 참조되지 않아 필요없는 객체”“도달할 수 없는 객체”로 정의합니다.

구체적으로는 3단계에 걸쳐 동작합니다.

  • 마킹 : 어떤 객체들이 가비지 컬렉션 대상인지 알아내기 위한 단계로, “roots”라는 객체의 집합으로 시작해서 모든 도달할 수 있는 객체들을 찾고, 도달할 수 없는 모든 객체들을 수집하여 Tri-color(white, gray, black)로 마킹합니다.
  • 스위핑 : 도달할 수 없는 객체들의 메모리 주소를 free-list라고 부르는 자료구조에 추가합니다. 이 주소들의 메모리 공간은 사용 가능하여 새로운 객체를 저장할 수 있습니다.
  • 압축 : 메모리 단편화가 심한 페이지들을 재배치하여 추가적인 메모리를 확보합니다.

현재 모든 최신 엔진은 Mark-and-sweep 가비지 수집을 제공하며 몇 년간의 모든 개선들은 이 알고리즘의 구현을 통한 개선입니다.
이러한 알고리즘을 통해서 객체들은 가비지 수집기가 도달할 수 없는 것으로 판명되었을 때 할당되었던 메모리를 회수하게 됩니다. 하지만, 가비지 수집을 수동으로 조작할 수 없기 때문에 객체의 메모리를 반환하기 위해 해당 객체는 명시적으로 도달할 수 없게 되어야 합니다.
또한 가비지 컬렉션을 계산하는 것 자체가 오래 걸릴 수 있고 개발자는 언제 가비지 컬렉션이 일어날지 추적하기 힘듭니다. 객체를 코드 상에서는 null로 할당해서 해제한 것 같지만, 실제로 메모리에서 없어지는 것은 가비지 컬렉션이 돌아간 이후이기 때문에 정확한 파악이 어렵습니다.
이렇게 가비지 수집이 동작하는 시간은 우리가 알 수 없지만 가비지 컬렉션의 동작을 이해함으로써 효율적인 메모리 사용과 좋은 프로그래밍 패턴을 생각하는 데 도움이 됩니다.

조금 더 토끼굴 파기

여기서 조금 더 토끼굴을 파보기로 했습니다.
WeakRefFinalizers 에 대해서 살펴보겠습니다

자세한 내용은 여기MDN을 확인해 주세요

WeakRef

WeakRef 자바스크립트에서 가비지 컬렉션(GC) 알고리즘의 대상이 될 수 있는 객체에 대한 "약한 참조(weak reference)"를 생성할 수 있게 해줍니다.
약한 참조란, 객체에 대한 참조를 유지하면서도 그 객체가 가비지 컬렉션에 의해 수집(collected)될 수 있게 하는 참조를 말합니다. 이는 WeakRef를 사용하여 참조된 객체가 여전히 메모리에 남아 있는지에 여부에 의존하지 않는 프로그래밍 패턴을 가능하게 합니다.
일반적으로 값이 객체인 모든 변수들은 해당 객체에 대한 강한 참조였고, 이런 객체들은 가비지 컬렉션의 대상이 되지 않았기 때문에 종종 필요 없게 된 큰 용량의 정보를 메모리에서 계속 들고 있는 메모리 누수가 일어났습니다.
그래서 약한 참조를 갖는 WeakMap, WeakSet이 등장했고 WeakMap, WeakSet은 객체를 키로 하는 key-value 쌍과 객체 집합에 대한 약한 참조를 제공하지만 WeakRef는 단일 객체에 대한 약한 참조를 제공합니다.
WeakRef를 사용하면 객체가 가비지 컬렉션에 의해 수집될 수 있는 상태를 유지하면서도 그 객체에 조건부로 접근할 수 있습니다.

let dami = { name: "이다미" };
let weakRef = new WeakRef(dami);

// 나중에 dami에 다시 접근
let derefObj = weakRef.deref();
if (derefObj) {
    console.log(derefObj.name); // 객체가 GC에 의해 회수되지 않았다면 "이다미" 출력
} else {
    // dami가 가비지 컬렉션에 의해 회수되었다면 실행
    console.log("가비지 컬렉션에 의해 수거되었습니다.")
}

WeakRef를 통해 참조된 객체는 deref 메서드를 사용하여 접근할 수 있습니다.
즉, WeakRef의 deref() 메서드를 통해 해당 객체가 아직 가비지 컬렉션에 의해 회수되지 않았는지 확인할 수 있습니다. 이 메서드를 통해서 객체의 생존 상태에 따라 특정 로직을 실행할 수 있게 됩니다.

WeakRef는 개발자가 객체의 생존 상태에 따라 선택적으로 작업할 수 있지만 WeakMap, WeakSet은 객체와 연관된 추가 데이터를 메모리 누수 없이 저장하는데 적합하다는 차이점이 있습니다.
또한 WeakRef는 객체가 더 이상 필요하지 않을 때 메모리에서 해제될 수 있도록 하는 반면 WeakMap, WeakSetkey 또는 value로 사용된 객체의 참조가 끊길 때 연결된 값을 자동으로 해제합니다.

WeakRef의 단점

WeakRef객체 생존 여부는 가비지 컬렉션 알고리즘에 의해 결정되고 이는 결국 가비지 컬렉션이 언제 실행될지 특정 객체가 언제 메모리에서 제거될지 예측하기 어렵다는 것을 의미합니다.
따라서 WeakRef를 사용하는 코드는 객체가 언제든지 사라질 수 있다는 점을 고려해야 하기 때문에 코드 예측 가능성과 안정성이 저하될 수 있음으로 주의해야 합니다.
또한 WeakRefderef 메서드는 참조된 객체가 여전히 살아 있는 경우에만 해당 객체를 반환하고 가비지 컬렉션에 의해 수집된 경우 undefined를 반환합니다. 이는 매번 객체를 사용하기 전에 이를 확인해야 한다는 의미이며, 이 과정이 프로그램의 복잡성을 증가시킬 수 있습니다.
WeakRef를 과도하게 사용하면 가비지 컬렉션 최적화를 방해할 수 있습니다. 객체에 대한 약한 참조가 많을 경우, 가비지 컬렉터가 이러한 객체들의 생존 상태를 추적하고 관리하는 데 추가적인 자원이 소모될 수 있습니다. 이는 특히 메모리가 제한적인 환경에서 더 문제가 될 수 있기 때문에 주의해야 합니다.

FinalizationRegistry

FinalizationRegistry객체가 가비지 컬렉션에 의해 수집(메모리에서 제거) 될 때 실행할 콜백 함수를 등록할 수 있는 메커니즘을 제공합니다.
이 기능은 메모리 누수를 방지하거나, 필요한 청소 작업을 수행하기 위해 사용될 수 있습니다. 그러나 FinalizationRegistry는 가비지 컬렉션이 특정 객체를 수집하는 정확한 시점을 보장하지 않습니다. 따라서, 이를 사용하여 프로그램의 기본 로직을 구현하는 것은 권장되지 않습니다.

const registry = new FinalizationRegistry((heldValue) => {
  // 객체가 GC에 의해 회수되었을 때 실행될 로직
  console.log(`${heldValue} 가비지 컬렉션에 의해 수거되었습니다.`);
});

let dami = { name: "이다미" };
// dami를 등록하면서, dami가 가비지 컬렉션되었을 때 콘솔에 출력할 메시지를 전달
registry.register(dami, "developer");

dami = null; // dami에 대한 참조를 제거하여 가비지 컬렉션 가능하게 함

FinalizationRegistry의 콜백 실행 타이밍의 정확한 시점은 알 수 없는데 그 이유는 가비지 컬렉션의 구현은 엔진마다 조금씩 다르기 때문에 어느 시점에 호출될지, 반드시 호출될지 보장할 수 없기 때문에 신중하게 고려되어서 사용되어야 합니다.
WeakRef와 FinalizationRegistry은 순전히 장시간 실행되는 프로그램의 메모리 사용 최적화를 위해 존재한다는 것을 이해해야 합니다.

심벌 타입과 메모리는 무슨 관계가 있을까

심벌(Symbol)에 대한 자세한 내용은 MDN을 확인해주세요

심벌은 고유하며 변경할 수 없는 원시 타입이며, 상대적으로 작은 메모리 공간을 차지합니다. 각 심벌의 메모리 사용량은 자바스크립트 엔진의 구현에 따라 다를 수 있지만, 고유한 식별자로서의 역할을 수행하기 위한 충분히 작은 정보만을 포함합니다. 따라서 심벌을 생성하고 사용하는 것 자체는 메모리 사용에 있어서 큰 부담이 되지 않습니다.
심벌은 주로 객체의 속성 키로 사용되는데, 키로 사용될 경우 해당 객체와 함께 심벌도 메모리에서 관리됩니다. 객체가 더 이상 사용되지 않아 가비지 컬렉션의 대상이 되면, 객체와 함께 해당 객체의 심벌 키도 메모리에서 제거될 수 있습니다.

let globalSymbol = Symbol.for('globalSymbol');

전역 심벌 레지스트리에 등록된 심벌은 Symbol.for() 메서드를 사용하여 생성되며, 이 메서드에 동일한 문자열을 전달하여 생성된 심벌은 언제나 동일합니다. 이러한 심벌은 애플리케이션이 실행되는 동안 메모리에 남아 있어, 다른 코드 부분에서도 같은 심벌을 재사용할 수 있게 합니다.
심벌은 메모리 공간을 효율적으로 사용하며, 심벌을 키로 사용하는 객체의 속성에 접근하는 것은 속도 면에서도 매우 빠릅니다.
이러한 심벌의 고유성과 불변성은 메모리 내에서 안정적인 사용을 보장하며, 가비지 컬렉션에 의해 객체와 함께 적절하게 관리됩니다. 전역 심벌 레지스트리를 통해 생성되는 심벌은 전역적으로 재사용될 수 있어 메모리 사용을 최적화하는 데 기여합니다.
하지만 심벌이 직접적으로 메모리 사용량을 줄이거나 가비지 컬렉션 효율을 높이는 것은 아닙니다. 그러나 심벌을 사용하면 코드의 안정성과 캡슐화를 높일 수 있으며, 이러한 코드 설계 방식은 간접적으로 메모리 관리를 개선할 수 있는 기회를 제공합니다. 심벌의 사용은 메모리에 큰 부담을 주지 않으면서 프로그램의 안전성과 유지 보수성을 향상시킬 수 있는 방법 중 하나입니다.

심벌과 메타프로그래밍

메타프로그래밍이란, 프로그램이 자신이나 다른 프로그램의 구조를 읽고 변형하며 동적으로 생성할 수 있게 하는 프로그래밍 기법을 말합니다.
자바스크립트에서의 메타 프로그래밍은 주로 객체의 기본적인 동작을 수정하거나 확장하는 형태로 나타나는데 심벌을 통해서도 구현될 수 있습니다.
예를 들어 객체의 기본 변환 동작을 커스터마이징 할 수 있습니다.

let user = {
  name: "이다미",
  age: 1,
  [Symbol.toPrimitive](type) {
    return type == "string" ? `이름: ${this.name}` : this.age;
  }
};

console.log(user); // 객체 자체 출력
console.log(Number(user)); // 1 (type이 "number")
console.log(String(user)); // 이름: 이다미 (type이 "string")

Symbol.toPrimitive 는 객체가 원시 값으로 변환될 때 사용되는 메서드를 정의합니다.
객체를 숫자나 문자열로 변환해야 할 때 커스터마이징 할 수 있습니다.
이렇게 심벌은 메타프로그래밍 기법을 활용하여 자바스크립트의 기본 동작을 확장하거나 변경할 수 있는 메커니즘을 제공합니다.

마무리

이렇게 제가 클로저로 시작해서 메타 프로그래밍까지 토끼굴 파게 된 과정을 정리했습니다.
이 글은, 추후에도 제가 토끼굴을 파야 할 때의 기준점을 삼기 위해 기록했습니다.
사실 메모리 관리에 대해서 더 심도 있게 들어가면 결국 CS까지 공부해야 합니다.
토끼굴을 팔 때의 유의점은 어디까지의 범위를 공부할 것인가를 정하는 것 같습니다. 저는 저만의 기준점을 세워서 여기까지 토끼굴을 파기로 결정했는데 그 이유는 이후에 공부할 내용도 너무나 많고 다양하기 때문입니다.
앞으로도 토끼굴을 팔 때 목표했던 주제에서 너무 벗어나지 않도록 유의하면서 토끼굴을 팔 예정입니다.

profile
병아리에서 닭이 될 때까지

1개의 댓글

comment-user-thumbnail
2024년 3월 15일

이렇게까지 자세하게 파보시다니....! 잘 읽고갑니다!

답글 달기