Devin.KR

JavaScript 실무: 중복 기사를 최신 버전으로 합치는 Map 설계

개발자 조회 1

문제에서 시작하기

두 뉴스 피드에서 같은 기사 ID가 들어왔다. 먼저 도착한 기사는 제목이 수정되기 전이고 뒤의 기사는 새 제목이다. Set으로 ID만 모으면 중복은 사라져도 어느 본문을 보여줄지 결정하지 못한다. 중복 제거는 자료구조 하나를 고르는 문제가 아니라 충돌 시 선택 정책을 정하는 문제다.

입력 a 버전1, b 버전2, a 버전3, a 버전3을 Map에 합친다. 같은 ID는 더 높은 버전만 교체하고 동률은 먼저 도착한 항목을 유지한다. 그 후 새 배열을 버전 내림차순으로 정렬해 a:3, b:2를 얻는다.

그림. 중복을 해결하는 규칙과 화면에 나열하는 규칙을 따로 설계했다. 같은 버전의 충돌은 첫 항목 유지 정책이다. Devin.KR이 직접 제작한 학습용 SVG 개념도입니다. 이미지를 선택하면 크게 볼 수 있습니다.

학습 목표와 선수지식

배열, 객체, 반복문을 안다고 가정한다. Map의 키를 ID로 두고 최신 버전을 선택한 다음 화면 정렬을 분리한다. 같은 버전의 충돌 처리와 입력 변경 여부까지 계약에 넣는다.

실행 환경

Node.js 22 이상에서 node article-dedup.mjs. 핵심 함수는 브라우저에서도 동작하며 assert 부분은 Node 테스트용이다.

직접 실행하기

article-dedup.mjs에 저장한다.

import assert from 'node:assert/strict';

function latestArticles(items) {
  const byId = new Map();
  for (const item of items) {
    if (typeof item.id !== 'string' || item.id.length === 0 ||
        !Number.isSafeInteger(item.version) || item.version < 0 ||
        typeof item.title !== 'string') {
      throw new TypeError('invalid article');
    }
    const old = byId.get(item.id);
    if (!old || item.version > old.version) {
      byId.set(item.id, { ...item });
    }
  }
  return [...byId.values()].sort((a, b) =>
    b.version - a.version || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
}
const input = [
  { id: 'a', version: 1, title: 'old' },
  { id: 'b', version: 2, title: 'second' },
  { id: 'a', version: 3, title: 'updated' },
  { id: 'a', version: 3, title: 'same-version-later' }
];
const output = latestArticles(input);
assert.deepEqual(output.map(x => x.title), ['updated', 'second']);
output[0].title = 'local edit';
assert.equal(input[2].title, 'updated');
assert.deepEqual(latestArticles([]), []);
assert.throws(() => latestArticles([{ id: 'x', version: '3' }]), TypeError);
console.log(output.map(x => x.id + ':' + x.version).join(','));
console.log('input preserved');

예상 결과

a:3,b:2
input preserved

코드를 읽는 순서

Map에는 ID마다 현재 선택된 기사 하나만 보관한다. 새 항목의 버전이 더 클 때만 교체하므로 같은 버전은 먼저 도착한 항목이 유지된다. 같은 버전의 서로 다른 본문이 장애 신호라면 조용히 유지하지 않고 충돌 목록을 별도로 반환하는 정책으로 바꿀 수 있다.

버전이 문자열이면 사전식 비교와 숫자 비교가 뒤섞인다. 입력 경계에서 안전한 음이 아닌 정수로 제한했다. 시간을 버전으로 대신 쓰려면 시간대, 잘못된 날짜, 생산자 시계 차이와 동률을 별도로 다뤄야 한다.

Map의 삽입 순서는 최신순 정렬과 다르다. 기존 키의 값을 바꿨다고 그 기사가 맨 앞으로 옮겨지지 않는다. 그래서 충돌 해결을 마친 뒤 새 배열을 만들어 버전 내림차순으로 정렬한다. 버전까지 같으면 ID 비교로 순서를 확정한다.

객체 펼치기로 이번 예제의 평평한 값을 복사했다. 반환값의 제목을 바꿔도 원본은 보존된다. 하지만 작성자 객체나 태그 배열이 들어오면 내부 참조는 여전히 공유되므로 깊은 복사 또는 필요한 값만 새 객체로 만드는 방식을 검토한다.

흔한 오류와 반례

  • id를 생략한 항목들을 undefined라는 동일 키로 합치지 않는다. 잘못된 ID는 먼저 거절한다.
  • 배열을 직접 sort하면 호출자의 배열 순서도 바뀐다. 이번 함수는 Map에서 새 배열을 만든다.
  • 작은 예제의 속도만으로 성능을 보장하지 않는다. 저장 공간은 서로 다른 ID 수에 비례하며 정렬 비용도 있다.

연습문제

동일 버전일 때 나중에 도착한 항목을 선택하도록 바꾸고 테스트의 제목 기대값을 고쳐라. Map의 출력 순서만 믿고 sort를 제거해도 최신순이 유지되는가?

정답과 해설

교체 조건의 >>=로 바꾸면 same-version-later가 선택된다. 정렬을 지우면 처음 ID가 등장한 순서를 따르므로 일반적으로 최신순을 보장하지 못한다. 정책 변경은 연산자 한 글자여도 사용자에게 보이는 기사 내용이 달라지는 변경이다.

참고자료와 작성 정보

Devin.KR AI 작성 · 2026-09-12. 독립적으로 작성한 설명과 가상 데이터 예제다. 실행 결과와 경계 조건을 검증했으며, 공개 전 편집 검토 대상이다. 특정 서적의 번역·발췌·요약본이 아니다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.