자바스크립트로 배열을 다루다 보면 for 루프를 반복적으로 쓰거나, 복잡한 중첩 구조로 데이터를 처리한 경험이 있을 것이다. 다만 대부분의 개발자들은 map, filter, reduce 같은 고차 함수가 정확히 뭐하는 건지, 언제 써야 하는지, 성능은 괜찮은지 제대로 알지 못한 채 구글에서 가져온 코드를 그냥 복사해 붙인다. 이번에는 배열 메서드의 동작 원리부터 실전 예제, 성능 최적화까지 완벽하게 정리해서 소개하겠다.

 

배열 메서드가 필요한 이유

기존 for 루프로 배열을 처리하면 코드가 길어지고 버그가 생기기 쉽다. 예를 들어 배열의 각 요소에 2를 곱해야 한다면, for 루프를 쓸 때와 map을 쓸 때의 차이를 보자.

// ✗ for 루프 - 코드가 길고 실수하기 쉬움
const numbers = [1, 2, 3, 4, 5];
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
  doubled.push(numbers[i] * 2);
}
console.log(doubled); // [2, 4, 6, 8, 10]
// ✓ map - 간결하고 의도가 명확함
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

map은 배열의 각 요소를 변환해서 새로운 배열을 반환한다. 코드가 짧을 뿐 아니라 "각 요소를 2배로 만든다"는 의도가 한눈에 들어온다.

 

map() 메서드 - 배열 요소 변환하기

map()은 배열의 모든 요소에 함수를 적용하고, 그 결과들로 새로운 배열을 만든다. 원본 배열은 변경되지 않는다.

기본 문법
const result = array.map((element, index, array) => {
  // element: 현재 요소
  // index: 현재 요소의 인덱스
  // array: 전체 배열
  return 변환된값;
});
실전 예제
// 상품 목록에서 가격만 추출하기
const products = [
  { id: 1, name: 'notebook', price: 5000 },
  { id: 2, name: 'pen', price: 1000 },
  { id: 3, name: 'pencil', price: 800 }
];

const prices = products.map(p => p.price);
console.log(prices); // [5000, 1000, 800]
// 문자열 배열을 숫자로 변환하기
const strNumbers = ['1', '2', '3', '4', '5'];
const nums = strNumbers.map(Number);
console.log(nums); // [1, 2, 3, 4, 5]

map()은 변환이 필요한 모든 상황에 쓸 수 있다. HTML 요소의 속성 추출, API 응답 재정렬, 데이터 타입 변환 등이 자주 나타난다.

 

filter() 메서드 - 조건에 맞는 요소 추출하기

filter()는 배열에서 조건을 만족하는 요소들만 골라서 새로운 배열을 만든다. 조건 함수가 true를 반환하는 요소만 포함된다.

기본 문법
const result = array.filter((element, index, array) => {
  return 조건식; // true면 포함, false면 제외
});
실전 예제
// 5000원 이상의 상품만 필터링
const products = [
  { id: 1, name: 'notebook', price: 5000 },
  { id: 2, name: 'pen', price: 1000 },
  { id: 3, name: 'pencil', price: 800 },
  { id: 4, name: 'eraser', price: 2000 }
];

const expensiveProducts = products.filter(p => p.price >= 2000);
console.log(expensiveProducts);
// [{id: 1, name: 'notebook', price: 5000}, {id: 4, name: 'eraser', price: 2000}]
// 배열에서 falsy 값 제거하기
const mixed = [0, 1, false, 2, '', 3, null, undefined, 4];
const clean = mixed.filter(Boolean);
console.log(clean); // [1, 2, 3, 4]
// 중복 제거하기
const numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5];
const unique = numbers.filter((n, index, arr) => arr.indexOf(n) === index);
console.log(unique); // [1, 2, 3, 4, 5]

filter()의 핵심은 조건 함수 내에서 단순한 true/false만 반환하면 된다는 것. map()과 달리 새로운 값을 만들지 않고, 기존 요소를 그대로 선별하는 것이다.

 

reduce() 메서드 - 배열을 단일 값으로 축약하기

reduce()는 배열의 모든 요소를 순회하면서 누적 값을 계산한다. 배열을 하나의 값으로 축약할 때 쓰인다. 처음에는 복잡해 보이지만, 원리를 이해하면 가장 강력한 메서드다.

기본 문법
const result = array.reduce((accumulator, currentValue, index, array) => {
  // accumulator: 누적된 값
  // currentValue: 현재 요소
  // index: 현재 요소의 인덱스
  // array: 전체 배열
  return 다음누적값;
}, 초기값);
실전 예제
// 배열의 합계 구하기
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15

// 단계별 실행:
// acc=0, n=1 => 0+1=1
// acc=1, n=2 => 1+2=3
// acc=3, n=3 => 3+3=6
// acc=6, n=4 => 6+4=10
// acc=10, n=5 => 10+5=15
// 객체 배열에서 총 가격 계산하기
const cart = [
  { name: 'notebook', price: 5000, qty: 2 },
  { name: 'pen', price: 1000, qty: 3 },
  { name: 'pencil', price: 800, qty: 5 }
];

const total = cart.reduce((acc, item) => acc + (item.price * item.qty), 0);
console.log(total); // 5000*2 + 1000*3 + 800*5 = 17000
// 배열을 객체로 변환하기
const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' }
];

