JavaScript 배열 메서드: map filter forEach reduce 차이

2026.02.10·수정 2026.09.12·약 27분·작성: 해비·블로그 소개

주요 포인트 한눈에 보기

배열(Array)은 장바구니, 검색 결과, 태그 목록처럼 “화면에 반복 렌더링되는 목록”의 출발점입니다. 이 문서는 배열을 만들고(생성), 원본을 지키고(복사), 필요한 것만 골라 쓰는(메서드) 흐름을 예제로 빠르게 정리합니다. reduce()는 활용 패턴이 많아 별도 글로 분리했으니, 아래 링크 섹션을 참고해 주세요.

배열 개념과 특징

배열은 “순서가 있는 목록”입니다. 인덱스와 length만 익히면, 대부분의 배열 코드를 읽기 시작할 수 있습니다.

const cartItems = ['apple', 'banana', 'peach'];

console.log(cartItems[0]); // 'apple' (첫 번째)
console.log(cartItems[cartItems.length - 1]); // 'peach' (마지막)
console.log(cartItems.length); // 3 (총 개수)

동작 설명

배열은 “번호가 붙은 목록”입니다. cartItems[0]처럼 번호(인덱스)로 바로 꺼낼 수 있고, length로는 “몇 개가 들어있는지”를 확인합니다.

초보자를 위한 포인트!

배열 생성·복사·구조 분해

이 파트는 어렵게 외우지 않으셔도 됩니다. “만들기 → 복사하기 → 꺼내기”만 예시로 바로 확인하시면 됩니다.

배열 만들기

const arr = [10, 20, 30];
console.log(arr);

[]로 만들면 끝입니다. 값이 순서대로 들어가고, 그 순서대로 꺼내 쓰는 “목록”이 됩니다.

배열 복사 (원본 보호)

const origin = [1, 2, 3];
const alias = origin;      // 복사 아님(같은 배열 공유)
const copy = [...origin];  // 새 배열(복사)

alias.push(4);
console.log(origin); // [1, 2, 3, 4]
console.log(copy);   // [1, 2, 3]

alias = origin은 “복사”가 아니라 “같은 배열을 같이 보는 것”입니다. 원본을 지키려면 [...origin]처럼 새 배열을 만들어 작업하시면 됩니다.

객체가 들어있을 때 주의

const users = [{ id: 1 }, { id: 2 }];
const copied = [...users];

copied[0].id = 999;
console.log(users[0].id); // 999

배열은 새로 만들었지만, 안의 객체는 공유될 수 있습니다. 그래서 복사본에서 객체 속성을 바꾸면 원본 쪽에도 영향이 갈 수 있습니다.

Array.from으로 배열로 변환

function demo() {
  const realArray = Array.from(arguments);
  return realArray.map((v) => String(v));
}

console.log(demo(1, 2, 3)); // ['1','2','3']

arguments처럼 배열처럼 보이지만 배열 메서드 사용이 애매한 값이 있습니다. 이럴 땐 Array.from()으로 배열로 바꾼 다음 쓰시면 됩니다.

구조 분해로 값 꺼내기

const point = [10, 20];
const [x, y] = point;

console.log(x); // 10
console.log(y); // 20

인덱스를 여러 번 쓰지 않고도, 필요한 값만 변수로 바로 꺼낼 수 있습니다.

추가·삭제·수정: 원본 변경 메서드

이 파트의 메서드는 배열 “원본”을 직접 바꿉니다. 빠르고 편하지만, 같은 배열을 여러 곳에서 공유하면 예상치 못한 결과가 생길 수 있어 주의가 필요합니다.

push / pop (끝에 추가·삭제)

const cart = ['apple'];
cart.push('banana');
cart.pop();

console.log(cart); // ['apple']

이 메서드들은 “원본 배열을 직접 바꿉니다”. 로컬 임시 배열을 만들 때는 편하지만, 공유 데이터에는 조심하셔야 합니다.

splice (중간 삭제/삽입)

const list = ['a', 'b', 'c', 'd'];
list.splice(1, 2, 'x');

console.log(list); // ['a', 'x', 'd']

splice는 중간을 잘라내거나 바꾸는 데 강력하지만, 원본이 바뀌기 때문에 “원본 유지가 필요한 곳”에서는 주의가 필요합니다.

sort (원본 변경) + 안전한 정렬

const scores = [3, 1, 2];

const sorted1 = [...scores].sort((a, b) => a - b);
console.log(scores);  // [3, 1, 2]
console.log(sorted1); // [1, 2, 3]

