자바스크립트/모던 자바스크립트

모던 자바스크립트, 위크 맵(WeakMap) 과 위크 셋(WeakSet)

Jake Seo 2023. 3. 24. 00:58

위크맵 (WeakMap)

  • 키를 메모리에 유지하지 않고 키와 관련된 값을 저장할 수 있다.
  • 만일 키를 참조하는 것이 사라지면, 가비지 컬렉트 때 해당 키가 사라진다.

위크맵은 Iterable 이 아니다.

  • Map 은 이터러블이었지만, WeakMap 은 아니다.
  • 이러한 특성 때문에 키를 알아야만 값을 가져올 수 있다.
  • Iterator 를 제공하면 제공된 Iterator 에 의해 키가 계속 참조되고, 영원히 가비지 컬렉트 될 수 없기 때문이다.

위크맵이 제공하는 메서드

  • has(): 키의 존재 여부 반환
  • get(): 키에 대한 값 반환, 없다면 undefined
  • delete(): 키 삭제 성공하면 true 실패하면 false

WeakMap 은 약한 참조를 이용하는 만큼, 키를 모르면 참조할 수 없기 때문에 키에 접근하는 size, forEach,keys, values 등이 없다.

WeakMap 은 이름이 비슷하고 인터페이스가 비슷하지만 Map 의 서브 클래스가 아니다.

위크맵의 용례

  • 어떤 경우에 위크맵을 쓰면 좋을까?

private 한 정보를 보관할 때

  • 아래 예제에서 private 한 정보를 보관하는 Map 으로 사용되었다.
  • ES2021 에서는 Private class feature 가 나와서 사실 이것보다 훨씬 간단한 문법으로 이를 수행할 수 있다.
const Example = (() => {
  const privateMap = new WeakMap();

  return class Example {
    constructor() {
      privateMap.set(this, 0);
    }

    incrementCounter() {
      const result = privateMap.get(this) + 1;
      privateMap.set(this, result);
      return result;
    }

    showCounter() {
      console.log(`Counter is ${privateMap.get(this)}`);
    }
  };
})();

const e1 = new Example(); // Example {}
e1.incrementCounter();
console.log(e1); // Example {}

const e2 = new Example();
e2.incrementCounter();
e2.incrementCounter();
e2.incrementCounter();

e1.showCounter(); // Counter is 1
e2.showCounter(); // Counter is 3

사실 Map 을 사용해도 비공개라는 속성 자체는 유지된다. 그러나, 계속 맵에 데이터가 쌓이고 쌓일수록 가비지 컬렉트 되지 않고 맵의 덩치가 커질 것이다.

제어할 수 없는 객체에 대한 정보 저장

  • 이를테면, 브라우저의 DOM 객체를 키로 걸고 그에 대한 값을 저장해둘 수 있다.
  • 이 DOM 객체는 사용자의 인터렉션을 통해 지워질 수 있는데, 지워지면 WeakMap 키의 레퍼런스도 날아가기 때문에 추후에 가비지컬렉트된다.
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Storing Data for DOM Elements</title>
  </head>
  <style>
  .person {
      cursor: pointer;
  }
  </style>
  <body>
    <label>
    <div id="status"></div>
    <div id="people"></div>
    <div id="person"></div>
    <script src="storing-data-for-dom.js"></script>
  </body>
</html>
(async () => {
  const statusDisplay = document.getElementById("status");
  const personDisplay = document.getElementById("person");
  try {
    // The WeakMap that will hold the information related to our DOM elements
    const personMap = new WeakMap();
    await init();

    async function init() {
      const peopleList = document.getElementById("people");
      const people = await getPeople();
      // In this loop, we store the person that relates to each div in the
      // WeakMap using the div as the key
      for (const person of people) {
        const personDiv = createPersonElement(person);
        personMap.set(personDiv, person);
        peopleList.appendChild(personDiv);
      }
    }

    async function getPeople() {
      // This is a stand-in for an operation that would fetch the person
      // data from the server or similar
      return [
        { name: "Joe Bloggs", position: "Front-End Developer" },
        { name: "Abha Patel", position: "Senior Software Architect" },
        { name: "Guo Wong", position: "Database Analyst" },
      ];
    }

    function createPersonElement(person) {
      const div = document.createElement("div");
      div.className = "person";
      div.innerHTML =
        '<a href="#show" class="remove">X</a> <span class="name"></span>';
      div.querySelector("span").textContent = person.name;
      div.querySelector("a").addEventListener("click", removePerson);
      div.addEventListener("click", showPerson);
      return div;
    }

    function stopEvent(e) {
      e.preventDefault();
      e.stopPropagation();
    }

    function showPerson(e) {
      stopEvent(e);
      // Here, we get the person to show by looking up the clicked element
      // in the WeakMap
      const person = personMap.get(this);
      if (person) {
        const { name, position } = person;
        personDisplay.textContent = `${name}'s position is: ${position}`;
      }
    }

    function removePerson(e) {
      stopEvent(e);
      this.closest("div").remove();
    }
  } catch (error) {
    statusDisplay.innerHTML = `Error: ${error.message}`;
  }
})();

