JavaScript reduce 사용법: 배열 누적 계산을 이해하는 기준

··설명 약 8분·작성: 해비·블로그 소개

JavaScript reduce의 초기값과 콜백 반환값으로 배열 합계를 어떻게 계산하나요?

reduce는 초기값부터 시작해 콜백의 반환값을 다음 누적값으로 넘깁니다. 마지막 값이 결과입니다. 초기값을 명시해 전체·완료·미완료 개수를 계산하면 빈 배열도 같은 모양의 결과를 돌려줍니다.

적용 조건·범위: 초기 accumulator의 세 필드를 0으로 명시해 빈 배열에서도 반환 형태를 유지합니다. 외부 tasks와 내부 task 객체는 수정하지 않습니다. Node.js에서 위 출력과 일치함을 확인했습니다. 브라우저 Console 실행은 미확인입니다.

공식 문서 참고: MDN: reduce의 누적값·초기값·빈 배열 처리

혼자 실행해 보는 reduce로 요약 객체 만들기

JavaScript reduce 사용법: 배열 누적 계산을 이해하는 기준 · 독립 실습

일반 목록과 빈 배열의 total·completed·pending 집계를 비교하세요.

예제 크게 보기 (새 탭)

독자 질문: 빈 목록을 포함해 total·completed·pending을 한 번에 어떻게 계산하는가?

js-06-example.mjs로 저장한 뒤 node js-06-example.mjs을 실행하세요. 외부 패키지는 필요하지 않습니다.

const getTaskSummary = (tasks) => tasks.reduce((summary, task) => {
  summary.total += 1;
  if (task.completed) summary.completed += 1;
  else summary.pending += 1;
  return summary;
}, { total: 0, completed: 0, pending: 0 });
const tasks = [
  { id: '1', title: '학습', completed: false },
  { id: '2', title: '확인', completed: true },
  { id: '3', title: '문제', completed: false },
];
console.log('현재:', JSON.stringify(getTaskSummary(tasks)));
console.log('빈 목록:', JSON.stringify(getTaskSummary([])));

예상 출력:

현재: {"total":3,"completed":1,"pending":2}
빈 목록: {"total":0,"completed":0,"pending":0}

초기 accumulator의 세 필드를 0으로 명시해 빈 배열에서도 반환 형태를 유지합니다. 외부 tasks와 내부 task 객체는 수정하지 않습니다. Node.js에서 위 출력과 일치함을 확인했습니다. 브라우저 Console 실행은 미확인입니다.

직접 풀어볼 문제

세 번째 작업을 완료 상태로 바꾸고 현재 요약을 예측해 보세요. 예상 값은 total 3, completed 2, pending 1입니다.

reduce는 이전 결과를 다음 반복으로 넘깁니다

const numbers = [10, 20, 30];


const total = numbers.reduce((sum, number) => {
  return sum + number;
}, 0);


console.log(total);

실행 흐름을 적으면 다음과 같습니다.

초기 sum = 0
1회차: 0 + 10 = 10
2회차: 10 + 20 = 30
3회차: 30 + 30 = 60
최종 결과 = 60

콜백의 첫 번째 인수는 이전 반복까지 누적한 값이고 두 번째 인수는 현재 요소입니다.

reduce는 각 항목을 처리하며 누적값을 갱신해 최종 결과 하나를 만듭니다.
개념 설명용 일러스트. reduce는 각 항목을 처리하며 누적값을 갱신해 최종 결과 하나를 만듭니다. 이미지 원본 열기

initialValue를 명시하는 이유

다음처럼 초깃값을 생략할 수도 있습니다.

const total = [10, 20, 30].reduce((sum, number) => sum + number);

하지만 빈 배열에서는 오류가 발생합니다.

const total = [].reduce((sum, number) => sum + number);

학습 과정과 실무 코드에서는 누적 결과의 형태를 명확하게 하기 위해 초깃값을 명시하는 편이 안전합니다.

const total = [].reduce((sum, number) => sum + number, 0);

숫자가 아니라 객체도 누적할 수 있습니다

const tasks = [
  { completed: true },
  { completed: false },
  { completed: true },
];


const summary = tasks.reduce(
  (result, task) => {
    result.total += 1;


    if (task.completed) {
      result.completed += 1;
    } else {
      result.pending += 1;
    }


    return result;
  },
  { total: 0, completed: 0, pending: 0 },
);

accumulator의 시작 형태가 객체라면 최종 결과도 객체로 만들 수 있습니다.

accumulator를 직접 수정해도 될까

reduce 내부에서 accumulator 객체를 수정하는 패턴은 로컬 누적 객체를 만드는 용도라면 사용할 수 있습니다. 여기서 수정하는 객체는 외부의 Task 상태가 아니라 이번 reduce 호출 안에서 새로 만든 accumulator입니다.

