두 배열의 교집합을 구할 일이 생겼다. 선택된 태그 목록과 게시글이 가진 태그 목록을 비교해서 겹치는 걸 뽑는, 흔한 작업이다.
그동안은 이렇게 썼다.
const common = tagsA.filter((t) => tagsB.includes(t));
동작은 하는데, filter 안에서 매번 includes가 배열을 처음부터 훑으니 O(n²)이다. 태그 몇 개일 때야 상관없지만, 수백~수천 개짜리 목록을 비교하면 눈에 띄게 느려진다. 그래서 보통 한쪽을 Set으로 만들어 has로 조회하는 식으로 최적화하는데, 코드가 은근히 길어진다.
이제 Set에 메서드가 생겼다
2025년 들어 모든 주요 브라우저에 Set 인스턴스 메서드가 baseline으로 들어왔다. 집합 연산을 Set끼리 바로 할 수 있다.
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
a.intersection(b); // Set(2) { 3, 4 } 교집합
a.union(b); // Set(6) { 1, 2, 3, 4, 5, 6 } 합집합
a.difference(b); // Set(2) { 1, 2 } 차집합 (a에만 있는 것)
a.symmetricDifference(b); // Set(4) { 1, 2, 5, 6 } 대칭 차집합
이름만 봐도 뭘 하는지 바로 읽힌다. 반환값도 새 Set이라 원본은 안 건드린다.
앞의 태그 교집합은 이렇게 바뀐다.
const common = new Set(tagsA).intersection(new Set(tagsB));
// 배열이 필요하면
const commonArr = [...common];
내부적으로 해시 조회라 O(n)에 가깝다. filter/includes 조합보다 빠르면서 의도도 더 분명하다.
판별 메서드도 같이 왔다
값을 반환하는 것 말고, 관계를 불린으로 확인하는 메서드도 세 개 있다.
const required = new Set(['read', 'write']);
const granted = new Set(['read', 'write', 'delete']);
required.isSubsetOf(granted); // true — 필요한 권한을 다 가졌나?
granted.isSupersetOf(required); // true
a.isDisjointFrom(b); // false — 겹치는 게 하나도 없나?
권한 체크처럼 "이 목록이 저 목록에 다 포함되나?"를 확인할 때 특히 깔끔하다.
알아둘 점 하나
인자로는 Set이 아니라 size와 has, keys를 가진 "set-like" 객체면 되지만, 실무에선 그냥 배열을 new Set()으로 감싸 넘기는 게 안전하다. 배열을 바로 넘기면 동작하지 않는다.
new Set(a).intersection(b); // b가 배열이면 TypeError
new Set(a).intersection(new Set(b)); // OK
교집합·차집합 로직을 직접 짜거나 lodash의 intersection, difference를 부르던 습관이 있었다면, 이제 표준 문법으로 대체할 수 있다. 코드도 짧아지고 성능도 챙긴다.