JavaScript 내장 객체 - Set, WeakSet

Take!·2025년 8월 19일

JavaScript

목록 보기
12/12

Set

Set 객체는 중복 없는 값의 저장을 보장합니다. 그것이 Primitive 값이든, reference 값이든 상관없이요!

<script>

const mySet = new Set();

mySet.add(1); // Set(1) { 1 }
mySet.add(5); // Set(2) { 1, 5 }
mySet.add("hello"); // Set(3) { 1, 5, 'hello' }

//	이미 존재하는 값 추가
mySet.add(1); // 변화 없음! Set(3) { 1, 5, 'hello' }

console.log(mySet);

</script>

🚀 Set의 핵심 특징 3가지

  • 유일무이한 값 (Uniqueness)
    - 같은 값을 여러번 add()해도 딱 하나만 저장됨.
  • 순서의 보장 (Order-Preserving)
    - Set에 추가한 요소들은 삽입된 순서 그대로 유지. for...of 문으로 순회할 때도 넣은 순서대로 나오는 것을 보장!
  • 빠른 성능 (Performance)
    - Set은 특정 값이 있는지 확인할 때 has() 메서드를 사용 -> 이 방법은 수십만 개의 데이터가 있는 배열에서 includes()로 값을 찾는 것보다 훨씬 빠름.(O(1) vs O(N)) 내부적으로 해시 테이블과 유사한 방식으로 구현되어 있어 데이터가 많아져도 성능 저하가 적음!

🛠️ Set의 기본 메서드

  • new Set(iterable): 새로운 Set을 만듭니다. 배열을 넣어 초기화할 수도 있어요.

  • add(value): 값을 추가합니다.

  • has(value): 값이 있는지 확인하고 true/false를 반환합니다.

  • delete(value): 특정 값을 삭제합니다.

  • clear(): 모든 값을 삭제합니다.

  • size: 메서드가 아닌 속성으로, Set에 몇 개의 값이 있는지 알려줍니다.

<script>

const memberSet = new Set(["Alex", "Bella", "Chris"]);

console.log(memberSet.size); // 3

console.log(memberSet.has("Alex")); // true
console.log(memberSet.has("David")); // false

memberSet.delete("Chris");
console.log(memberSet.has("Chris")); // false

memberSet.forEach(member => {
  console.log(`Welcome, ${member}!`);
});
// Welcome, Alex!
// Welcome, Bella!

</script>

💡 Set의 활용 -> 중복 제거

<script>

const messyNumbers = [1, 2, 5, 2, 4, 8, 1, 5, 9];

// Set으로 변환했다가 다시 배열로 돌려오기!
const uniqueNumbers = [...new Set(messyNumbers)];

console.log(uniqueNumbers); // [1, 2, 5, 4, 8, 9]

</script>

🧮 Set의 활용 -> 집합 연산

  • 최신 JavaScript에서는 Set을 이용해 합집합, 교집합, 차집합 같은 수학적인 집합 연산을 매우 쉽게 처리할 수 있는 메서드를 제공합니다.

    • union(otherSet): 합집합 (두 Set의 모든 요소를 포함)

    • intersection(otherSet): 교집합 (두 Set에 공통으로 있는 요소만 포함)

    • difference(otherSet): 차집합 (A에만 있고 B에는 없는 요소)

<script>

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

// 교집합: A와 B에 모두 있는 요소는?
const intersection = setA.intersection(setB);
console.log([...intersection]); // [3, 4]

// 차집합: A에는 있지만 B에는 없는 요소는?
const difference = setA.difference(setB);
console.log([...difference]); // [1, 2]

</script>

🧐 Set vs Array

  • Set을 사용해야 할 때

    • 데이터의 고유성이 가장 중요할 때
    • 데이터 목록에 특정 값이 있는지 없는지 자주 확인해야 할 때
  • Array를 사용해야 할 때

    • 중복된 값을 허용해야 할 때
    • map, filter, reduce 등 다양한 배열 전용 메서드가 필요할 때
    • 순서가 중요하고 인덱스([0], [1])로 접근해야 할 때

