forEach를 쓰지 말아야 하는 이유 - 더 나은 배열 메소드 선택하기
JavaScript 코드 리뷰를 받으면서 이런 피드백을 받아본 적 있나요?
// 리뷰어: "forEach 대신 map을 사용하세요"
const userNames = [];
users.forEach(user => {
userNames.push(user.name);
});
// "이게 뭐가 다르지? 둘 다 잘 동작하는데..."
처음에는 이해가 안 됐습니다. forEach나 map이나 결과는 같은데, 왜 굳이 map을 써야 할까요? 하지만 함수형 프로그래밍을 공부하고, 실무에서 복잡한 데이터 처리 로직을 다루면서 깨달았습니다. forEach는 생각보다 많은 문제를 일으킨다는 것을.
이 문서에서는 왜 forEach를 지양해야 하는지, 그리고 각 상황에서 어떤 배열 메소드를 사용해야 하는지를 실전 경험을 바탕으로 자세히 설명하겠습니다.
목차
- 왜 forEach를 지양해야 할까요?
- 먼저, 문제 상황을 보면서 시작해볼까요?
- forEach의 근본적인 문제점
- 상황별 올바른 배열 메소드 선택하기
- 실전에서 forEach 제거하기
- forEach를 써도 되는 예외 상황
- 함수형 프로그래밍 관점에서 본 배열 메소드
- 성능 비교와 고려사항
- 결론: 배열 메소드 선택 가이드
- 참고 자료
왜 forEach를 지양해야 할까요?
1. 의도가 불분명합니다
forEach는 “무엇을 하는지” 코드를 읽어야만 알 수 있습니다.
// ❌ forEach - 코드를 읽어봐야 알 수 있음
const results = [];
items.forEach(item => {
if (item.price > 100) {
results.push(item.name);
}
});
// ✅ filter + map - 한눈에 의도 파악 가능
const results = items
.filter(item => item.price > 100)
.map(item => item.name);
코드를 읽는 사람(미래의 당신 포함)은 forEach 블록 전체를 읽어야 “아, 필터링하고 변환하는구나”를 알 수 있습니다. 반면 filter와 map은 이름만 봐도 무엇을 하는지 명확합니다.
2. 반환값이 없어서 체이닝이 불가능합니다
// ❌ forEach는 undefined를 반환
const result = [1, 2, 3].forEach(x => x * 2);
console.log(result); // undefined
// ✅ map은 새 배열을 반환하여 체이닝 가능
const result = [1, 2, 3]
.map(x => x * 2)
.filter(x => x > 2)
.reduce((sum, x) => sum + x, 0);
함수형 프로그래밍의 핵심은 데이터 변환의 흐름을 명확히 표현하는 것입니다. forEach는 이 흐름을 끊어버립니다.
3. 부작용(Side Effect)을 유발하기 쉽습니다
forEach는 반환값이 없기 때문에, 외부 상태를 변경하는 것 외에는 할 수 있는 일이 없습니다.
// ❌ forEach - 외부 변수를 수정해야 함
let total = 0;
numbers.forEach(num => {
total += num; // 외부 변수 수정 (부작용)
});
// ✅ reduce - 순수 함수
const total = numbers.reduce((sum, num) => sum + num, 0);
외부 상태 변경은 디버깅을 어렵게 만들고, 예측 불가능한 버그를 일으킵니다.
4. 코드가 장황해집니다
// ❌ forEach - 7줄
const activeUsers = [];
users.forEach(user => {
if (user.isActive) {
activeUsers.push(user);
}
});
console.log(activeUsers);
// ✅ filter - 1줄
const activeUsers = users.filter(user => user.isActive);
간결함은 단순히 코드 줄 수를 줄이는 것이 아닙니다. 핵심 로직에 집중할 수 있게 해줍니다.
먼저, 문제 상황을 보면서 시작해볼까요?
시나리오: 전자상거래 장바구니 처리
온라인 쇼핑몰에서 장바구니의 총 금액을 계산하는 코드를 작성한다고 해봅시다.
접근 1: forEach 사용 (문제 많음)
// 장바구니 데이터
const cartItems = [
{ id: 1, name: '노트북', price: 1200000, quantity: 1, inStock: true },
{ id: 2, name: '마우스', price: 30000, quantity: 2, inStock: true },
{ id: 3, name: '키보드', price: 80000, quantity: 1, inStock: false },
];
// ❌ forEach로 구현 - 문제점이 많음
function calculateCartTotal(items) {
let total = 0;
let count = 0;
const availableItems = [];
items.forEach(item => {
if (item.inStock) {
const itemTotal = item.price * item.quantity;
total += itemTotal;
count += item.quantity;
availableItems.push(item.name);
}
});
return { total, count, items: availableItems };
}
const result = calculateCartTotal(cartItems);
문제점:
- 여러 개의 외부 변수를 수정 (
total,count,availableItems) - 하나의 루프에서 여러 가지 일을 동시에 처리
- 디버깅이 어려움 (어느 시점에 어떤 값이 변경되었는지 추적 곤란)
- 코드의 의도가 불명확
접근 2: 적절한 배열 메소드 조합 (권장)
// ✅ 명확한 배열 메소드 조합
function calculateCartTotal(items) {
const inStockItems = items.filter(item => item.inStock);
const total = inStockItems
.map(item => item.price * item.quantity)
.reduce((sum, price) => sum + price, 0);
const count = inStockItems
.map(item => item.quantity)
.reduce((sum, qty) => sum + qty, 0);
const itemNames = inStockItems.map(item => item.name);
return { total, count, items: itemNames };
}
const result = calculateCartTotal(cartItems);
개선된 점:
- ✅ 각 단계의 의도가 명확 (필터링 → 변환 → 누적)
- ✅ 외부 상태 변경 없음 (순수 함수)
- ✅ 중간 결과를 쉽게 확인 가능 (디버깅 용이)
- ✅ 각 연산을 독립적으로 테스트 가능
접근 3: 더 최적화된 버전
// 🎯 성능과 가독성을 모두 고려
function calculateCartTotal(items) {
return items
.filter(item => item.inStock)
.reduce((acc, item) => ({
total: acc.total + (item.price * item.quantity),
count: acc.count + item.quantity,
items: [...acc.items, item.name]
}), { total: 0, count: 0, items: [] });
}
const result = calculateCartTotal(cartItems);
장점:
- 한 번의 순회로 모든 계산 완료 (성능 최적화)
- 여전히 순수 함수 유지
- 체이닝으로 데이터 흐름이 명확
forEach의 근본적인 문제점
문제 1: 부작용(Side Effect)을 강제합니다
forEach는 반환값이 undefined이기 때문에, 유용한 일을 하려면 반드시 부작용을 일으켜야 합니다.
// forEach는 부작용 없이는 아무것도 할 수 없음
[1, 2, 3].forEach(x => x * 2); // 아무 일도 일어나지 않음
// 부작용을 일으켜야 함
const results = [];
[1, 2, 3].forEach(x => {
results.push(x * 2); // 외부 배열 수정 (부작용)
});
부작용이 왜 문제일까요?
실제 프로젝트에서 이런 버그를 경험한 적이 있습니다:
// 버그가 있는 코드 - forEach 사용
function processUserData(users) {
const processedUsers = [];
users.forEach(user => {
// 여러 조건 처리...
if (user.age >= 18) {
processedUsers.push(user);
}
// ... 100줄의 코드 ...
// 버그: 여기서 실수로 다시 push
if (user.isPremium) {
processedUsers.push(user); // 중복 추가!
}
});
return processedUsers;
}
100줄짜리 forEach 블록에서 같은 배열을 여러 곳에서 수정하다 보니, 중복 데이터가 들어간 버그를 발견하는 데 한참이 걸렸습니다.
// ✅ filter를 사용하면 이런 버그가 불가능
function processUserData(users) {
return users.filter(user =>
(user.age >= 18) || user.isPremium
);
}
문제 2: 함수형 프로그래밍 원칙 위배
함수형 프로그래밍의 핵심 원칙은 불변성(Immutability)과 순수 함수(Pure Function)입니다.
불변성 위반
// ❌ forEach - 원본 데이터 수정
const users = [
{ name: 'John', active: false },
{ name: 'Jane', active: false }
];
users.forEach(user => {
user.active = true; // 원본 객체 수정!
});
console.log(users); // 원본이 변경됨
// ✅ map - 새로운 데이터 생성
const activeUsers = users.map(user => ({
...user,
active: true
}));
console.log(users); // 원본 유지
console.log(activeUsers); // 새 배열
왜 불변성이 중요할까요?
React 같은 프레임워크를 사용하면서 이런 경험을 해보셨을 겁니다:
// React에서 흔한 실수
function TodoList() {
const [todos, setTodos] = useState([...]);
const toggleTodo = (id) => {
// ❌ 원본 수정 - React가 변경을 감지 못함!
const todo = todos.find(t => t.id === id);
todo.completed = !todo.completed;
setTodos(todos); // 리렌더링 안 됨!
// ✅ 새 배열 생성 - React가 변경 감지
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
}
순수 함수 위반
// ❌ 부작용이 있는 함수 (forEach 사용)
let globalCounter = 0;
function processItems(items) {
const results = [];
items.forEach(item => {
globalCounter++; // 전역 상태 수정 (부작용)
results.push(item * 2);
});
return results;
}
// 같은 입력, 다른 결과
console.log(processItems([1, 2])); // [2, 4], globalCounter: 2
console.log(processItems([1, 2])); // [2, 4], globalCounter: 4 (다름!)
// ✅ 순수 함수 (map 사용)
function processItems(items) {
return items.map(item => item * 2);
}
// 같은 입력, 항상 같은 결과
console.log(processItems([1, 2])); // [2, 4]
console.log(processItems([1, 2])); // [2, 4] (항상 같음)
문제 3: 코드의 의도를 숨깁니다
// ❌ forEach - 무엇을 하는지 모름
const result = [];
data.forEach(item => {
// 50줄의 복잡한 로직...
});
// result에 뭐가 들어있을까?
// ✅ map/filter - 이름이 의도를 말해줌
const validItems = data.filter(isValid);
const transformedItems = validItems.map(transform);
const total = transformedItems.reduce(sum, 0);
코드는 사람이 읽기 위한 것입니다. forEach는 “순회한다”는 것만 알려줄 뿐, 그 안에서 무슨 일이 일어나는지는 블록을 열어봐야 합니다.
문제 4: 조기 종료가 불가능합니다
// ❌ forEach - break, return이 동작하지 않음
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(num => {
if (num === 3) {
return; // forEach만 종료되는 것이 아니라, 현재 반복만 건너뜀
}
console.log(num); // 1, 2, 4, 5 모두 출력됨
});
// ✅ for...of - 정상적으로 break 가능
for (const num of numbers) {
if (num === 3) {
break; // 전체 루프 종료
}
console.log(num); // 1, 2만 출력
}
// ✅ some - 조건을 만족하면 종료
const found = numbers.some(num => {
console.log(num);
return num === 3; // 1, 2, 3만 출력되고 종료
});
상황별 올바른 배열 메소드 선택하기
각 배열 메소드는 특정 목적을 위해 설계되었습니다. 올바른 도구를 선택하면 코드가 명확하고, 안전하며, 효율적입니다.
1. 데이터 변환 - map 사용
배열의 각 요소를 변환하여 새로운 배열을 만들 때 사용합니다.
// 사용자 이름만 추출
const users = [
{ id: 1, name: 'John', age: 25 },
{ id: 2, name: 'Jane', age: 30 }
];
// ❌ forEach
const names = [];
users.forEach(user => {
names.push(user.name);
});
// ✅ map
const names = users.map(user => user.name);
실전 예제: API 응답 데이터 변환
// API에서 받은 데이터
const apiResponse = {
users: [
{ user_id: 1, user_name: 'John', user_email: 'john@example.com' },
{ user_id: 2, user_name: 'Jane', user_email: 'jane@example.com' }
]
};
// ❌ forEach - 장황함
const normalizedUsers = [];
apiResponse.users.forEach(user => {
normalizedUsers.push({
id: user.user_id,
name: user.user_name,
email: user.user_email
});
});
// ✅ map - 명확하고 간결
const normalizedUsers = apiResponse.users.map(user => ({
id: user.user_id,
name: user.user_name,
email: user.user_email
}));
// 🎯 더 나아가기: 재사용 가능한 변환 함수
const normalizeUser = user => ({
id: user.user_id,
name: user.user_name,
email: user.user_email
});
const normalizedUsers = apiResponse.users.map(normalizeUser);
2. 데이터 필터링 - filter 사용
조건을 만족하는 요소만 선택할 때 사용합니다.
const products = [
{ name: '노트북', price: 1200000, inStock: true },
{ name: '마우스', price: 30000, inStock: true },
{ name: '키보드', price: 80000, inStock: false }
];
// ❌ forEach
const availableProducts = [];
products.forEach(product => {
if (product.inStock) {
availableProducts.push(product);
}
});
// ✅ filter
const availableProducts = products.filter(product => product.inStock);
실전 예제: 복잡한 필터링 로직
// 여러 조건으로 필터링
const users = [
{ name: 'John', age: 25, isPremium: true, lastLogin: '2025-12-01' },
{ name: 'Jane', age: 17, isPremium: false, lastLogin: '2025-11-28' },
{ name: 'Bob', age: 30, isPremium: true, lastLogin: '2025-10-15' }
];
// ❌ forEach - 복잡하고 읽기 어려움
const activeUsers = [];
users.forEach(user => {
const isAdult = user.age >= 18;
const recentlyActive = new Date(user.lastLogin) > new Date('2025-11-01');
if (isAdult && (user.isPremium || recentlyActive)) {
activeUsers.push(user);
}
});
// ✅ filter - 조건이 명확
const activeUsers = users.filter(user => {
const isAdult = user.age >= 18;
const recentlyActive = new Date(user.lastLogin) > new Date('2025-11-01');
return isAdult && (user.isPremium || recentlyActive);
});
// 🎯 더 나아가기: 조건을 함수로 분리
const isAdult = user => user.age >= 18;
const isRecentlyActive = user =>
new Date(user.lastLogin) > new Date('2025-11-01');
const isEligible = user =>
isAdult(user) && (user.isPremium || isRecentlyActive(user));
const activeUsers = users.filter(isEligible);
3. 누적 계산 - reduce 사용
배열을 순회하며 하나의 값으로 축약할 때 사용합니다.
const orders = [
{ id: 1, amount: 10000 },
{ id: 2, amount: 25000 },
{ id: 3, amount: 15000 }
];
// ❌ forEach
let total = 0;
orders.forEach(order => {
total += order.amount;
});
// ✅ reduce
const total = orders.reduce((sum, order) => sum + order.amount, 0);
실전 예제: 복잡한 데이터 구조 생성
const users = [
{ id: 1, name: 'John', role: 'admin' },
{ id: 2, name: 'Jane', role: 'user' },
{ id: 3, name: 'Bob', role: 'admin' }
];
// ❌ forEach - ID를 키로 하는 맵 생성
const userMap = {};
users.forEach(user => {
userMap[user.id] = user;
});
// ✅ reduce - 의도가 명확
const userMap = users.reduce((map, user) => {
map[user.id] = user;
return map;
}, {});
// 🎯 역할별 그룹화
const usersByRole = users.reduce((groups, user) => {
const role = user.role;
if (!groups[role]) {
groups[role] = [];
}
groups[role].push(user);
return groups;
}, {});
// 결과: { admin: [John, Bob], user: [Jane] }
4. 조건 확인 - some/every 사용
하나라도 조건을 만족하는지 또는 모두 만족하는지 확인할 때 사용합니다.
const products = [
{ name: '노트북', price: 1200000 },
{ name: '마우스', price: 30000 },
{ name: '키보드', price: 80000 }
];
// ❌ forEach - 불필요하게 전체 순회
let hasExpensive = false;
products.forEach(product => {
if (product.price > 1000000) {
hasExpensive = true;
}
});
// ✅ some - 조건 만족하면 즉시 종료
const hasExpensive = products.some(product => product.price > 1000000);
// ✅ every - 모든 제품이 재고 있는지
const allInStock = products.every(product => product.stock > 0);
실전 예제: 폼 유효성 검사
const formFields = [
{ name: 'email', value: 'test@example.com', valid: true },
{ name: 'password', value: '', valid: false },
{ name: 'age', value: '25', valid: true }
];
// ❌ forEach
let hasError = false;
formFields.forEach(field => {
if (!field.valid) {
hasError = true;
}
});
// ✅ some - 하나라도 유효하지 않은 필드가 있는지
const hasError = formFields.some(field => !field.valid);
// ✅ every - 모든 필드가 유효한지
const isFormValid = formFields.every(field => field.valid);
5. 검색 - find/findIndex 사용
특정 조건을 만족하는 첫 번째 요소를 찾을 때 사용합니다.
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
{ id: 3, name: 'Bob' }
];
// ❌ forEach - 찾아도 계속 순회
let foundUser = null;
users.forEach(user => {
if (user.id === 2) {
foundUser = user;
}
});
// ✅ find - 찾으면 즉시 종료
const foundUser = users.find(user => user.id === 2);
// ✅ findIndex - 인덱스 찾기
const index = users.findIndex(user => user.id === 2);
실전에서 forEach 제거하기
실제 프로젝트 코드에서 forEach를 제거하는 패턴들을 살펴봅시다.
패턴 1: 배열 변환 + 누적
// ❌ forEach - 변환과 누적을 동시에
let totalPrice = 0;
const processedItems = [];
cartItems.forEach(item => {
const discountedPrice = item.price * 0.9;
processedItems.push({
...item,
finalPrice: discountedPrice
});
totalPrice += discountedPrice * item.quantity;
});
// ✅ map + reduce - 각 단계가 명확
const processedItems = cartItems.map(item => ({
...item,
finalPrice: item.price * 0.9
}));
const totalPrice = processedItems.reduce(
(sum, item) => sum + (item.finalPrice * item.quantity),
0
);
// 🎯 더 간결하게: 한 번의 reduce로
const { items: processedItems, total: totalPrice } = cartItems.reduce(
(acc, item) => {
const finalPrice = item.price * 0.9;
return {
items: [...acc.items, { ...item, finalPrice }],
total: acc.total + (finalPrice * item.quantity)
};
},
{ items: [], total: 0 }
);
패턴 2: 조건부 필터링 + 변환
// ❌ forEach - 조건과 변환이 뒤섞임
const results = [];
users.forEach(user => {
if (user.age >= 18 && user.isActive) {
results.push({
id: user.id,
displayName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase()
});
}
});
// ✅ filter + map - 각 단계가 명확
const results = users
.filter(user => user.age >= 18 && user.isActive)
.map(user => ({
id: user.id,
displayName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase()
}));
패턴 3: 그룹화
// ❌ forEach - 복잡한 그룹화 로직
const groupedByCategory = {};
products.forEach(product => {
const category = product.category;
if (!groupedByCategory[category]) {
groupedByCategory[category] = [];
}
groupedByCategory[category].push(product);
});
// ✅ reduce - 의도가 명확
const groupedByCategory = products.reduce((groups, product) => {
const category = product.category;
return {
...groups,
[category]: [...(groups[category] || []), product]
};
}, {});
// 🎯 재사용 가능한 유틸리티 함수로
const groupBy = (array, key) => {
return array.reduce((groups, item) => {
const value = item[key];
return {
...groups,
[value]: [...(groups[value] || []), item]
};
}, {});
};
const groupedByCategory = groupBy(products, 'category');
패턴 4: 데이터 정규화
// API 응답을 정규화하는 실제 시나리오
const apiResponse = [
{
post_id: 1,
post_title: 'Hello',
author: { author_id: 10, author_name: 'John' },
tags: ['javascript', 'web']
},
{
post_id: 2,
post_title: 'World',
author: { author_id: 20, author_name: 'Jane' },
tags: ['react', 'web']
}
];
// ❌ forEach - 복잡하고 읽기 어려움
const posts = {};
const authors = {};
const allTags = new Set();
apiResponse.forEach(post => {
posts[post.post_id] = {
id: post.post_id,
title: post.post_title,
authorId: post.author.author_id
};
authors[post.author.author_id] = {
id: post.author.author_id,
name: post.author.author_name
};
post.tags.forEach(tag => allTags.add(tag));
});
// ✅ reduce - 구조화되고 명확
const normalized = apiResponse.reduce(
(acc, post) => ({
posts: {
...acc.posts,
[post.post_id]: {
id: post.post_id,
title: post.post_title,
authorId: post.author.author_id
}
},
authors: {
...acc.authors,
[post.author.author_id]: {
id: post.author.author_id,
name: post.author.author_name
}
},
tags: new Set([...acc.tags, ...post.tags])
}),
{ posts: {}, authors: {}, tags: new Set() }
);
forEach를 써도 되는 예외 상황
모든 규칙에는 예외가 있습니다. 다음 경우에는 forEach를 사용해도 괜찮습니다 (하지만 for...of가 더 나을 수 있습니다).
1. 순수한 부작용이 목적인 경우
외부 시스템과의 상호작용이나 로깅 등, 부작용 자체가 목적일 때입니다.
// DOM 조작 (하지만 for...of가 더 명확)
// ⚠️ forEach 사용 가능
buttons.forEach(button => {
button.addEventListener('click', handleClick);
});
// 🎯 더 나은 방법: for...of
for (const button of buttons) {
button.addEventListener('click', handleClick);
}
// 로깅
users.forEach(user => {
console.log(`Processing user: ${user.name}`);
logToAnalytics('user_processed', { userId: user.id });
});
// 🎯 하지만 이것도 for...of가 더 명확
for (const user of users) {
console.log(`Processing user: ${user.name}`);
logToAnalytics('user_processed', { userId: user.id });
}
2. 비동기 작업 (하지만 주의 필요)
// ⚠️ forEach는 Promise를 기다리지 않음!
users.forEach(async user => {
await sendEmail(user.email, message);
});
// 모든 이메일이 전송되기 전에 다음 코드가 실행됨
// ✅ Promise.all + map 사용
await Promise.all(
users.map(user => sendEmail(user.email, message))
);
// ✅ 순차 실행이 필요하면 for...of
for (const user of users) {
await sendEmail(user.email, message);
}
실제로 경험한 버그:
// 버그가 있는 코드
async function processOrders(orders) {
orders.forEach(async order => {
await updateInventory(order);
await sendConfirmation(order);
});
console.log('All done!'); // 너무 빨리 실행됨!
return 'success';
}
// ✅ 올바른 코드
async function processOrders(orders) {
for (const order of orders) {
await updateInventory(order);
await sendConfirmation(order);
}
console.log('All done!'); // 모든 작업 완료 후 실행
return 'success';
}
3. 성능이 극도로 중요한 경우
매우 큰 배열(수백만 개)을 처리할 때는 일반 for 루프가 가장 빠릅니다.
const hugeArray = new Array(10000000).fill(0).map((_, i) => i);
// 가장 빠름: for 루프
console.time('for');
let sum1 = 0;
for (let i = 0; i < hugeArray.length; i++) {
sum1 += hugeArray[i];
}
console.timeEnd('for'); // ~40ms
// 중간: for...of
console.time('for...of');
let sum2 = 0;
for (const num of hugeArray) {
sum2 += num;
}
console.timeEnd('for...of'); // ~45ms
// 느림: forEach
console.time('forEach');
let sum3 = 0;
hugeArray.forEach(num => sum3 += num);
console.timeEnd('forEach'); // ~50ms
// 가장 느림: reduce
console.time('reduce');
const sum4 = hugeArray.reduce((sum, num) => sum + num, 0);
console.timeEnd('reduce'); // ~60ms
하지만 대부분의 경우 이 정도 차이는 무시할 수 있습니다. 가독성과 유지보수성이 더 중요합니다.
함수형 프로그래밍 관점에서 본 배열 메소드
함수형 프로그래밍의 핵심 개념들을 이해하면, 왜 forEach를 지양해야 하는지 더 명확해집니다.
순수 함수 (Pure Function)
같은 입력에 항상 같은 출력을 반환하고, 부작용이 없는 함수입니다.
// ❌ 순수하지 않은 함수 (forEach 사용)
let total = 0;
const addToTotal = (numbers) => {
numbers.forEach(num => {
total += num; // 외부 상태 수정 (부작용)
});
};
addToTotal([1, 2, 3]); // total = 6
addToTotal([1, 2, 3]); // total = 12 (같은 입력, 다른 결과!)
// ✅ 순수 함수 (reduce 사용)
const sum = (numbers) => {
return numbers.reduce((acc, num) => acc + num, 0);
};
sum([1, 2, 3]); // 6
sum([1, 2, 3]); // 6 (항상 같은 결과)
순수 함수의 장점:
- 테스트하기 쉽움
- 예측 가능함
- 병렬 처리 가능
- 메모이제이션 가능
불변성 (Immutability)
데이터를 변경하지 않고, 새로운 데이터를 생성합니다.
// ❌ 가변 (forEach로 원본 수정)
const updatePrices = (products, increaseBy) => {
products.forEach(product => {
product.price += increaseBy; // 원본 수정!
});
return products;
};
const original = [{ name: 'A', price: 100 }];
const updated = updatePrices(original, 10);
console.log(original === updated); // true (같은 객체!)
console.log(original[0].price); // 110 (원본이 변경됨!)
// ✅ 불변 (map으로 새 배열 생성)
const updatePrices = (products, increaseBy) => {
return products.map(product => ({
...product,
price: product.price + increaseBy
}));
};
const original = [{ name: 'A', price: 100 }];
const updated = updatePrices(original, 10);
console.log(original === updated); // false (다른 객체)
console.log(original[0].price); // 100 (원본 유지)
console.log(updated[0].price); // 110 (새 객체)
함수 합성 (Function Composition)
작은 함수들을 조합하여 복잡한 기능을 만듭니다.
// 작은 순수 함수들
const isActive = user => user.isActive;
const isAdult = user => user.age >= 18;
const toUpperCase = str => str.toUpperCase();
const getUserName = user => user.name;
// ❌ forEach - 합성이 어려움
const getActiveAdultNames = (users) => {
const names = [];
users.forEach(user => {
if (isActive(user) && isAdult(user)) {
names.push(toUpperCase(getUserName(user)));
}
});
return names;
};
// ✅ filter + map - 함수 합성이 자연스러움
const getActiveAdultNames = (users) => {
return users
.filter(isActive)
.filter(isAdult)
.map(getUserName)
.map(toUpperCase);
};
// 🎯 더 나아가기: 범용 유틸리티
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const processUser = pipe(
users => users.filter(isActive),
users => users.filter(isAdult),
users => users.map(getUserName),
names => names.map(toUpperCase)
);
const result = processUser(users);
성능 비교와 고려사항
벤치마크 테스트
다양한 크기의 배열에서 성능을 측정해봤습니다.
// 테스트 데이터
const smallArray = Array.from({ length: 100 }, (_, i) => i);
const mediumArray = Array.from({ length: 10000 }, (_, i) => i);
const largeArray = Array.from({ length: 1000000 }, (_, i) => i);
// 테스트 함수
function benchmarkSum(arr, method) {
console.time(method);
let result;
switch(method) {
case 'for':
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
result = sum;
break;
case 'for...of':
let sum2 = 0;
for (const num of arr) {
sum2 += num;
}
result = sum2;
break;
case 'forEach':
let sum3 = 0;
arr.forEach(num => sum3 += num);
result = sum3;
break;
case 'reduce':
result = arr.reduce((sum, num) => sum + num, 0);
break;
}
console.timeEnd(method);
return result;
}
// 큰 배열에서 테스트
console.log('Large Array (1,000,000 items):');
benchmarkSum(largeArray, 'for'); // ~40ms
benchmarkSum(largeArray, 'for...of'); // ~45ms
benchmarkSum(largeArray, 'forEach'); // ~50ms
benchmarkSum(largeArray, 'reduce'); // ~60ms
성능 순위:
for- 가장 빠름 (하지만 가독성이 떨어짐)for...of- 빠르면서도 읽기 쉬움forEach- 중간 (부작용 필요)reduce- 느림 (하지만 함수형)
실무에서의 성능 고려사항
대부분의 경우 성능 차이는 무시할 수 있습니다:
// 일반적인 웹 애플리케이션의 배열 크기
const typicalData = Array.from({ length: 100 }, (_, i) => i);
// 100개 요소에서의 차이
benchmarkSum(typicalData, 'reduce'); // ~0.1ms
benchmarkSum(typicalData, 'forEach'); // ~0.08ms
// 차이: 0.02ms = 무시 가능!
성능보다 중요한 것:
- 코드 가독성
- 유지보수성
- 버그 가능성 감소
- 팀원들의 이해도
성능이 정말 중요한 경우에만 for 사용:
- 실시간 데이터 처리
- 게임 엔진의 렌더링 루프
- 수백만 개 이상의 데이터
- 초당 수천 번 호출되는 함수
메모리 사용량
const data = Array.from({ length: 1000000 }, (_, i) => ({ id: i, value: i * 2 }));
// ❌ forEach - 중간 배열 필요
const results1 = [];
data.forEach(item => {
if (item.value > 100) {
results1.push(item.id);
}
});
// ✅ filter + map - 두 개의 중간 배열 생성
const results2 = data
.filter(item => item.value > 100)
.map(item => item.id);
// 🎯 최적화: reduce로 한 번만 순회
const results3 = data.reduce((acc, item) => {
if (item.value > 100) {
acc.push(item.id);
}
return acc;
}, []);
// 🚀 최고 성능: for 루프
const results4 = [];
for (let i = 0; i < data.length; i++) {
if (data[i].value > 100) {
results4.push(data[i].id);
}
}
권장사항:
- 일반적인 경우:
filter+map(가독성 우선) - 매우 큰 배열:
reduce또는for(성능 우선) - 실시간 처리:
for(최고 성능)
결론: 배열 메소드 선택 가이드
빠른 결정 트리
데이터를 변환하나요?
├─ 예 → map 사용
└─ 아니오 → 다음으로
조건에 맞는 것만 선택하나요?
├─ 예 → filter 사용
└─ 아니오 → 다음으로
하나의 값으로 누적하나요?
├─ 예 → reduce 사용
└─ 아니오 → 다음으로
조건을 확인만 하나요?
├─ 하나라도 만족? → some 사용
├─ 모두 만족? → every 사용
└─ 특정 요소 찾기? → find 사용
부작용이 목적인가요?
├─ 예 → for...of 사용 (forEach보다 명확)
└─ 성능이 극도로 중요? → for 루프 사용
핵심 원칙
forEach는 최후의 수단- 다른 메소드로 표현 가능하면 그것을 사용
- 부작용이 목적이라면
for...of가 더 명확
- 의도를 명확하게
map: “변환합니다”filter: “선택합니다”reduce: “누적합니다”forEach: “…뭔가 합니다?” (불명확!)
- 함수형 우선, 성능은 나중에
- 먼저 읽기 쉬운 코드 작성
- 성능 문제가 실제로 발생하면 최적화
- 대부분의 경우 차이를 느낄 수 없음
- 순수 함수와 불변성
- 외부 상태 수정 지양
- 새로운 데이터 생성
- 테스트와 디버깅이 쉬워짐
실전 체크리스트
코드 리뷰 시 확인할 항목:
forEach대신map/filter/reduce를 쓸 수 있나?- 외부 변수를 수정하고 있나?
- 반환값이 필요한가?
- 체이닝이 가능하면 더 명확한가?
- 조기 종료가 필요한가? (
some/every/find) - 부작용이 목적이라면
for...of가 더 명확한가?
마지막 조언
forEach를 완전히 금지할 필요는 없습니다. 하지만 forEach를 사용할 때마다 “더 나은 방법이 있지 않을까?”를 자문해보세요.
처음에는 map, filter, reduce가 낯설 수 있습니다. 하지만 익숙해지면, 코드가 더 읽기 쉽고, 더 안전하며, 더 유지보수하기 쉬워진다는 것을 느낄 수 있을 겁니다.
함수형 프로그래밍은 단순히 “멋있어 보이는” 코드를 작성하는 것이 아닙니다. 더 적은 버그, 더 쉬운 테스트, 더 명확한 의도를 가진 코드를 작성하는 실용적인 방법입니다. 🎯
참고 자료
MDN 공식 문서
함수형 프로그래밍 학습
- Functional Programming in JavaScript - 함수형 프로그래밍 입문서
- You Don’t Know JS - ES6 & Beyond - ES6+ 기능 심화
- JavaScript Array Methods Guide
도구와 라이브러리
- ESLint - functional rules - 함수형 프로그래밍 규칙 강제
- Ramda - 함수형 유틸리티 라이브러리
- Lodash/FP - Lodash의 함수형 버전
관련 문서
- array-like-objects.md - 유사 배열 객체 다루기
- mutation-vs-non-mutation.md - 불변성의 중요성
댓글