Deep JavaScript

WeakRef와 FinalizationRegistry (메모리 관리)

GC에 의해 수거되는 리소스에 대해 개발자가 처리해야할 때 알아두면 좋은 코드이다. 바로 WeakRef와 FinalizationRegistry인데 관련된 코드는 다음과 같다.

// 무거운 데이터 객체
let bigData = { payload: new Array(1000000) };

// 약한 참조 생성
// WeakRef: 객체를 참조하되, 가비지 컬렉션의 대상이 되는 것을 막지 않는 '약한 참조'를 만듭니다. 
// 객체가 메모리에서 해제되면 참조도 자동으로 끊깁니다.
const ref = new WeakRef(bigData);

// 사용처에서 꺼내 쓰기
const derefData = ref.deref();
if (derefData) {
  console.log("아직 메모리에 남아있음:", derefData.payload);
}

// 특정 객체가 메모리에서 해제될 때 정리 작업을 해주는 레지스트리
// FinalizationRegistry: 객체가 가비지 컬렉터에 의해 메모리에서 완전히 지워지는 시점에 특정 콜백 함수를 실행하도록 등록합니다.
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`${heldValue} 객체가 가비지 컬렉션되었습니다.`);
});
registry.register(bigData, "무거운 데이터");

// 참조를 끊으면 가비지 컬렉터가 수거해감
bigData = null;

코드 실행을 일정 시간 블로킹시키는 함수

Node 환경과 웹브라우저 환경에 따라 다르긴 한데 Node에서는 다음 방법을 추천한다.

function sleep(ms) {
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}

console.log("시작 - 1");
sleep(2000); // 2초 블로킹
console.log("2초 후 - 1");

웹 브라우저에서는 다음 방법을 추천하며 Node에서도 가능함.

