배열 안에 같은 값이 여러 번 반복해서 등장하는 경우가 종종 있습니다. 예를 들어 다음과 같은 숫자 배열을 살펴보겠습니다.
const arr = [1, 4, 3, 3, 1, 3, 2, 4, 2, 1, 4, 4];
이 배열에는 1이 세 번, 4가 네 번, 3이 세 번, 2가 두 번씩 등장합니다. 여기서 만들고자 하는 JavaScript 함수는 이 배열에서 두 번 이상 나타난 값(중복 값)만 골라내고, 각 값을 딱 한 번만 결과에 포함하는 것입니다.
따라서 위 배열에 대한 기대 출력은 다음과 같습니다.
const output = [1, 4, 3, 2];
기본 구현: indexOf와 lastIndexOf 활용
핵심 아이디어는 간단합니다. 어떤 요소의 첫 번째 인덱스(indexOf)와 마지막 인덱스(lastIndexOf)가 서로 다르다면, 그 값은 배열 안에서 두 번 이상 등장한 중복 값이라는 의미입니다. 여기에 이미 결과 배열에 담겼는지 확인하는 includes 검사를 더하면 됩니다.
const arr = [1, 4, 3, 3, 1, 3, 2, 4, 2, 1, 4, 4];
const removeDuplicate = arr => {
const res = [];
for (let i = 0; i < arr.length; i++) {
// indexOf와 lastIndexOf가 다르면 중복 값
if (arr.indexOf(arr[i]) !== arr.lastIndexOf(arr[i])) {
if (!res.includes(arr[i])) {
res.push(arr[i]);
}
}
}
return res;
};
console.log(removeDuplicate(arr));
실행 결과
콘솔에 출력되는 결과는 다음과 같습니다.
[1, 4, 3, 2]
출력 순서는 각 값이 원본 배열에서 처음 등장한 순서를 그대로 따릅니다.
더 간결한 대안: filter와 Set 조합
같은 로직은 filter와 Set을 활용하면 훨씬 짧은 코드로 표현할 수 있습니다. 먼저 filter로 중복 값만 걸러낸 뒤, Set으로 겹치는 값을 제거하고 전개 연산자(spread)로 다시 배열로 변환합니다.
const removeDuplicate = arr =>
[...new Set(arr.filter(v => arr.indexOf(v) !== arr.lastIndexOf(v)))];
console.log(removeDuplicate(arr)); // [1, 4, 3, 2]
성능 개선 팁: Map으로 등장 횟수 세기
위 방식들은 indexOf, lastIndexOf, includes가 모두 배열을 처음부터 끝까지 탐색하므로 전체 시간 복잡도가 O(n²)입니다. 데이터 양이 많다면 Map으로 각 값의 등장 횟수를 한 번에 집계한 후 필터링하면 O(n)으로 최적화할 수 있습니다.
const removeDuplicate = arr => {
const count = new Map();
for (const v of arr) {
count.set(v, (count.get(v) || 0) + 1);
}
return [...count].filter(([, c]) => c > 1).map(([v]) => v);
};
console.log(removeDuplicate(arr)); // [1, 4, 3, 2]상황에 맞는 방법을 선택하면 되는데, 코드 가독성이 중요하다면 filter + Set 조합이, 대용량 데이터 처리 성능이 중요하다면 Map 기반 접근이 적합합니다.