Task 객체 자체를 변경하는 것과는 구분해야 합니다.

reduce를 억지로 쓰지 않습니다

const tasksForTitles = [{ title: '학습', completed: false }, { title: '확인', completed: true }];
const pendingTitles = tasksForTitles
  .filter((task) => !task.completed)
  .map((task) => task.title);

이 코드를 하나의 reduce로 합칠 수는 있지만 반드시 더 좋은 코드는 아닙니다. 선택과 변환이라는 의도가 분명하다면 filter와 map이 더 읽기 쉽습니다.

자주 발생하는 오류

return을 빠뜨립니다

const result = numbers.reduce((sum, number) => {
  sum + number;
}, 0);

다음 반복의 accumulator가 undefined가 됩니다.

초깃값 형태가 결과와 다릅니다

객체를 만들려는 reduce라면 {} 또는 필요한 기본 필드를 가진 객체로 시작해야 합니다.

한 함수에서 너무 많은 일을 합니다

그룹화, 정렬, 문자열 생성, 통계까지 한 reduce에서 처리하면 이해하기 어렵습니다. 결과가 복잡해질수록 작은 함수로 분리하는 편이 좋습니다.

학습 완료 체크리스트

  • accumulator와 currentValue를 설명할 수 있습니다.
  • initialValue가 왜 필요한지 이해했습니다.
  • 빈 배열에서도 안전한 reduce를 작성할 수 있습니다.
  • 숫자뿐 아니라 객체를 누적할 수 있습니다.
  • reduce가 불필요한 상황을 구분할 수 있습니다.

독립 실습은 여기서 마칠 수 있습니다. 아래 선택 실습은 TaskFlow 프로젝트 파일로 진행합니다.

선택 실습: 할 일 앱에 적용하기

TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript로 화면에 동작을 붙입니다.

getTaskSummary를 추가해 전체·완료·미완료 작업 수를 계산합니다.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow 프로젝트 적용

JavaScript reduce 사용법: 배열 누적 계산을 이해하는 기준 · 단계별 TaskFlow

TaskFlow JavaScript 6단계입니다. 기존 서비스 화면과 실제 콘솔 출력을 확인하세요.

예제 크게 보기 (새 탭)

변경 설명과 부분 예제를 읽은 뒤, 시작·완료 파일과 적용 순서에서 필요한 선언·import·화면 연결을 포함한 전체 파일을 확인하세요. 이번 단계의 변경 이유와 적용 위치도 함께 확인합니다.

시작본과 이번 변경

JavaScript 객체 참조와 불변 갱신: 중첩 객체를 안전하게 바꾸기의 완료 체크포인트를 시작본으로 사용합니다.

js/task-store.js의 기존 CRUD 함수는 유지하고 getTaskSummary를 추가합니다. js/app.js에는 기존 코드 아래 요약을 출력하는 호출만 추가합니다. 초기 데이터에서 total 3·completed 1·pending 2인지, 모두 삭제한 뒤 세 값이 0인지 확인하세요.

연속 프로젝트 선수 조건

  • map, filter, forEach의 역할
  • 객체 불변 갱신
  • javascript-step-05-immutable-update 체크포인트

이전 체크포인트

javascript-step-05-immutable-update

getTaskSummary() 추가

js/task-store.js에 다음 함수를 추가합니다.

export function getTaskSummary() {
  return tasks.reduce(
    (summary, task) => {
      summary.total += 1;


      if (task.completed) {
        summary.completed += 1;
      } else {
        summary.pending += 1;
      }


      return summary;
    },
    {
      total: 0,
      completed: 0,
      pending: 0,
    },
  );
}

이번 회차 변경 코드와 전체본 연결

이 회차에서는 task-store.js가 핵심 변경 파일이며 나머지 파일은 이전 체크포인트를 유지합니다.

전체 파일·코드 보기

js/task-store.js 전체 코드

let tasks = [
  { id: '1', title: 'JavaScript 과정 정리', completed: false },
  { id: '2', title: 'TaskFlow 화면 확인', completed: true },
  { id: '3', title: '다음 글 읽기', completed: false },
];


export function getTasks() {
  return [...tasks];
}


export function addTask(title) {
  const task = {
    id: String(Date.now()),
    title,
    completed: false,
  };


  tasks = [...tasks, task];
  return task;
}


export function toggleTask(id) {
  let updatedTask = null;


  tasks = tasks.map((task) => {
    if (task.id !== id) return task;


    updatedTask = {
      ...task,
      completed: !task.completed,
    };


    return updatedTask;
  });


  return updatedTask;
}


export function deleteTask(id) {
  tasks = tasks.filter((task) => task.id !== id);
}