키를 참조하는 값이 있다면 메모리 해제가 가능할까?

  • 구현 스펙에 키로 사용되는 객체가 값에서 시작되는 경로로만 참조가 가능하다면, 가비지컬렉트 된다고 나와있다.
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Values Referring Back to the Key</title>
  </head>
  <body>
    <label>
      Objects to create:
      <input type="text" id="objects" value="100000" />
    </label>
    <input type="button" id="btn-create" value="Create" />
    <input type="button" id="btn-release" value="Release" />
    <script src="value-referring-to-key.js"></script>
  </body>
</html>
const log = (msg) => {
  const p = document.createElement("pre");
  p.appendChild(document.createTextNode(msg));
  document.body.appendChild(p);
};

const AAAAExample = (() => {
  const privateMap = new WeakMap();

  return class AAAAExample {
    constructor(secret, limit) {
      privateMap.set(this, { counter: 0, owner: this });
    }

    get counter() {
      return privateMap.get(this).counter;
    }

    incrementCounter() {
      return ++privateMap.get(this).counter;
    }
  };
})();

const e = new AAAAExample();

document.getElementById("btn-create").addEventListener("click", function (e) {
  const counter = +document.getElementById("objects").value || 100000;
  log(`Generating ${count} objects...`);

  for (let n = count; n > 0; n--) {
    a.push(new AAAAExample());
  }

  log(`Done, ${a.length} objects in the array`);
});

document.getElementById("btn-release").addEventListener("click", function (e) {
  a.length = 0;
  log("All objects released");
});
  • owner 에서 this 를 이용해 자기 자신을 참조하고 있다.
  • 동작은 아주 간단하다.
    • Create 버튼을 누르면 AAAAExample 클래스를 10만개 만든다.
      • 만들어진 10만개의 AAAAExample 을 배열 a 에서 참조하도록 만든다.
    • Release 버튼을 누르면 배열 alength0 으로 만들어서 참조를 끊는다.

  • Create 버튼을 눌렀을 때의 스냅샷이다.

  • Release 버튼을 눌렀을 때의 스냅샷이다.

  • Collect garbage 버튼을 누르면 더 빠르게 가비지 컬렉트를 시킬 수 있다.

위크셋, 위크세트 (WeakSet)

  • WeakMapSet 버전이다.
  • WeakMap 과 대부분의 특성이 동일하다.
  • 다만, 엔트리를 저장하는 것이 아니라 값을 저장한다.

용례

  • 이전에 이 객체를 보았는지 확인할 때 유용하다.

일회성 토큰 예제

  • 단 한번만 사용되어야 하는 일회성 토큰을 저장하는 경우, 해당 토큰이 이미 사용되었는지 판단해주는데에 유용하다.
  • 만일 해당 토큰의 유효기간이 끝났다면 참조가 해제될테고 가비지컬렉트 될 것이다.
    • 이 경우 다시 해당 토큰을 사용할 수 있게 되는 원리다.
const SingleUseObject = (() => {
  const used = new WeakSet();

  return class SingleUseObject {
    constructor(name) {
      this.name = name;
    }

    use() {
      if (used.has(this)) {
        throw new Error(`${this.name} has already been used`);
      }

      console.log(`Using ${this.name}`);
      used.add(this);
    }
  };
})();

const obj1 = new SingleUseObject("hello");
const obj2 = new SingleUseObject("what");

obj1.use();
obj1.use();

출처 확인 예제

  • 이 객체의 출처가 정확히 어느곳인지 확인하는데 사용할 수 있다.
  • 물론 이 경우에도 해당 값에 대한 참조가 끊기면 자동으로 가비지 컬렉트 될 것이다.
const Thingy = (() => {
  const known = new WeakSet();
  let nextId = 1;

  return class Thingy {
    constructor(name) {
      this.name = name;
      this.id = nextId++;
      Object.freeze(this);
      known.add(this);
    }

    action() {
      if (!known.has(this)) {
        throw new Error("정상적인 방법으로 만들어진 Thingy 가 아닙니다.");
      }

      console.log(`Action on Thingy #${this.id} (${this.name})`);
    }
  };
})();

const t1 = new Thingy("t1");
t1.action();
const t2 = new Thingy("t2");
t2.action();

const faket2 = Object.create(Thingy.prototype);
faket2.name = "faket2";
faket2.id = 2;
Object.freeze(faket2);
faket2.action(); // 오직 생성자로 만든 Thingy 만 사용 가능.
반응형