Published on

ES2024 Set 메서드로 교집합·차집합 최적화

Authors

서버/클라이언트 어디서든 “두 컬렉션의 교집합/차집합”은 자주 등장합니다. 권한(roles) 비교, 태그 필터링, 캐시 키 정리, A/B 실험 대상 그룹 계산처럼요. 그런데 지금까지의 자바스크립트는 Set이 있어도 집합 연산이 표준 메서드로 없어서 매번 루프를 직접 작성해야 했습니다.

ES2024에서는 Set에 집합 연산 메서드가 추가되면서, 교집합/차집합을 표준 API로 명확하게 표현하고, 구현체 최적화(엔진 레벨 최적화 가능성)까지 기대할 수 있게 됐습니다.

이 글에서는 ES2024 Set 메서드로 교집합·차집합을 구현하는 법, 성능을 좌우하는 포인트(작은 쪽을 순회하기), 그리고 런타임 호환/폴리필 전략을 실전 관점에서 정리합니다.

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

ES2024(정확히는 최신 ECMAScript 표준에 반영된 Set methods)에서 아래 메서드들이 추가되었습니다.

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

여기서 other는 보통 Set이지만, 스펙상 “Set-like” 객체(예: keys()/has()를 제공하는 형태)로도 동작하도록 설계된 구현들이 있습니다. 다만 호환성과 예측 가능성을 위해 실무에서는 Set으로 맞춰서 쓰는 것을 권장합니다.

포인트: 이 메서드들은 원본 Set을 변경하지 않고 새 Set을 반환합니다(불변 스타일).

교집합: intersection으로 의도를 코드에 박기

기존 방식(수동 루프)

많이 쓰던 패턴은 “작은 Set을 순회하며 큰 Set의 has를 확인”하는 방식입니다.

function intersectionManual(a, b) {
  const out = new Set();
  const [small, large] = a.size <= b.size ? [a, b] : [b, a];
  for (const v of small) {
    if (large.has(v)) out.add(v);
  }
  return out;
}

이 구현은 충분히 빠르지만, 매번 작성해야 하고 팀마다 스타일이 달라지기 쉽습니다.

ES2024 방식

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

const inter = a.intersection(b);
// Set { 3, 4 }

코드만 봐도 “교집합”이라는 도메인 의미가 즉시 드러나고, 리뷰 비용이 줄어듭니다.

성능 관점: 작은 쪽을 순회하는 최적화는 여전히 중요

교집합은 본질적으로 “한쪽을 순회하면서 다른 쪽을 has로 검사”합니다. 따라서 시간 복잡도는 대략 O(min(n, m))에 가깝게 만들 수 있습니다.

  • 좋은 구현: 작은 쪽을 순회
  • 나쁜 구현: 큰 쪽을 순회

엔진의 intersection이 내부적으로 이 최적화를 적용할 가능성이 높지만(특히 Set-Set 조합), 런타임/폴리필에 따라 다를 수 있습니다. 성능이 민감한 구간이라면 아래처럼 입력 크기를 통제하는 습관이 좋습니다.

function intersectionFast(a, b) {
  // ES2024 메서드를 쓰되, 작은 쪽을 왼쪽에 두는 습관
  return (a.size <= b.size ? a : b).intersection(a.size <= b.size ? b : a);
}

차집합: difference로 “제거 목록” 처리 단순화

차집합은 “A에서 B를 제거한 결과”입니다. 예를 들어, 캐시 키 목록에서 만료된 키를 제거하거나, 사용자 권한에서 금지 권한을 제거할 때 자주 씁니다.

기존 방식(수동 루프)

function differenceManual(a, b) {
  const out = new Set(a);
  for (const v of b) out.delete(v);
  return out;
}

이 구현은 a를 복사한 뒤 b를 순회하며 삭제합니다.

ES2024 방식

const allRoles = new Set(["read", "write", "admin"]);
const revoked = new Set(["admin"]);

