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

[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 핵심 차이 비교
| 항목 | map | forEach |
|---|---|---|
| 반환값 | 새로운 배열 | 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
이게 이 글에서 가장 중요한 내용이다. forEach에 async 콜백을 넣으면 에러도 없이 조용히 실패한다.
// 잘못된 예 -- 에러 없이 빈 배열이 나온다
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를 쓰자. map과 forEach 둘 다 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이 답이다.