sort()는 원본을 바꿉니다. 그래서 안전하게 정렬하려면 [...scores]로 복사 후 정렬하는 방식이 가장 흔합니다.

배열에서 “찾기/확인하기”를 담당하는 메서드들입니다. 무엇이 있는지 확인하거나, 조건을 만족하는 값을 찾고, 필요하면 중간에 멈추는 패턴까지 함께 익힙니다.

includes / indexOf (있나? 어디 있나?)

const tags = ['js', 'ts', 'react'];

console.log(tags.includes('ts')); // true
console.log(tags.indexOf('react')); // 2
console.log(tags.indexOf('next'));  // -1

includes는 “있는지”만 필요할 때 가장 깔끔합니다. indexOf는 “몇 번째인지”가 필요할 때 씁니다. 없으면 -1이 나옵니다.

find / findIndex (첫 번째로 찾기)

const users = [
  { id: 1, name: 'A' },
  { id: 2, name: 'B' },
];

const u = users.find((x) => x.id === 2);
const idx = users.findIndex((x) => x.id === 2);

console.log(u);   // { id: 2, name: 'B' }
console.log(idx); // 1

find는 “요소 자체”를, findIndex는 “위치(인덱스)”를 반환합니다. 찾자마자 멈추기 때문에 “첫 번째만 필요할 때” 좋습니다.

forEach에는 break가 없습니다

const nums = [1, 2, 3, 4];

nums.forEach((n) => {
  if (n === 3) return; // 반복 중단이 아님 (그냥 이번 콜백만 종료)
  console.log(n);
});

forEach에는 반복을 종료하는 break 문법이 없습니다. “찾으면 멈추기”가 필요하면 아래처럼 some 또는 find를 쓰는 편이 안전합니다.

some으로 “찾으면 멈추기”

const nums = [1, 2, 3, 4];

nums.some((n) => {
  if (n === 3) return true; // true면 즉시 중단
  console.log(n);
  return false;
});

some은 콜백이 true를 반환하는 순간 즉시 멈춥니다. “조건을 만족하는 게 하나라도 있나요?” 같은 질문에 특히 잘 맞습니다.

변환·가공 메서드

원본은 그대로 두고, 새 배열을 만들어 “모양을 바꾸거나(map)” “필요한 것만 남기는(filter)” 작업을 합니다. 화면에 뿌릴 데이터를 만들 때 가장 자주 쓰입니다.

map / filter (변환 vs 추출)

const nums = [1, 2, 3, 4];

const doubled = nums.map((n) => n * 2);
const evens = nums.filter((n) => n % 2 === 0);

console.log(doubled); // [2, 4, 6, 8]
console.log(evens);   // [2, 4]

map은 “모양 바꾸기(변환)”, filter는 “조건에 맞는 것만 남기기(추출)”입니다. 둘 다 새 배열을 반환하며 메서드 자체는 원본 배열을 변경하지 않습니다. 다만 콜백에서 원본이나 요소 객체를 직접 바꾸면 변경이 남습니다. filter에 남은 객체는 원본과 같은 객체입니다.

flat / flatMap (중첩을 펴기)

const nested = [[1, 2], [3, 4]];
console.log(nested.flat()); // [1, 2, 3, 4]

const words = ['hi', 'ok'];
console.log(words.flatMap((w) => w.split(''))); // ['h','i','o','k']

flat은 중첩 배열을 펴고, flatMap은 “map 후 깊이 1의 flat”을 한 번에 처리합니다. 더 깊은 중첩까지 모두 펴지는 것은 아닙니다.

오류 예제: map에서 push 쓰기

const nums = [1, 2, 3];
const out = [];

nums.map((n) => {
  out.push(n * 2);
});

console.log(out); // [2, 4, 6]

map은 “반환값으로 새 배열을 만드는 도구”인데, 위처럼 외부 배열에 push하면 의도가 흐려집니다.

개선 예제: map은 반환으로 끝내기

const nums = [1, 2, 3];
const out = nums.map((n) => n * 2);

console.log(out); // [2, 4, 6]

같은 결과라도 “의도가 보이게” 쓰면 코드가 훨씬 읽기 쉬워집니다.

정렬·역순·부분 변경: 비파괴 메서드

정렬·역순·부분 교체가 필요해도, 원본을 지키고 새 결과를 얻는 방법입니다. 상태 관리나 렌더링 데이터처럼 “원본 보호”가 중요한 곳에서 특히 유용합니다.

toSorted / toReversed (원본 유지)

const a = [3, 1, 2];
const sorted = a.toSorted((x, y) => x - y);
const reversed = a.toReversed();