WeakSet

WeakSet은 오직 객체(Object) 타입의 값만 저장할 수 있는 컬렉션입니다. Set과 가장 큰 차이점은 객체를 참조하는 방식에 있습니다.

  • Set도 Map과 마찬가지로 key 값이 null이 되어도 value가 null이 되지 않는다면 여전히 메모리에 등록되어 메모리 누수가 일어난다. 하지만 WeakSet은 WeakMap과 마찬가지로 null값이 되면 메모리에서 자동 해제된다.(GC)

🆚 Set vs. WeakSet: 결정적 차이점


(이지로 대체한다)

🛠️ WeakSet, 왜 쓰고 어떻게 쓸까?

  • 순회도 안되고 크기도 알 수 없어서 상당히 불편할 것으로 예상되나, 객체에 대한 추가 정보를 임시로 저장하고 싶지만, 그 정보 때문에 객체가 메모리에서 사라지지 못하는 상황을 막고 싶을 때 주로 사용한다.

구체적 사용 예시(가상)

  • (Zustand나 Redux 같은 라이브러리에서는 store.subscribe(callback)를 통해 상태 변경을 감지하는 콜백 함수를 등록합니다. 이 콜백 함수는 보통 Set이나 배열에 저장됩니다.)
<script>

// 일반적인 구독 패턴
const unsubscribe = store.subscribe(() => console.log('상태 변경!'));

// 컴포넌트가 사라질 때 반드시 호출해야 함
unsubscribe();

</script>

만약 개발자가 컴포넌트가 언마운트될 때 unsubscribe() 호출을 잊어버리면 어떻게 될까? 스토어의 구독자 목록(Set)이 콜백 함수에 대한 강한 참조를 계속 붙들고 있기 때문에, 가비지 컬렉터는 이 콜백 함수와 관련된 메모리를 해제하지 못한다. --> 이것이 바로 메모리 누수!!

  • WeakSet을 이용한 해결책: 자동 구독 해제
<script>

// 개념 설명을 위한 가상 코드입니다.
class SafeStore {
  // 구독 콜백(함수 객체)을 WeakSet으로 관리합니다.
  #listeners = new WeakSet();

  subscribe(listener) {
    this.#listeners.add(listener);
    console.log("새로운 리스너가 등록되었습니다.");
  }

  // ... (상태 변경 및 알림 로직)
}

const safeStore = new SafeStore();

function setupComponent() {
  // 컴포넌트 스코프 내에 리스너 함수가 정의됩니다.
  const myListener = () => {
    console.log("상태가 변경되었습니다!");
  };
  safeStore.subscribe(myListener);

  // 이 함수(setupComponent)가 실행 종료되면
  // myListener에 대한 강한 참조는 사라집니다.
}

setupComponent();

// 이제 setupComponent 스코프가 사라져 myListener에 대한 강한 참조가 없습니다.
// 따라서 가비지 컬렉터는 언젠가 myListener 함수를 메모리에서 수거해가고,
// safeStore의 #listeners(WeakSet)에서도 자동으로 흔적이 사라집니다.
// unsubscribe를 호출하지 않아도 메모리 누수가 발생하지 않습니다!

</script>

(이처럼 WeakSet은 객체의 생명주기에 관여하지 않으면서 해당 객체에 대한 부가 정보(여기서는 '구독자'라는 사실)를 안전하게 저장하고 싶을 때 또 하나의 선택지가 될 수 있다.)

참고: 실제 Zustand는 WeakSet 대신 Set을 사용하고 개발자가 직접 unsubscribe를 호출하는 방식을 사용한다. 이는 명시적인 제어를 선호하고 가비지 컬렉션 시점의 불확실성을 피하기 위함이지만, 위 예시는 WeakSet의 핵심 원리를 이해하는 데 큰 도움이 될 듯해서 필자가 넣어보았다!

profile
확장성 있는 설계와 유지보수가 용이한 클린 코드 지향하는 개발자입니다.

0개의 댓글