export function filterTasks(status = 'all') {
  if (status === 'completed') {
    return tasks.filter((task) => task.completed);
  }


  if (status === 'pending') {
    return tasks.filter((task) => !task.completed);
  }


  return [...tasks];
}


export function getTaskSummary() {
  return tasks.reduce(
    (summary, task) => {
      summary.total += 1;


      if (task.completed) {
        summary.completed += 1;
      } else {
        summary.pending += 1;
      }


      return summary;
    },
    { total: 0, completed: 0, pending: 0 },
  );
}

js/app.js

import { getTaskSummary } from './task-store.js';


console.log(getTaskSummary());

기대 결과:

{ total: 3, completed: 1, pending: 2 }

빈 배열이 되더라도 같은 필드가 모두 0인 객체를 반환해야 합니다.

중간부터 시작하는 전체 파일과 적용 순서

이번 글 시작 ZIP · 이번 글 완료 ZIP

시작 ZIP은 직전 프로젝트 단계 javascript-step-05-immutable-update의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.

  1. 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
  2. 기존 CRUD를 유지한 store 전체본과 요약 호출 app.js를 적용합니다.
  3. 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
  4. 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.

추가·교체 파일

  • CHANGES.md — 전체 교체
  • README.md — 전체 교체
  • js/app.js — 전체 교체
  • js/task-store.js — 전체 교체

실행과 확인

npm start
npm test

개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.

예상 결과: total 3·completed 1·pending 2, 모두 삭제한 뒤 0·0·0입니다.

실제 검증: JavaScript 모듈·DOM·경계값 실행 검사 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.

이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.

복사할 변경 파일 전체 코드

js/app.js — 전체 파일

import { getTaskSummary } from './task-store.js';


console.log(getTaskSummary());

js/task-store.js — 전체 파일

let tasks = [
  { id: '1', title: 'JavaScript 과정 정리', completed: false },
  { id: '2', title: 'TaskFlow 화면 확인', completed: true },
  { id: '3', title: '다음 글 읽기', completed: false },
];


export function getTasks() {
  return [...tasks];
}


export function addTask(title) {
  const task = {
    id: crypto.randomUUID(),
    title,
    completed: false,
  };


  tasks = [...tasks, task];
  return task;
}


export function toggleTask(id) {
  let updatedTask = null;


  tasks = tasks.map((task) => {
    if (task.id !== id) return task;


    updatedTask = {
      ...task,
      completed: !task.completed,
    };


    return updatedTask;
  });


  return updatedTask;
}


export function deleteTask(id) {
  tasks = tasks.filter((task) => task.id !== id);
}


export function filterTasks(status = 'all') {
  if (status === 'completed') {
    return tasks.filter((task) => task.completed);
  }


  if (status === 'pending') {
    return tasks.filter((task) => !task.completed);
  }


  return [...tasks];
}


export function getTaskSummary() {
  return tasks.reduce(
    (summary, task) => {
      summary.total += 1;


      if (task.completed) {
        summary.completed += 1;
      } else {
        summary.pending += 1;
      }


      return summary;
    },
    { total: 0, completed: 0, pending: 0 },
  );
}

완료 체크포인트

javascript-step-06-reduce-summary

다음 학습

다음 글부터 브라우저 화면과 연결합니다. 기존 TaskFlow 서비스 페이지에 작업 관리 데모 섹션과 입력 폼을 추가하고 JavaScript 입력 검증을 적용합니다.

다음 글: JavaScript DOM 폼 만들기: 입력 검증과 접근성 처리

글 소개·학습 준비·전체 목차
reduce 누적 계산: 배열에서 요약으로

JavaScript · 6 / 15 · 필수 학습 · reduce로 누적 계산 이해하기

reduce()는 initialValue에서 시작해 콜백이 반환한 값을 다음 반복의 accumulator로 넘기고, 마지막 누적값을 결과로 반환합니다. 초기값을 명시하면 빈 배열에서도 같은 결과 형태를 유지할 수 있습니다.

이번 글에서는 accumulator가 어떻게 변하는지 직접 추적한 뒤 TaskFlow의 전체·완료·미완료 개수를 하나의 요약 객체로 계산합니다.

이번 글에서 배우는 내용

  • accumulator와 currentValue
  • initialValue
  • 숫자 합계
  • 객체 누적
  • 빈 배열 처리
  • reduce를 쓰지 않는 편이 나은 경우
  • TaskFlow 요약 데이터 만들기

먼저 알아야 할 내용

배열·객체·함수와 콜백을 알고 있으면 됩니다. 아래 단독 실습은 Node.js에서 외부 패키지 없이 실행합니다.

목차

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 15개 · 필수 14개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기