console.log(a);        // [3, 1, 2]
console.log(sorted);   // [1, 2, 3]
console.log(reversed); // [2, 1, 3]

이 메서드들은 원본을 바꾸지 않고, 결과를 새 배열로 반환합니다. “원본 유지가 중요한 코드”에서 특히 안전합니다. 구형 환경을 고려해야 한다면 [...a].sort((x, y) => x - y) 같은 방식으로 대체하는 편이 무난합니다.

toSpliced / with (부분 수정도 안전하게)

const items = ['a', 'b', 'c', 'd'];

const removed = items.toSpliced(1, 2); // 1번부터 2개 제거한 새 배열
const replaced = items.with(1, 'x');   // 1번 값을 바꾼 새 배열

console.log(items);    // ['a', 'b', 'c', 'd']
console.log(removed);  // ['a', 'd']
console.log(replaced); // ['a', 'x', 'c', 'd']

중간 삭제/교체가 필요해도 원본을 보존할 수 있습니다. splice와 달리 “원본을 지키는” 점이 핵심입니다.

배열 메서드 정리표

원본을 바꾸는 메서드와, 원본을 유지하는 메서드를 빠르게 구분하는 용도입니다.

구분 대표 메서드 한 줄 요약
원본 변경(주의) push, pop, splice, sort, reverse 배열 자체가 바뀜 (공유 데이터면 버그 원인)
비변경/비파괴 map, filter, slice, concat, toSorted, toReversed, toSpliced, with 새 배열 반환 (내부 객체 참조와 콜백 변경은 별도 확인)
JavaScript 배열 메서드: map filter forEach reduce 차이 핵심 내용을 설명하는 본문 이미지
JavaScript 배열 메서드의 원본 배열 변환 선별 실행 누적 선택 흐름

reduce()는 활용 패턴이 많아서 따로 자세히 정리해두었습니다. 필요하실 때 아래 링크를 참고해 주세요.

JavaScript reduce: 초기값과 누적 과정 자세히 보기

결론

배열은 결국 “목록을 화면에 쓰기 좋은 형태로 만들기” 위한 도구입니다. 그래서 실무에서는 문법을 외우기보다, 어떤 메서드를 쓰면 의도가 잘 보이는지를 기준으로 선택하시는 편이 훨씬 빠르게 늘어납니다.

FAQ (면접 질문)

Q. map과 forEach의 차이와 선택 기준을 말해보세요.
map은 “새 배열을 만들 때” 쓰고, 콜백의 반환값이 새 배열의 요소가 됩니다. forEach는 “그냥 실행” 용도라 반환값이 의미 없고, 중간에 멈추기도 어렵습니다.

Q. find와 filter는 결과가 어떻게 다르며, 언제 각각 쓰나요?
find는 조건을 만족하는 “첫 번째 요소 1개”를 반환하고 못 찾으면 undefined입니다. filter는 조건을 만족하는 “전부”를 모아 “새 배열”로 반환합니다.

Q. sort나 splice가 실무에서 위험하다고 하는 이유는 무엇인가요?
둘 다 원본 배열을 바꿉니다. 같은 배열을 다른 화면/로직이 공유하고 있으면, 한 번의 정렬·삭제가 다른 곳 결과까지 바꾸면서 버그가 나기 쉽습니다. 그래서 보통 “복사 후 작업”을 합니다.

Q. includes와 indexOf는 어떤 차이가 있나요?
includes는 “있는지(불리언)”만 필요할 때 가장 읽기 쉽습니다. indexOf는 “몇 번째인지(숫자)”가 필요할 때 쓰고, 없으면 -1이 나옵니다.

Q. […arr]로 복사했는데도 원본이 바뀌는 것처럼 보일 때가 있습니다. 왜 그런가요?
배열 안에 객체가 들어있으면, 배열은 새로 만들었더라도 “안의 객체”는 공유될 수 있습니다. 그래서 복사본에서 객체 속성을 바꾸면 원본 쪽에도 영향이 갈 수 있습니다.

직접 실습: 예산에 맞는 상품 목록 만들기

이 글의 목표는 배열 메서드의 선택입니다. 합계·그룹 누적은 reduce 글에서 다루고, 여기서는 목록·요소 하나·참거짓이라는 반환 모양을 먼저 정합니다. ZIP의 starter/catalog.mjs에서 아래 요구를 완성하세요.

  1. 예산 이하 상품을 filter로 남기고 숫자 가격 오름차순으로 정렬합니다.
  2. map으로 이름 목록을 만듭니다.
  3. find로 요청 ID의 상품 하나를 찾습니다.
  4. some으로 예산에 맞는 상품이 있는지, every로 모든 가격이 양수인지 검사합니다.

