자바스크립트 reduce, 너 정말 매력 있구나?

홍태극·2023년 9월 19일
0

자바스크립트 reduce, 너 정말 매력 있구나? (초보 탈출 가이드)

자바스크립트로 개발하다 보면 배열을 다룰 일이 정말 많죠? map, filter 같은 메서드는 이제 익숙하실 텐데요, 혹시 reduce 메서드는 어떠신가요? 뭔가 이름부터 '줄인다'는 느낌인데, 처음엔 살짝 헷갈릴 수도 있어요. 저도 그랬거든요! 😉

하지만 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가 어떻게 변하는지 감이 오시죠? acccur를 계속 더해서 다음 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의 기본 감각, 조금 잡히셨나요? 😊

초기값을 줄 때 vs 안 줄 때, 뭐가 다를까요?

자, 이제 reduce의 핵심 중 하나인 초기값에 대해 이야기해 볼게요. 이 초기값을 주느냐, 안 주느냐에 따라 reduce의 행동이 꽤 달라지거든요. 이게 헷갈리는 포인트가 될 수 있어요!

1. ✨ 초기값을 딱! 설정했을 때

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를 사용해도 에러가 나지 않아요! 빈 배열에 초기값을 주면, 그 초기값이 그냥 결과로 반환돼요. 안전하죠!

2. 🤔 초기값을... 깜빡했을 때 (설정 안 했을 때)

만약 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 하나로 욱여넣으려는 생각은 조금 위험할 수 있어요. 왜냐하면, 콜백 함수 안에 너무 많은 조건과 계산이 들어가면 코드가 복잡해져서 나중에 본인이 다시 보거나 동료 개발자가 볼 때 "이게 도대체 무슨 코드지??" 하고 머리를 쥐어뜯게 될 수도 있거든요. 🤯

코드는 잘 돌아가는 것도 중요하지만, 다른 사람이 (그리고 미래의 내가!) 쉽게 읽고 이해할 수 있는 것도 정말 중요해요. 이걸 '가독성'이라고 하죠. 특히 함께 일하는 동료가 있다면 가독성은 더더욱 중요하고요.

예를 들어 한번 볼까요? 배열에서 양수인 짝수만 골라내서, 각각을 제곱한 다음, 그 결과들을 모두 더하는 로직을 생각해 볼게요. 이걸 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를 사용하면서 겪었던 재미있는 경험이나 궁금한 점이 있다면 댓글로 자유롭게 남겨주시고요! 같이 이야기 나누면 더 좋잖아요? 😉


참고하면 좋은 자료들

0개의 댓글