const userMap = users.reduce((acc, user) => {
  acc[user.id] = user.name;
  return acc;
}, {});
console.log(userMap);
// { '1': 'Alice', '2': 'Bob', '3': 'Charlie' }

reduce()를 제대로 이해하면 배열 집계, 그룹화, 변환 등 거의 모든 데이터 처리를 할 수 있다.

 

메서드 체이닝 - 여러 배열 메서드 연결하기

실무에서는 한 가지 메서드만 쓰는 게 아니라, 여러 메서드를 연결해서 쓴다. filter와 map을 함께 사용하거나, reduce와 map을 조합하는 식이다.

// 2000원 이상 상품의 가격만 추출해서 20% 할인 적용하기
const products = [
  { id: 1, name: 'notebook', price: 5000 },
  { id: 2, name: 'pen', price: 1000 },
  { id: 3, name: 'pencil', price: 800 },
  { id: 4, name: 'eraser', price: 2000 }
];

const discounted = products
  .filter(p => p.price >= 2000)  // 2000원 이상만
  .map(p => p.price * 0.8);     // 20% 할인

console.log(discounted); // [4000, 1600]
// filter → map → reduce 조합: 특정 조건의 총액 계산
const orders = [
  { id: 1, status: 'completed', amount: 50000 },
  { id: 2, status: 'pending', amount: 30000 },
  { id: 3, status: 'completed', amount: 25000 },
  { id: 4, status: 'cancelled', amount: 10000 }
];

const completedTotal = orders
  .filter(order => order.status === 'completed')
  .reduce((acc, order) => acc + order.amount, 0);

console.log(completedTotal); // 75000

체이닝할 때 주의할 점은 각 단계에서 무엇을 반환하는지 명확히 하는 것이다. filter 뒤에는 배열이, reduce 뒤에는 단일 값이 나온다.

 

흔한 실수와 올바른 패턴
실수 1: filter 후 인덱스 사용
// ✗ 잘못된 코드
const numbers = [10, 20, 30, 40, 50];
const filtered = numbers.filter(n => n > 25);
const secondItem = filtered[1]; // 인덱스는 필터링 후 배열 기준
console.log(secondItem); // 40 (원본 배열에서 4번째)
// ✓ 올바른 코드
// filter가 새로운 배열을 만들므로, 필터링된 배열에서 인덱스를 사용하면 됨
const numbers = [10, 20, 30, 40, 50];
const filtered = numbers.filter(n => n > 25);
console.log(filtered); // [30, 40, 50]
console.log(filtered[1]); // 40 (맞음)
실수 2: reduce에서 초기값 생략
// ✗ 잘못된 코드 - 초기값 없으면 첫 요소가 acc가 됨
const products = [{price: 5000}, {price: 1000}];
const total = products.reduce((acc, p) => acc + p.price);
console.log(total); // {price: 5000}1000 (타입 오류 발생)
// ✓ 올바른 코드
const products = [{price: 5000}, {price: 1000}];
const total = products.reduce((acc, p) => acc + p.price, 0);
console.log(total); // 6000
실수 3: 원본 배열 변경
// ✗ 잘못된 코드
const numbers = [1, 2, 3];
numbers.forEach(n => numbers[n] = n * 2); // 원본 변경
console.log(numbers); // 예측 불가능한 결과
// ✓ 올바른 코드
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2); // 새 배열 생성
console.log(numbers); // [1, 2, 3] (원본 유지)
console.log(doubled); // [2, 4, 6]

 

성능 최적화 팁
체이닝 대신 하나의 루프로 통합하기
// ✗ 성능 낮음 - 배열을 3번 순회
const data = Array.from({ length: 10000 }, (_, i) => i);
const result = data
  .filter(n => n % 2 === 0)     // 순회 1
  .map(n => n * 2)             // 순회 2
  .reduce((acc, n) => acc + n, 0); // 순회 3
// ✓ 성능 높음 - 배열을 1번만 순회
const data = Array.from({ length: 10000 }, (_, i) => i);
const result = data.reduce((acc, n) => {
  if (n % 2 === 0) {
    return acc + (n * 2);
  }
  return acc;
}, 0);

다만 가독성을 해칠 정도로 최적화할 필요는 없다. 보통 filter와 map을 2~3개 정도 체이닝하는 것은 성능 문제가 되지 않는다. 데이터 크기가 매우 크면(10만 개 이상) 이런 최적화를 고려하자.

대용량 데이터는 generator나 lazy evaluation 고려
// 매우 큰 배열을 다룰 때는 처음부터 모두 메모리에 올리지 않기
function* filterMap(array, filterFn, mapFn) {
  for (const item of array) {
    if (filterFn(item)) {
      yield mapFn(item);
    }
  }
}

const data = Array.from({ length: 1000000 }, (_, i) => i);
const lazy = filterMap(data, n => n % 2 === 0, n => n * 2);

// 필요한 만큼만 꺼내서 사용
const first10 = [...Array.from({ length: 10 }, () => lazy.next().value)];

 

마무리

배열 메서드는 자바스크립트에서 데이터를 다루는 가장 기본적이면서도 강력한 도구다. for 루프 대신 map, filter, reduce를 쓰는 작은 습관이 모여서 코드 가독성과 유지보수성을 크게 향상시킨다는 점을 잊지 말자. 이 글의 실전 예제 부분을 참고해 프로젝트의 데이터 처리 코드를 리팩토링하면, 더 깔끔하고 버그가 적은 코드를 만날 수 있을 것이다.