function sleepWebBrowser(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

console.log("시작 - 2");
await sleepWebBrowser(2000);
console.log("2초 후 - 2");

Object.groupBy() (데이터 그룹화 공식 지원)

배열의 데이터를 특정 기준에 따라 객체로 묶어주는 기능입니다. 과거에는 reduce를 복잡하게 쓰거나 외부 라이브러리(lodash 등)를 썼어야 했는데, 이제 네이티브 문법으로 지원.

const inventory = [
  { name: "asparagus", type: "vegetables", quantity: 5 },
  { name: "bananas", type: "fruit", quantity: 0 },
  { name: "goat", type: "meat", quantity: 23 },
  { name: "cherries", type: "fruit", quantity: 5 },
];

// 'type' 속성을 기준으로 그룹화
const result = Object.groupBy(inventory, ({ type }) => type);

/*
결과:
{
  vegetables: [ 
    { name: "asparagus", type: "vegetables", ... } 
  ],
  fruit: [ 
    { name: "bananas", ... }, 
    { name: "cherries", ... } 
  ],
  meat: [ 
    { name: "goat", ... } 
  ]
}
*/

Proxy와 Reflect (메타 프로그래밍)

자바스크립트의 기본 동작(객체의 속성 조회, 할당, 열거 등)을 가로채서(Intercept) 사용자 정의 동작을 주입하는 강력한 기능임. Vue 3 같은 모던 프레임워크의 상태 변화 감지(Reactivity) 시스템이 바로 이 Proxy를 기반으로 동작함.

const target = { message: "hello" };

const handler = {
  // 객체의 속성을 읽으려고 할 때(get) 가로챔
  get(target, prop, receiver) {
    console.log(`[로그] ${prop} 속성에 읽기로 접근했습니다.`);
    return Reflect.get(...arguments); // 원래 동작 수행
  },
  // 객체의 속성을 바꿀 때(set) 가로챔
  set(target, prop, value, receiver) {
    console.log(`[로그] ${prop} 속성에 쓰기로 접근했습니다.`);

    if (prop === 'age' && value < 0) {
      throw new Error("나이는 음수가 될 수 없습니다.");
    }
    return Reflect.set(...arguments);
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.message); // [로그] message 속성에 읽기로 접근했습니다. -> "hello"
proxy.message = "안녕하세요." // [로그] message 속성에 쓰기로 접근했습니다. 
proxy.age = 10; // [로그] age 속성에 쓰기로 접근했습니다.
proxy.age = -10; // [로그] age 속성에 쓰기로 접근했습니다. (Error: 나이는 음수가 될 수 없습니다.)

구조적 복제 (Structured Clone)

객체의 깊은 복사(Deep Copy)를 수행하는 내장 API임. 과거에는 JSON.parse(JSON.stringify(obj))라는 편법을 썼지만, 이 방식은 Date, RegExp, Map, Set 같은 특수 객체나 순환 참조가 있으면 깨지는 치명적인 문제가 있음. structuredClone은 이를 완벽하게 복사해 줌.

const original = {
  date: new Date(),
  set: new Set([1, 2, 3]),
  nested: { inner: "value" }
};

// 완벽한 깊은 복사
const clone = structuredClone(original);

console.log(clone.date instanceof Date); // true
console.log(clone.nested === original.nested); // false (참조가 분리됨, 깊은 복사로 객체에 대한 주소값이 다름)

생성기 (Generators)와 이터레이터

함수의 실행을 중간에 멈췄다가(yield), 원하는 시점에 다시 재개(next)할 수 있는 특수 함수. 비동기 스트림을 제어하거나, 메모리를 아끼면서 대용량의 무한한 데이터를 순차적으로 처리할 때 매우 유용하며 필자는 메인 스레드를 얼리지 않고 대량의 데이터를 순차적으로 처리하는 기능에 적용하기도 함.

function* idGenerator() {
  let id = 1;
  while (true) {
    yield id++; // 여기서 실행을 멈추고 값을 반환
  }
}

const gen = idGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3 (필요할 때만 값을 무한히 생성)

명시적 자원 관리 (Explicit Resource Management – using)

최근 ECMAScript 스펙에 추가되고 있는 매우 획기적인 기능. 파일 핸들러, 네트워크 소켓, 데이터베이스 커넥션, 혹은 그래픽 컨텍스트처럼 사용 후 반드시 닫아주어야(close/dispose) 하는 자원을 다룰 때 씀. try…finally로 일일이 닫지 않아도, 블록을 벗어나면 자동으로 자원이 해제됨.

// 자원 정의 (Symbol.dispose 메서드를 구현해야 함)
const disposableResource = {
  [Symbol.dispose]() {
    console.log("자원이 자동으로 해제되었습니다.");
  },
  action() {
    console.log("작업 수행 중...");
  }
};

{
  // using 키워드로 선언
  using res = disposableResource;
  res.action();
} // 이 블록을 나가는 순간 Symbol.dispose()가 자동으로 호출됨!

using은 최근에 도입된 아직은 시험 중으로 실행시 다음 옵션이 필요함. node --js-explicit-resource-management

ArrayBuffer.prototype.resize() & SharedArrayBuffer.prototype.grow()

메모리 버퍼의 크기를 가변적으로 조절할 수 있는 기능. WebGL/WebGPU를 다루거나 큰 바이너리 데이터를 청크(Chunk) 단위로 읽어 들일 때, 이전에는 버퍼 크기가 모자라면 더 큰 버퍼를 새로 만들고 기존 데이터를 하나하나 복사(ArrayBuffer.transfer)해야 했음. 이제는 내부 메모리 주소를 유지한 채로 최대 크기(maxByteLength) 범위 내에서 버퍼를 동적으로 늘리고 줄일 수 있음.

// 초기 1MB, 최대 10MB까지 커질 수 있는 버퍼 선언
const buffer = new ArrayBuffer(1024 * 1024, { maxByteLength: 10 * 1024 * 1024 });

console.log(buffer.byteLength); // 1MB

// 복사 작업 없이 인라인으로 즉시 2MB로 확장
buffer.resize(2 * 1024 * 1024);
console.log(buffer.byteLength); // 2MB

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다