자바스크립트로 개발하다 보면 배열을 다룰 일이 정말 많죠? map
, filter
같은 메서드는 이제 익숙하실 텐데요, 혹시 reduce
메서드는 어떠신가요? 뭔가 이름부터 '줄인다'는 느낌인데, 처음엔 살짝 헷갈릴 수도 있어요. 저도 그랬거든요! 😉
하지만 reduce
를 제대로 알고 나면 정말 강력하고 유용하다는 걸 깨닫게 될 거예요. 배열 데이터를 원하는 형태로 '요리조리' 가공해서 딱 하나의 결과 값으로 만들어낼 수 있거든요. 오늘은 이 매력적인 reduce
메서드에 대해 쉽고 재미있게 알아보려고 해요. 특히 '초기값'을 넣고 안 넣고에 따라 동작이 어떻게 달라지는지 확실히 파헤쳐 보자고요!
reduce
메서드는 이름 그대로 배열의 요소들을 하나의 값으로 줄여나가는 역할을 해요. 배열을 순회하면서 우리가 정의한 콜백 함수를 각 요소에 적용하고, 그 결과를 누적해서 최종 결과값을 만들어내는 거죠.
기본적인 사용법은 이렇답니다.
const 최종결과 = 배열.reduce(콜백함수, 초기값);
여기서 중요한 건 콜백함수와 초기값인데요.
accumulator
(누적값, 줄여서 acc
) - 이전 콜백 호출의 반환값이에요. 계속 누적되는 값이죠.currentValue
(현재값, 줄여서 cur
) - 현재 처리 중인 배열 요소예요.currentIndex
(현재 인덱스, 줄여서 idx
) - 현재 처리 중인 배열 요소의 인덱스고요 (선택적).array
(원본 배열, 줄여서 src
) - reduce
가 호출된 배열 자체예요 (선택적).initialValue
) - 이게 정말 중요해요! accumulator
가 맨 처음 어떤 값으로 시작할지를 정해주는 거예요. 이건 선택사항인데, 있고 없고에 따라 동작 방식이 달라져요 (이건 뒤에서 자세히!).제일 간단한 예시! 숫자 배열의 합 구하기
가장 흔하게 볼 수 있는 예시죠? 배열 안의 숫자들을 모두 더하는 거예요.
const numbers = [1, 2, 3, 4];
// acc는 누적 합계, cur는 현재 숫자
// 초기값으로 0을 줬어요.
const sum = numbers.reduce((acc, cur) => {
console.log(`acc 상태 ${acc}, 현재 값 ${cur}`); // 어떻게 동작하는지 볼까요?
return acc + cur;
}, 0); // 초기값은 0!
console.log('최종 합계', sum);
// 출력 예상
// acc 상태 0, 현재 값 1
// acc 상태 1, 현재 값 2
// acc 상태 3, 현재 값 3
// acc 상태 6, 현재 값 4
// 최종 합계 10
로그를 보면 acc
가 어떻게 변하는지 감이 오시죠? acc
에 cur
를 계속 더해서 다음 acc
로 넘겨주는 거예요. 초기값 0부터 시작해서요.
조금 더 응용해볼까요? 객체 배열 다루기
숫자 배열만 다루면 재미없죠! 객체 배열에서 특정 속성 값만 쏙쏙 뽑아서 합계를 낼 수도 있어요. 예를 들어 장바구니 상품 목록에서 총가격을 계산하는 거죠.
const products = [
{ name: '사과', price: 2000 },
{ name: '바나나', price: 1000 },
{ name: '체리', price: 1500 },
];
// 이번에도 초기값은 0
const totalPrice = products.reduce((acc, cur) => {
console.log(`누적 금액 ${acc}, 현재 상품 가격 ${cur.price}`);
return acc + cur.price; // 현재 상품(cur)의 price만 누적(acc)해요.
}, 0);
console.log('총 상품 가격', totalPrice); // 결과는 4500이 나오겠죠?
// 출력 예상
// 누적 금액 0, 현재 상품 가격 2000
// 누적 금액 2000, 현재 상품 가격 1000
// 누적 금액 3000, 현재 상품 가격 1500
// 총 상품 가격 4500
어때요? reduce
의 기본 감각, 조금 잡히셨나요? 😊
자, 이제 reduce
의 핵심 중 하나인 초기값에 대해 이야기해 볼게요. 이 초기값을 주느냐, 안 주느냐에 따라 reduce
의 행동이 꽤 달라지거든요. 이게 헷갈리는 포인트가 될 수 있어요!
reduce
함수의 두 번째 인자로 초기값을 넘겨주면, 어떤 일이 벌어질까요?
accumulator
의 시작 - 우리가 넘겨준 초기값이 accumulator
의 첫 번째 값이 돼요.index 0
)부터 순회를 시작해요.코드로 직접 확인해 볼까요?
아까 숫자 합계 예제에서 초기값을 10으로 바꿔볼게요.
const numbers = [1, 2, 3, 4];
// 초기값을 10으로 설정했어요!
const sumWithInitial = numbers.reduce((acc, cur) => {
console.log(`acc 상태 ${acc}, 현재 값 ${cur}`);
return acc + cur;
}, 10); // 초기값 10
console.log('초기값 10 포함 합계', sumWithInitial); // 10 + 1 + 2 + 3 + 4 = 20
// 출력 예상
// acc 상태 10, 현재 값 1
// acc 상태 11, 현재 값 2
// acc 상태 13, 현재 값 3
// acc 상태 16, 현재 값 4
// 초기값 10 포함 합계 20
결과가 20이 되었죠? acc
가 0이 아니라 10부터 시작했기 때문이에요. 그리고 배열의 첫 번째 요소인 1부터 순회가 시작된 걸 볼 수 있어요.
더 복잡한 활용 - 초기값을 객체로!
초기값을 꼭 숫자나 문자열로 줄 필요는 없어요. 객체나 배열을 줄 수도 있답니다! 이걸 이용하면 더 재미있는 일을 할 수 있어요. 예를 들어 상품 목록에서 총 가격과 상품 개수를 동시에 계산해 볼까요?
const products = [
{ name: '사과', price: 2000 },
{ name: '바나나', price: 1000 },
{ name: '체리', price: 1500 },
];
// 초기값으로 객체를 설정! { 총가격: 0, 개수: 0 }
const summary = products.reduce(
(acc, cur) => {
console.log(`현재 acc ${JSON.stringify(acc)}, 현재 상품 ${cur.name}`);
acc.totalPrice += cur.price; // acc 객체의 totalPrice 속성에 가격 누적
acc.count += 1; // acc 객체의 count 속성에 개수 누적
return acc; // 다음 순회를 위해 수정된 acc 객체를 반환! 중요해요!
},
{ totalPrice: 0, count: 0 } // 초기값 객체
);
console.log('최종 요약', summary); // 결과는 { totalPrice: 4500, count: 3 }
// 출력 예상
// 현재 acc {"totalPrice":0,"count":0}, 현재 상품 사과
// 현재 acc {"totalPrice":2000,"count":1}, 현재 상품 바나나
// 현재 acc {"totalPrice":3000,"count":2}, 현재 상품 체리
// 최종 요약 { totalPrice: 4500, count: 3 }
이렇게 초기값으로 객체를 주면 여러 값을 한 번에 계산해서 깔끔한 객체로 만들 수 있어요. 정말 유용하죠?
⭐ 초기값 설정의 장점
acc
가 무엇으로 시작하는지 바로 알 수 있죠.reduce
를 사용해도 에러가 나지 않아요! 빈 배열에 초기값을 주면, 그 초기값이 그냥 결과로 반환돼요. 안전하죠!만약 reduce
에 두 번째 인자인 초기값을 안 주면 어떻게 될까요?
accumulator
의 시작 - 배열의 첫 번째 요소(index 0
)가 accumulator
의 초기값이 돼요.index 1
)부터 순회를 시작해요. 첫 번째 요소는 acc
초기값으로 사용되었으니까요!코드로 바로 확인해 봅시다!
숫자 합계 예제에서 초기값을 빼 볼게요.
const numbers = [1, 2, 3, 4];
// 초기값 없이 reduce 호출!
const sumWithoutInitial = numbers.reduce((acc, cur) => {
console.log(`acc 상태 ${acc}, 현재 값 ${cur}`);
return acc + cur;
}); // 초기값 없음!
console.log('초기값 없는 합계', sumWithoutInitial); // 결과는 똑같이 10!
// 출력 예상
// acc 상태 1, 현재 값 2 <-- 어? acc가 1부터 시작하고, cur는 2부터 시작해요!
// acc 상태 3, 현재 값 3
// acc 상태 6, 현재 값 4
// 초기값 없는 합계 10
결과는 10으로 같지만, 로그를 자세히 보세요! 첫 번째 로그에서 acc
는 배열의 첫 요소인 1이고, cur
는 두 번째 요소인 2부터 시작했어요. 신기하죠?
다른 예시 - 문자열 합치기
초기값 없이도 문자열 같은 다른 타입도 잘 처리할 수 있어요.
const words = ['안녕', '하세요', 'reduce', '입니다'];
// 초기값 없이 문자열 배열 합치기
const sentence = words.reduce((acc, cur) => {
console.log(`acc 상태 '${acc}', 현재 단어 '${cur}'`);
return acc + ' ' + cur; // 중간에 공백 추가
});
console.log('만들어진 문장', sentence); // 결과는 '안녕하세요 reduce 입니다'
// 출력 예상
// acc 상태 '안녕', 현재 단어 '하세요'
// acc 상태 '안녕 하세요', 현재 단어 'reduce'
// acc 상태 '안녕 하세요 reduce', 현재 단어 '입니다'
// 만들어진 문장 안녕 하세요 reduce 입니다
여기서도 acc
는 첫 단어 '안녕'으로 시작하고, cur
는 '하세요'부터 시작하는 것을 볼 수 있어요.
⚠️ 초기값 미설정 시 주의할 점!
acc
를 뭘로 시작해야 할지 모르니까 TypeError
가 빵! 터져요. 조심해야 해요![1, 'hello', 3]
)그래서 웬만하면 초기값을 명시적으로 설정하는 습관을 들이는 것이 더 안전하고 예측 가능한 코드를 작성하는 데 도움이 된답니다. 👍
reduce
가 정말 다재다능하고 강력하긴 한데요, 그렇다고 해서 모든 로직을 reduce
하나로 욱여넣으려는 생각은 조금 위험할 수 있어요. 왜냐하면, 콜백 함수 안에 너무 많은 조건과 계산이 들어가면 코드가 복잡해져서 나중에 본인이 다시 보거나 동료 개발자가 볼 때 "이게 도대체 무슨 코드지??" 하고 머리를 쥐어뜯게 될 수도 있거든요. 🤯
코드는 잘 돌아가는 것도 중요하지만, 다른 사람이 (그리고 미래의 내가!) 쉽게 읽고 이해할 수 있는 것도 정말 중요해요. 이걸 '가독성'이라고 하죠. 특히 함께 일하는 동료가 있다면 가독성은 더더욱 중요하고요.
예를 들어 한번 볼까요? 배열에서 양수인 짝수만 골라내서, 각각을 제곱한 다음, 그 결과들을 모두 더하는 로직을 생각해 볼게요. 이걸 reduce
하나로만 구현하면 이렇게 될 수 있어요.
const numbers = [1, -2, 3, 4, -5, 6, 7, 8];
// 😫 이렇게 하면 한눈에 파악하기 너무 복잡해요!
const complexSum = numbers.reduce((acc, cur) => {
console.log(`현재 acc ${acc}, 현재 값 ${cur}`);
// 조건 1: 양수인가?
if (cur > 0) {
// 조건 2: 짝수인가?
if (cur % 2 === 0) {
// 조건 통과 시 제곱해서 더하기
return acc + cur * cur;
}
}
// 조건에 안 맞으면 그냥 이전 누적값 반환
return acc;
}, 0);
console.log('복잡한 reduce 결과', complexSum); // 4*4 + 6*6 + 8*8 = 16 + 36 + 64 = 116
물론 결과(116)는 잘 나오지만, reduce
콜백 함수 안에서 필터링(if
)과 값 변형(cur * cur
), 그리고 합산(acc + ...
)까지 여러 가지 일을 한꺼번에 처리하고 있죠. 코드가 길어지거나 조건이 더 복잡해지면 해독하기 정말 어려워질 거예요. 😅
그럼 어떻게 하는 게 더 좋을까요? 이럴 때는 filter
, map
같은 다른 배열 메서드들과 함께 사용하는 것이 훨씬 좋아요! 각 메서드가 한 가지 역할만 명확하게 수행하도록 단계를 나누는 거죠.
const numbers = [1, -2, 3, 4, -5, 6, 7, 8];
// ✨ 이렇게 단계를 나누면 훨씬 보기 좋아요!
const muchBetterSum = numbers
.filter(num => num > 0 && num % 2 === 0) // 1단계 : 양수인 짝수만 걸러내기 -> [4, 6, 8]
.map(num => num * num) // 2단계 : 각 숫자를 제곱하기 -> [16, 36, 64]
.reduce((acc, cur) => acc + cur, 0); // 3단계 : 마지막으로 합계 구하기 -> 116
console.log('개선된 코드 결과', muchBetterSum); // 결과는 똑같이 116
어때요? filter
로 원하는 숫자만 걸러내고, map
으로 제곱한 다음, 마지막에 reduce
로 간단하게 합계만 구하니까 각 단계가 훨씬 명확하고 이해하기 쉽지 않나요? 코드가 마치 물 흐르듯이 읽히는 느낌이에요!
결론은 이거예요! reduce
는 강력한 도구지만, 망치로 모든 문제를 해결하려 들면 안 되는 것처럼, reduce
로 모든 배열 처리를 하려고 하기보다는 가장 적절하고 읽기 좋은 방법을 선택하는 것이 중요해요. 때로는 filter
, map
등 다른 메서드들과 조합하는 것이 훨씬 더 좋은 코드를 만드는 비결이랍니다! 😉
자, reduce
메서드에 대해 좀 더 깊이 알게 되셨나요? 처음엔 조금 낯설 수 있지만, 배열 데이터를 요리조리 조합해서 원하는 결과 하나로 딱! 만들어내는 reduce
의 매력에 빠지면 헤어 나오기 어려울 거예요.
특히 초기값을 설정하는 것이 코드의 안정성과 명확성을 높여준다는 점, 꼭 기억해주세요!
이제 여러분의 코드 속에서 reduce
를 더 자신감 있게 사용해보세요! 혹시 reduce
를 사용하면서 겪었던 재미있는 경험이나 궁금한 점이 있다면 댓글로 자유롭게 남겨주시고요! 같이 이야기 나누면 더 좋잖아요? 😉
참고하면 좋은 자료들