Array.prototype.includes와 indexOf 성능 비교

문제 상황

레거시 코드베이스에서 배열 포함 여부를 체크하는 로직이 indexOf() !== -1 패턴으로 가득했다. ES2016 스펙이 안정화되면서 includes()를 도입할 수 있게 되어 리팩토링을 진행했다.

// 기존 코드
if (allowedRoles.indexOf(userRole) !== -1) {
  // ...
}

// 변경 후
if (allowedRoles.includes(userRole)) {
  // ...
}

성능 측정

1만 개 요소 배열에서 1만 번 탐색하는 간단한 벤치마크를 돌렸다.

const arr = Array.from({ length: 10000 }, (_, i) => i);
const target = 9999;

// indexOf 방식
console.time('indexOf');
for (let i = 0; i < 10000; i++) {
  arr.indexOf(target) !== -1;
}
console.timeEnd('indexOf'); // ~45ms

// includes 방식
console.time('includes');
for (let i = 0; i < 10000; i++) {
  arr.includes(target);
}
console.timeEnd('includes'); // ~47ms

결과는 거의 동일했다. V8 엔진이 내부적으로 같은 알고리즘을 사용하는 것으로 보인다.

includes의 장점

성능보다 중요한 건 NaN 처리였다.

[1, 2, NaN].indexOf(NaN);    // -1
[1, 2, NaN].includes(NaN);   // true

indexOf는 === 비교를 사용해서 NaN을 찾지 못한다. includes는 SameValueZero 알고리즘을 사용해 제대로 처리한다.

결론

코드 가독성과 정확성 측면에서 includes를 사용하는 게 맞다고 판단했다. Babel 트랜스파일 설정도 이미 되어있어서 IE 지원도 문제없었다. 점진적으로 기존 indexOf 패턴을 교체하기로 했다.