const effective = allRoles.difference(revoked);
// Set { "read", "write" }

불변 스타일이라 “원본이 변하지 않는다”는 점이 특히 중요합니다. 상태 관리나 캐시 레이어에서 원본을 실수로 변형시키면 디버깅이 어려워지는데, difference는 그런 실수를 줄여줍니다.

실전 패턴 1: 권한/피처 플래그 계산

예를 들어 서버에서 내려준 “허용 기능”과 클라이언트 정책상 “차단 기능”이 있을 때, 최종 활성 기능은 차집합으로 계산할 수 있습니다.

function computeEnabledFeatures(serverEnabled, clientBlocked) {
  // 둘 다 Set이라고 가정
  return serverEnabled.difference(clientBlocked);
}

그리고 특정 기능군이 모두 충족되는지(부분집합 여부)는 isSupersetOf 같은 메서드로 더 명확하게 표현할 수 있습니다.

function hasAll(required, enabled) {
  return enabled.isSupersetOf(required);
}

실전 패턴 2: 검색 필터에서 “남은 후보” 줄이기

태그 기반 검색에서 “포함 태그 교집합”을 이용하면 후보를 빠르게 줄일 수 있습니다.

// postId 집합을 태그별로 미리 만들어 둔다고 가정
const byTag = {
  js: new Set([1, 2, 3, 10]),
  web: new Set([2, 3, 4]),
  perf: new Set([3, 5, 10])
};

function queryAll(tags) {
  if (tags.length === 0) return new Set();
  let result = byTag[tags[0]] ?? new Set();
  for (const t of tags.slice(1)) {
    result = result.intersection(byTag[t] ?? new Set());
  }
  return result;
}

queryAll(["js", "web", "perf"]);
// Set { 3 }

여기서도 성능을 더 챙기려면, 교집합을 수행하기 전에 가장 작은 후보군부터 시작하는 정렬이 유리합니다.

function queryAllFast(tags) {
  const sets = tags
    .map(t => byTag[t] ?? new Set())
    .sort((a, b) => a.size - b.size);

  if (sets.length === 0) return new Set();

  let result = sets[0];
  for (const s of sets.slice(1)) {
    result = result.intersection(s);
    if (result.size === 0) break;
  }
  return result;
}

최적화 포인트: 언제 Set이 진짜 이득인가

Set의 강점은 평균적으로 hasO(1)에 가깝다는 점입니다. 따라서 아래 상황에서 특히 이득이 큽니다.

  • 배열 includes로 멤버십 체크를 반복하는 코드가 있다
  • 중복 제거가 필요하다
  • 교집합/차집합을 여러 번 수행한다

반대로 원소 수가 아주 작거나(수십 개 이하) 한 번만 검사하면, 배열 기반이 더 단순할 수 있습니다. 하지만 ES2024 메서드들은 표현력이 좋아서, 규모가 커질 가능성이 있는 코드에서는 미리 Set 기반으로 정리해두는 편이 유지보수에 유리합니다.

호환성: 런타임 지원과 폴리필 전략

문제는 “표준이 생겼다”와 “내 런타임이 지원한다”가 다르다는 점입니다.

기능 감지(feature detection)

function hasSetMethods() {
  return typeof Set.prototype.intersection === "function" &&
         typeof Set.prototype.difference === "function";
}

폴리필(최소 구현 예시)

빌드 타깃이 낮거나 일부 런타임(구형 Node.js, 일부 임베디드 환경)을 지원해야 한다면, 폴리필을 고려할 수 있습니다. 아래는 핵심 아이디어만 담은 예시입니다.

