Published on

ES2024 Set 메서드로 교집합·차집합 한줄 처리

Authors

서로 다른 데이터 소스에서 ID 목록을 합치거나, 권한/피처 플래그를 비교하거나, 캐시 무효화 대상을 뽑을 때 교집합·차집합은 자주 등장합니다. 그동안 자바스크립트에서는 filterhas 조합으로 직접 구현하는 경우가 많았는데, ES2024에서는 Set 에 집합 연산 메서드가 추가되면서 이 작업을 “의미가 드러나는 한 줄”로 표현할 수 있게 됐습니다.

이 글에서는 ES2024 Set 메서드의 핵심 사용법과 실전 패턴, 그리고 도입 시 주의해야 할 호환성/성능 포인트를 정리합니다.

ES2024에서 Set 에 추가된 집합 연산

ES2024(정확히는 최신 ECMAScript 표준에 반영된 변경)에서는 Set 프로토타입에 다음과 같은 메서드들이 추가되었습니다.

  • intersection(other) 교집합
  • difference(other) 차집합
  • union(other) 합집합
  • symmetricDifference(other) 대칭차집합
  • isSubsetOf(other) 부분집합 여부
  • isSupersetOf(other) 상위집합 여부
  • isDisjointFrom(other) 서로소 여부

이 메서드들은 공통적으로 otherSet 뿐 아니라 “집합처럼 순회 가능한(iterable) 값”을 받을 수 있습니다. 즉 new Set([1,2]) 뿐 아니라 배열, Map 의 키 이터레이터 등도 인자로 넣을 수 있습니다.

왜 중요한가: 가독성 + 의도 표현

기존 방식은 보통 아래처럼 작성했습니다.

const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5]);

// 교집합(기존)
const intersection = new Set([...a].filter(x => b.has(x)));

// 차집합(기존) - a \ b
const difference = new Set([...a].filter(x => !b.has(x)));

문제는 이 코드가 “교집합/차집합”이라는 의도가 드러나지 않고, 매번 동일한 패턴을 손으로 작성해야 하며, 실수로 ! 를 빼먹거나 입력을 배열로 바꾸는 등 변형이 생기기 쉽다는 점입니다.

ES2024에서는 이렇게 바뀝니다.

const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5]);

const intersection = a.intersection(b);
const difference = a.difference(b);

의도가 메서드 이름에 그대로 담기고, 구현 디테일을 반복하지 않아도 됩니다.

한 줄로 끝내는 교집합·차집합 패턴

1) 교집합: 공통 사용자/공통 권한 뽑기

예를 들어 “요청한 권한”과 “사용자에게 부여된 권한”의 교집합만 최종 허용 권한으로 만들고 싶다면 다음처럼 작성할 수 있습니다.

const requested = new Set(["read", "write", "admin"]);
const granted = new Set(["read", "write"]);

const allowed = requested.intersection(granted);
// Set { 'read', 'write' }

이 결과는 여전히 Set 이므로, 후속 로직에서 allowed.has("read") 같은 체크가 빠르게 동작합니다.

2) 차집합: 제거 대상/미동기화 대상만 추리기

캐시 키나 태그 목록에서 “이제 더 이상 존재하지 않는 항목”만 골라 삭제하는 패턴은 차집합이 딱 맞습니다.

const prev = new Set(["p1", "p2", "p3"]);
const next = new Set(["p2", "p3", "p4"]);

// 이전에는 있었지만 지금은 없는 것 = 삭제 대상
const toDelete = prev.difference(next);
// Set { 'p1' }

반대로 “새로 추가된 것”은 next.difference(prev) 로 얻습니다.

3) 배열을 그대로 넣기: Set 변환을 생략

인자로 이터러블을 받을 수 있으므로, 굳이 오른쪽을 Set 으로 감싸지 않아도 됩니다.

const a = new Set([1, 2, 3, 4]);

// 배열을 그대로 전달
const common = a.intersection([2, 4, 6]);
// Set { 2, 4 }

다만 성능 관점에서는, 오른쪽이 큰 배열이라면 내부적으로 has 를 빠르게 하기 위해 어떤 최적화를 하는지 엔진 구현에 따라 달라질 수 있습니다. 대량 데이터라면 오른쪽도 Set 으로 미리 만들어두는 편이 예측 가능성이 높습니다.

실전 예제: 피처 플래그 롤아웃 대상 계산

운영에서 흔한 요구는 “전체 사용자 집합 중, 실험군 대상만 뽑고, 제외 목록을 적용하고, 최종 적용 대상을 산출”하는 것입니다.

아래 예시는 ES2024 메서드들로 파이프라인처럼 읽히는 코드를 만드는 방식입니다.

const allUsers = new Set(["u1", "u2", "u3", "u4", "u5"]);
const experimentGroup = new Set(["u2", "u3", "u6"]);
const blocklist = new Set(["u3"]);

// 1) 전체 중 실험군에 포함되는 사용자
const candidates = allUsers.intersection(experimentGroup);

// 2) 후보 중 차단된 사용자 제거
const rolloutTargets = candidates.difference(blocklist);

// Set { 'u2' }

여기서 intersectiondifference 가 “도메인 용어”처럼 동작하기 때문에, 리뷰어가 코드를 읽을 때 구현을 따라가며 머릿속으로 변환할 필요가 줄어듭니다.

symmetricDifference 까지 알면 비교 로직이 깔끔해진다