실습 파일

파일 경로를 확인하고 같은 프로젝트 안에 저장하세요. 이미지 등 소스 목록에 없는 파일과 실행 안내는 실습 ZIP에 포함되어 있습니다.

원본 배열을 보존하면서 필요한 상품을 고르고 새 결과 배열을 만듭니다. 수정 전은 시작 상태, 수정 후는 비교할 완성 상태입니다.

수정 전

app.mjs

import { buildCatalog } from './catalog.mjs';
const products = [
  { id: 1, name: '노트', price: 3000 },
  { id: 2, name: '연필', price: 1000 },
  { id: 3, name: '펜', price: 2000 },
];
console.log(buildCatalog(products, 2000, 3));

catalog.mjs

export function buildCatalog(products, budget, wantedId) {
  return { names: [], selected: undefined, hasAffordable: false, allPriced: false };
}

verify.mjs

import assert from 'node:assert/strict';
import { buildCatalog } from './catalog.mjs';
const products = Object.freeze([
  Object.freeze({ id: 1, name: '노트', price: 3000 }),
  Object.freeze({ id: 2, name: '연필', price: 1000 }),
  Object.freeze({ id: 3, name: '펜', price: 2000 }),
]);
assert.deepEqual(buildCatalog(products, 2000, 3), {
  names: ['연필', '펜'],
  selected: products[2],
  hasAffordable: true,
  allPriced: true,
});
assert.equal(buildCatalog(products, 2000, 3).selected, products[2]);
assert.deepEqual(buildCatalog(products, 999, 99), {
  names: [],
  selected: undefined,
  hasAffordable: false,
  allPriced: true,
});
assert.deepEqual(buildCatalog([], 2000, 1), {
  names: [],
  selected: undefined,
  hasAffordable: false,
  allPriced: true,
});
assert.equal(
  buildCatalog([{ id: 4, name: '증정품', price: 0 }], 0, 4).allPriced,
  false,
);
assert.deepEqual(
  products.map((product) => product.id),
  [1, 2, 3],
);
assert.deepEqual(
  buildCatalog(
    [
      { id: 1, name: '10', price: 10 },
      { id: 2, name: '2', price: 2 },
    ],
    10,
    1,
  ).names,
  ['2', '10'],
);
console.log('메서드 선택, 가격 경계, 빈 배열, 미검색, 숫자 정렬, 원본 보존 검증 통과');

수정 방법

app.mjs

이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.

catalog.mjs

catalog.mjs에서 아래 차이가 있는 위치를 수정하세요. -는 삭제할 줄, +는 추가할 줄이며 표시는 실제 코드에 넣지 않습니다.

원본 배열을 보존하면서 필요한 상품을 고르고 새 결과 배열을 만듭니다.

--- 수정 전/catalog.mjs
+++ 수정 후/catalog.mjs
@@ -1,3 +1,10 @@
 export function buildCatalog(products, budget, wantedId) {
-  return { names: [], selected: undefined, hasAffordable: false, allPriced: false };
+  const affordable = products.filter((product) => product.price <= budget);
+  const sorted = [...affordable].sort((a, b) => a.price - b.price);
+  return {
+    names: sorted.map((product) => product.name),
+    selected: products.find((product) => product.id === wantedId),
+    hasAffordable: products.some((product) => product.price <= budget),
+    allPriced: products.every((product) => product.price > 0),
+  };
 }

수정 후 탭의 전체 파일과 비교한 뒤 본문의 확인 절차를 실행하세요.

verify.mjs

이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.

수정 후

app.mjs

import { buildCatalog } from './catalog.mjs';
const products = [
  { id: 1, name: '노트', price: 3000 },
  { id: 2, name: '연필', price: 1000 },
  { id: 3, name: '펜', price: 2000 },
];
console.log(buildCatalog(products, 2000, 3));

catalog.mjs

export function buildCatalog(products, budget, wantedId) {
  const affordable = products.filter((product) => product.price <= budget);
  const sorted = [...affordable].sort((a, b) => a.price - b.price);
  return {
    names: sorted.map((product) => product.name),
    selected: products.find((product) => product.id === wantedId),
    hasAffordable: products.some((product) => product.price <= budget),
    allPriced: products.every((product) => product.price > 0),
  };
}

verify.mjs