(function polyfillSetMethods() {
  if (typeof Set.prototype.intersection !== "function") {
    Set.prototype.intersection = function(other) {
      const b = other instanceof Set ? other : new Set(other);
      const out = new Set();
      const a = this;
      const [small, large] = a.size <= b.size ? [a, b] : [b, a];
      for (const v of small) if (large.has(v)) out.add(v);
      return out;
    };
  }

  if (typeof Set.prototype.difference !== "function") {
    Set.prototype.difference = function(other) {
      const b = other instanceof Set ? other : new Set(other);
      const out = new Set(this);
      for (const v of b) out.delete(v);
      return out;
    };
  }
})();

주의할 점은 “전역 프로토타입 오염”입니다. 앱 전체에 영향을 주므로, 라이브러리 코드라면 폴리필을 강제하기보다:

  • 애플리케이션 엔트리에서만 주입하거나
  • 별도 유틸로 감싸서(intersection(a, b) 형태) 사용하거나
  • 번들러/폴리필 서비스(core-js 등)를 통해 관리

하는 편이 안전합니다.

Next.js/프론트엔드에서의 체감 포인트

Next.js 같은 환경에서는 서버 컴포넌트(RSC)와 클라이언트 컴포넌트가 섞이며, 데이터 필터링이 서버/클라 양쪽에 존재할 수 있습니다. 이때 Set 집합 연산으로 필터 단계를 명확히 해두면, 렌더링 최적화나 상태 분리에서도 사고가 줄어듭니다.

  • 상태를 불변으로 다루기 쉬움(원본 Set 변경 방지)
  • 필터 로직이 선언적으로 변해 리뷰/리팩터링이 쉬움

관련해서 렌더링 최적화 관점은 Next.js RSC에서 Zustand 상태 분리로 렌더링 최적화 글도 같이 보면 맥락이 잘 이어집니다.

또한 타입스크립트를 쓴다면, “유틸 함수의 시그니처를 어떻게 잡아 추론을 안정화할지”가 중요한데, 이 주제는 TS 5.5+ const 타입파라미터로 추론 고정하기에서 다룬 방식과 결이 같습니다.

타입스크립트 팁: 반환 타입을 Set으로 고정하기

TS에서 Set 메서드가 포함된 lib 정의가 최신이어야 합니다. 프로젝트의 tsconfig.json에서 target 또는 lib를 최신으로 올리지 못하는 경우, 아래처럼 유틸로 감싸 타입을 고정해 두면 마이그레이션이 쉬워집니다.

export function setIntersection<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Set<T> {
  // 런타임이 ES2024 메서드를 지원한다는 전제
  return (a as Set<T>).intersection(b as Set<T>);
}

export function setDifference<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Set<T> {
  return (a as Set<T>).difference(b as Set<T>);
}

이 방식은 “읽기 전용 입력, 새 Set 반환”이라는 계약을 코드 레벨에서 강제해, 실수로 입력을 변경하는 버그를 줄여줍니다.

마이그레이션 체크리스트

  1. 기존의 수동 구현(루프 기반 교집합/차집합)을 검색한다.
  2. 결과가 “새 컬렉션”이어야 하는지, “원본 변형”이 허용되는지 확인한다.
  3. 불변이 필요하면 intersection/difference로 치환한다.
  4. 성능 민감 구간은 “작은 Set을 왼쪽에 두기”를 유지한다.
  5. 런타임 지원이 불확실하면 기능 감지 후 폴리필 또는 유틸 래퍼를 사용한다.

정리

ES2024 Set 메서드는 단순히 문법 설탕이 아니라, 자주 쓰는 집합 연산을 표준화된 이름으로 고정해 코드의 의도를 선명하게 만들고, 구현 중복을 줄이며, 엔진 최적화 여지까지 열어줍니다.

  • 교집합은 intersection
  • 차집합은 difference
  • 성능은 여전히 “작은 쪽부터 순회”가 핵심
  • 호환성은 기능 감지와 폴리필/래퍼로 관리

프로덕션 코드에서 교집합/차집합이 반복된다면, 지금이야말로 수동 루프를 걷어내고 ES2024 집합 연산으로 리팩터링하기 좋은 타이밍입니다.