대칭차집합은 “서로 다른 것만” 남깁니다. 즉 변경 사항(diff)을 뽑을 때 유용합니다.

const oldSet = new Set(["a", "b", "c"]);
const newSet = new Set(["b", "c", "d"]);

const changed = oldSet.symmetricDifference(newSet);
// Set { 'a', 'd' }

이 값은 “삭제된 항목 + 추가된 항목”을 합친 결과입니다. 이후에 삭제/추가를 분리해야 한다면 다음처럼 두 번의 차집합으로 나누는 편이 명확합니다.

const removed = oldSet.difference(newSet);
const added = newSet.difference(oldSet);

주의사항: Set 은 값 동등성에 SameValueZero 를 쓴다

Set 의 포함 여부는 객체의 “내용”이 아니라 “참조”를 기준으로 판단합니다. 따라서 객체를 원소로 쓰는 경우 교집합/차집합이 기대와 다를 수 있습니다.

const a1 = { id: 1 };
const a2 = { id: 1 };

const s1 = new Set([a1]);
const s2 = new Set([a2]);

const inter = s1.intersection(s2);
// Set {}  (비어 있음)

이 문제를 피하려면 원소를 원시값(예: id 문자열)으로 정규화하거나, 별도의 키 추출 로직을 적용해야 합니다.

런타임/번들링: 어디서 바로 쓸 수 있나

이 기능은 “문법”이 아니라 “표준 라이브러리 메서드” 추가입니다. 즉 트랜스파일러만으로는 해결되지 않고, 실행 환경이 해당 메서드를 제공해야 합니다.

  • 최신 Node.js / 최신 브라우저에서는 점진적으로 지원됩니다.
  • 구버전 Node.js나 레거시 브라우저를 타깃으로 한다면 폴리필 전략이 필요합니다.

Next.js 기반 프로젝트라면, 서버 런타임(Node.js) 버전과 엣지 런타임의 지원 차이를 함께 봐야 합니다. 특히 RSC나 캐시 계층을 다루는 프로젝트에서는 런타임 차이로 “로컬에서는 되는데 배포에서 깨지는” 상황이 생길 수 있으니, 관련해서는 Next.js 14 RSC 캐시 꼬임과 stale 데이터 해결법도 함께 참고하면 맥락을 잡는 데 도움이 됩니다.

폴리필이 필요할 때의 대안 구현(안전한 유틸)

당장 ES2024 메서드를 못 쓰는 환경이라면, 아래처럼 유틸 함수로 감싸 두면 나중에 런타임이 업그레이드됐을 때 내부 구현만 바꾸면 됩니다.

export function setIntersection(a, b) {
  const bSet = b instanceof Set ? b : new Set(b);
  const out = new Set();
  for (const x of a) {
    if (bSet.has(x)) out.add(x);
  }
  return out;
}

export function setDifference(a, b) {
  const bSet = b instanceof Set ? b : new Set(b);
  const out = new Set();
  for (const x of a) {
    if (!bSet.has(x)) out.add(x);
  }
  return out;
}

호출부는 ES2024 스타일과 거의 동일하게 유지할 수 있습니다.

const a = new Set([1, 2, 3, 4]);
const b = [3, 4, 5];

const inter = setIntersection(a, b);
const diff = setDifference(a, b);

성능 감각: Set 연산은 “작은 상수 시간”을 기대할 수 있다

교집합/차집합을 배열로 처리하면 보통 O(n*m) 이 되기 쉽습니다. 반면 Set 은 평균적으로 hasO(1) 에 가깝기 때문에, intersection 은 대체로 O(n+m) 감각으로 접근할 수 있습니다.

실무에서는 다음 기준이 유용합니다.

  • 입력이 작으면 무엇을 쓰든 차이는 미미하니 가독성을 우선
  • 입력이 커지면 Set 으로 정규화한 뒤 has 기반 연산이 유리
  • 객체 원소를 쓰는 순간 “동등성 기준” 문제가 생기니 키 기반으로 변환

테스트 팁: 결과가 Set 인 점을 잊지 말자

Jest나 Vitest에서 Set 을 직접 비교하면 실패하는 경우가 많습니다. 테스트에서는 배열로 변환해 비교하는 패턴이 편합니다.

import { expect, test } from "vitest";

test("intersection", () => {
  const a = new Set([1, 2, 3]);
  const b = new Set([2, 3, 4]);

  const out = a.intersection(b);
  expect([...out].sort()).toEqual([2, 3]);
});

마무리: 집합 연산을 ‘의도’로 쓰는 시대

ES2024의 Set 집합 연산 메서드는 단순히 코드 줄 수를 줄이는 기능이 아니라, 교집합·차집합이라는 도메인 개념을 코드에 직접 드러내는 도구입니다. 기존의 filter 패턴을 반복 작성하던 코드를 a.intersection(b) 같은 표현으로 바꾸면, 리뷰/유지보수 비용이 눈에 띄게 줄어듭니다.

특히 Node.js 런타임을 올리거나 ESM 전환, 번들링/폴리필 전략을 손보는 타이밍이라면 이런 표준 API 도입이 좋은 정리 포인트가 됩니다. 런타임 전환 이슈가 있다면 Node.js ESM 전환 시 ERR_REQUIRE_ESM 해결 가이드도 함께 확인해 두면 마이그레이션 과정에서 시행착오를 줄일 수 있습니다.