import assert from 'node:assert/strict';
import { buildCatalog } from './catalog.mjs';
const products = Object.freeze([
  Object.freeze({ id: 1, name: '노트', price: 3000 }),
  Object.freeze({ id: 2, name: '연필', price: 1000 }),
  Object.freeze({ id: 3, name: '펜', price: 2000 }),
]);
assert.deepEqual(buildCatalog(products, 2000, 3), {
  names: ['연필', '펜'],
  selected: products[2],
  hasAffordable: true,
  allPriced: true,
});
assert.equal(buildCatalog(products, 2000, 3).selected, products[2]);
assert.deepEqual(buildCatalog(products, 999, 99), {
  names: [],
  selected: undefined,
  hasAffordable: false,
  allPriced: true,
});
assert.deepEqual(buildCatalog([], 2000, 1), {
  names: [],
  selected: undefined,
  hasAffordable: false,
  allPriced: true,
});
assert.equal(
  buildCatalog([{ id: 4, name: '증정품', price: 0 }], 0, 4).allPriced,
  false,
);
assert.deepEqual(
  products.map((product) => product.id),
  [1, 2, 3],
);
assert.deepEqual(
  buildCatalog(
    [
      { id: 1, name: '10', price: 10 },
      { id: 2, name: '2', price: 2 },
    ],
    10,
    1,
  ).names,
  ['2', '10'],
);
console.log('메서드 선택, 가격 경계, 빈 배열, 미검색, 숫자 정렬, 원본 보존 검증 통과');

catalog.mjs 전체 코드 보기

기본 상품 가격은 노트 3000원, 연필 1000원, 펜 2000원입니다. 예산 2000원에서는 이름이 [“연필”, “펜”] 순서여야 합니다. node starter/verify.mjs로 직접 구현을 확인하고 node complete/app.mjs, node complete/verify.mjs로 완성 결과를 비교하세요.

빈 배열의 some은 false, every는 true입니다. 검사에서 어기는 요소가 없다는 뜻이며, 상품이 하나 이상 있다는 뜻은 아닙니다. 상품 존재도 필요하면 products.length > 0 && products.every(...)로 두 조건을 구분하세요. findIndex의 실패값 -1을 그대로 splice에 넘기면 마지막 요소를 지우므로 먼저 -1인지 확인합니다.

sort()의 기본 비교는 문자열 순서입니다. 숫자 [2, 10]은 비교 함수가 없으면 [10, 2]가 될 수 있으므로 (a, b) => a - b를 전달합니다. map에서 return을 빠뜨리면 생성된 배열 요소는 undefined가 됩니다. forEach의 반환값은 undefined이며 콜백의 return으로 반복 전체를 끝낼 수 없습니다.

최신 비변경 메서드는 실행 환경 지원을 확인하세요. 예를 들어 typeof [].toSorted === "function"으로 사용할 수 있는지 검사할 수 있습니다. 이 실습의 완성본은 복사 후 정렬을 사용합니다. MDN sortMDN flatMap에서 비교 함수와 평탄화 깊이를 확인할 수 있습니다.

시작 코드와 완성 코드 ZIP 내려받기

이 글이 도움이 되었나요?

조회 중

JavaScript 학습 순서

필수 13개 · 전체 14개

읽음 기록 관리

전체 과정 목차 (14개)
  1. 필수 학습 · JavaScript 조건문과 반복문: 변수 값의 흐름부터 추적하기
  2. 필수 학습 · JavaScript 함수와 객체, import export로 모듈 나누기
  3. 필수 학습 · JavaScript 배열 메서드: map filter forEach reduce 차이 현재 글
  4. 필수 학습 · JavaScript 객체 참조와 불변 갱신: 중첩 객체를 안전하게 바꾸기
  5. 필수 학습 · JavaScript reduce 사용법: 배열 누적 계산을 이해하는 기준
  6. 필수 학습 · JavaScript DOM 폼 만들기: 입력 검증과 접근성 처리
  7. 필수 학습 · JavaScript 투두리스트 만들기: 상태와 이벤트 위임으로 완성하기
  8. 필수 학습 · JavaScript URLSearchParams 사용법: URL 파라미터 읽고 수정하기
  9. 필수 학습 · JavaScript Date UTC KST 차이: 시간대 변환 기준 잡기
  10. 필수 학습 · JavaScript 실행 컨텍스트 기준: 스코프 호이스팅 클로저 연결하기
  11. 필수 학습 · JavaScript fetch 오류 처리: Promise부터 404까지
  12. 필수 학습 · JavaScript 이벤트 루프: Promise와 await 실행 순서 추적하기
  13. 필수 학습 · JavaScript 고급 비동기: AbortController와 최신 요청 경쟁 제어
  14. 선택 참고 · GSAP이 처음일 때 기초 사용법: 설치부터 기본 애니메이션까지

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기