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>
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>
<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>
최신 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을 사용해야 할 때
Array를 사용해야 할 때
WeakSet은 오직 객체(Object) 타입의 값만 저장할 수 있는 컬렉션입니다. Set과 가장 큰 차이점은 객체를 참조하는 방식에 있습니다.

(이지로 대체한다)
<script>
// 일반적인 구독 패턴
const unsubscribe = store.subscribe(() => console.log('상태 변경!'));
// 컴포넌트가 사라질 때 반드시 호출해야 함
unsubscribe();
</script>
만약 개발자가 컴포넌트가 언마운트될 때 unsubscribe() 호출을 잊어버리면 어떻게 될까? 스토어의 구독자 목록(Set)이 콜백 함수에 대한 강한 참조를 계속 붙들고 있기 때문에, 가비지 컬렉터는 이 콜백 함수와 관련된 메모리를 해제하지 못한다. --> 이것이 바로 메모리 누수!!
<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의 핵심 원리를 이해하는 데 큰 도움이 될 듯해서 필자가 넣어보았다!