JavaScript map vs forEach — 뭐가 다르고 언제 쓸까

map vs forEach 질문은 코드 리뷰에서 자주 나온다. 둘 다 배열을 순회한다. 둘 다 콜백 함수를 받는다. 그런데 용도가 완전히 다르다. 잘못 쓰면 코드가 동작하긴 하지만 의도한 대로 동작하지 않는다. 특히 async/await와 함께 쓸 때는 에러도 없이 조용히 실패하는 경우가 생긴다.

이 글에서는 mapforEach의 차이를 코드로 비교하고, 언제 뭘 써야 하는지 판단 기준을 정리한다.

JavaScript Logo

[Array. prototype. map](https://developer. mozilla. org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map)은 원본 배열의 각 요소를 변환해서 새로운 배열을 반환한다. 원본 배열은 바뀌지 않는다.

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

console. log(doubled); // [2, 4, 6]
console. log(numbers); // [1, 2, 3] -- 원본 그대로

반환값이 있어서 체이닝이 가능하다. 이게 forEach와 가장 크게 다른 점이다.

// filter -> map -> sort 체이닝
const activeUserNames = users. filter(user => user. isActive). map(user => user. name). sort();

map은 데이터를 변환할 때 쓴다. 배열의 각 요소를 다른 형태로 바꿔서 새 배열을 만드는 것이 목적이다.

forEach는 뭘 하는 메서드인가

[Array. prototype. forEach](https://developer. mozilla. org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)는 배열을 순회하면서 각 요소에 대해 작업을 실행한다. 반환값은 항상 undefined다.

const numbers = [1, 2, 3];
numbers. forEach(n => console. log(n));
// 1
// 2
// 3

체이닝이 불가능하다. forEach의 반환값이 undefined라서 뒤에 . map()이나 . filter()를 이을 수 없다.

forEach는 부수 효과를 실행할 때 쓴다. DOM 업데이트, 로깅, API 호출처럼 반환값 없이 “작업을 실행”하는 것이 목적인 경우다.

map과 forEach 핵심 차이 비교

항목mapforEach
반환값새로운 배열undefined
원본 배열변경 없음변경 없음
체이닝가능불가능
주 용도데이터 변환부수 효과 실행
중간 중단break 불가break 불가

TypeScript 타입 시그니처를 보면 이 차이가 더 명확하다.

// map: (value: T, index: number, array: T[]) => U -- 변환된 값 반환
// forEach: (value: T, index: number, array: T[]) => void -- 반환값 없음

forEach의 반환 타입은 void다. 이 사실이 뒤에서 설명할 async/await 함정의 근본 원인이다.

흔한 실수: map을 forEach처럼 쓰는 경우

map의 반환값을 무시하고 forEach처럼 쓰는 경우가 있다.

// 잘못된 예 -- 반환값 무시
const numbers = [1, 2, 3];
numbers. map(n => console. log(n));

// 올바른 예
numbers. forEach(n => console. log(n));

반대로 forEach로 새 배열을 만들려고 push를 쓰는 경우도 있다.

// 잘못된 예 -- map을 직접 쓰면 된다
const doubled = [];
numbers. forEach(n => doubled. push(n * 2));

// 올바른 예
const doubled = numbers. map(n => n * 2);

ESLint [array-callback-return](https://eslint. org/docs/latest/rules/array-callback-return) 규칙을 켜두면 map의 반환값을 빠뜨리는 실수를 자동으로 잡아준다.

async/await를 쓸 때는 map + Promise. all

이게 이 글에서 가장 중요한 내용이다. forEachasync 콜백을 넣으면 에러도 없이 조용히 실패한다.

// 잘못된 예 -- 에러 없이 빈 배열이 나온다
const results = [];
await items. forEach(async (item) => {
 const res = await fetchData(item); // 완료를 보장하지 않는다
 results. push(res);
});

console. log(results); // [] -- 빈 배열

왜 안 될까. forEach의 반환 타입은 void다. async 콜백이 Promise를 돌려줘도 forEach가 그걸 받지 않는다. await items. forEach(...)는 아무 의미가 없다. forEach는 콜백이 끝나기를 기다리지 않고 다음 항목으로 넘어간다.

비동기 배열 처리에는 두 가지 패턴이 있다.

순서가 중요한 경우: for... of + await

// 올바른 예 -- 순차 실행
const results = [];
for (const item of items) {
 const res = await fetchData(item);
 results. push(res);
}
console. log(results); // 채워진 배열

순서가 상관없고 빠르게 처리하고 싶은 경우: map + Promise. all

// 올바른 예 -- 병렬 실행
const results = await Promise. all(
 items. map(item => fetchData(item))
);
console. log(results); // 채워진 배열, 더 빠르다

Promise. all + map 조합은 요청을 동시에 보내고 전부 끝날 때까지 기다린다. 3개 요청이 각각 1초씩 걸리면 총 1초로 끝난다. for... of 방식은 순차 실행이라 3초가 걸린다.

forEach에 async를 붙이는 실수는 [async/await에서 자주 하는 실수 5가지](https://xnullbyte. com/async-await-common-mistakes/)에서 더 자세히 다루고 있다.

방법동작실행 방식언제 쓰나
forEach + async제대로 안 됨쓰지 마라
for... of + await순차 실행하나씩 기다림순서가 중요할 때
map + Promise. all병렬 실행동시에 요청독립적인 작업일 때

map vs forEach 언제 뭘 쓸까

판단 기준은 단순하다.

변환된 새 배열이 필요한가? 그렇다면 map이다.

// map -- 새 배열이 필요한 경우
const prices = products. map(p => p. price * 1.1); // 10% 인상된 가격 배열
const names = users. map(u => u. name); // 이름 배열 추출
const jsxList = items. map(item => <Item key={item. id} {... item} />); // React JSX

부수 효과를 실행하고 반환값이 필요 없는가? 그렇다면 forEach다.

// forEach -- 반환값 없이 작업만 실행하는 경우
users. forEach(user => sendEmail(user. email));
items. forEach(item => console. log(item));
elements. forEach(el => el. classList. add('active'));

중간에 멈춰야 하는가? for... of를 쓰자. mapforEach 둘 다 break가 안 된다.

// for... of -- 특정 조건에서 중단이 필요한 경우
for (const item of items) {
 if (item. isBlocked) break;
 process(item);
}

자주 묻는 질문

성능 차이가 있나요?

일반적인 데이터 크기에서는 무시할 수 있는 수준이다. 수십만 개 이상의 데이터를 다룰 때는 벤치마크를 직접 측정하자. 성능보다 코드 의도를 명확하게 하는 쪽이 더 중요하다.

map으로 forEach를 대체할 수 있나요?

가능하지만 의미상 맞지 않다. map은 “변환”, forEach는 “부수 효과 실행”이다. 반환값을 무시하는 map 사용은 코드의 의도를 흐린다. ESLint array-callback-return 규칙도 이를 경고한다.

둘 다 중간에 멈출 수 없다면 어떻게 하나요?

for... of를 쓰자. break가 된다. 특정 조건의 첫 번째 요소를 찾는다면 Array. find()Array. some()이 더 명확하다.

// 첫 번째 매칭 요소 찾기
const found = items. find(item => item. id === targetId);

// 하나라도 조건 만족하면 true
const hasAdmin = users. some(user => user. role === 'admin');

forEach에 async를 쓰면 어떻게 되나요?

에러 없이 빈 배열이 나온다. forEach는 async 콜백의 Promise를 기다리지 않는다. 비동기 배열 처리는 for... of + await(순차) 또는 map + Promise. all(병렬)을 써야 한다.

한줄 정리: 변환된 새 배열이 필요하면 map, 부수 효과를 실행할 때는 forEach다. async와 함께 쓸 때는 forEach를 절대 쓰지 않는다. for... of 또는 map + Promise. all이 답이다.

댓글 남기기