재귀적으로 생각하기: 배열 합의 호출과 복귀를 따라가기

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

배열의 합을 재귀로 구할 때 남은 원소 수와 반환값은 어떻게 이어지나요?

배열 전체의 합은 마지막 값을 제외한 합에 마지막 값을 더한 결과입니다. 배열을 매번 복사하지 않고 남은 원소 수만 줄이면 같은 생각을 더 분명하게 구현할 수 있습니다.

적용 조건·범위: 분석 조건: n은 0~256인 배열 길이입니다. 입력을 복사하지 않고 count만 줄이며 호출 스택을 공간에 포함합니다. Number의 표현 오차는 별도입니다.

본문 근거: MDN JavaScript Guide: Functions · MDN Glossary: Call stack

읽기 전에 알아두면 좋아요

아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.

개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.

과정 위치와 선택 복습 안내

과정 위치: 재귀와 호출·복귀 · 알고리즘 개념

과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.

JavaScript로 문법·실행 복습하기 (선택)

개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.

재귀적으로 생각하기: 배열 합의 호출과 복귀를 따라가기 주제를 표현한 밝은 일러스트와 작은 곰 표지

배열 전체의 합은 마지막 값을 제외한 합에 마지막 값을 더한 결과입니다. 배열을 매번 복사하지 않고 남은 원소 수만 줄이면 같은 생각을 더 분명하게 구현할 수 있습니다.

선수 지식: 재귀의 기저 조건, 배열 인덱스, 함수 반환값

  • 한 호출이 맡은 문제와 기다리는 결과를 구분합니다.
  • 빈 배열을 합의 기저 조건으로 처리합니다.
  • 배열 복사 비용과 호출 스택 비용을 따로 셉니다.

앞의 count개 원소의 합이라는 뜻부터 정합니다

[2, 4, 6]의 합을 구할 때 현재 함수가 모든 덧셈을 한 번에 할 필요는 없습니다. 앞의 두 원소 합이 6이라는 답을 받으면 마지막 값 6을 더해 12를 반환할 수 있습니다. 앞의 두 원소 합도 앞의 한 원소 합 2에 4를 더한 값입니다. 이처럼 크기만 다른 같은 문제로 바꾸는 관점이 재귀적 사고입니다.

2, 4, 6 배열의 앞쪽 count개 범위와 합 2, 6, 12
count는 원소 개수입니다. 현재 호출이 더하는 마지막 원소는 count−1 위치이며, 아무것도 포함하지 않는 solve(0)의 합은 0입니다. 원본 보기 (새 탭)

내부 함수 solve(count)는 배열의 앞쪽 count개 원소의 합을 뜻합니다. count가 3이면 인덱스 0부터 2까지, count가 1이면 인덱스 0까지만 포함합니다. 따라서 현재 호출이 더할 마지막 값의 인덱스는 count가 아니라 count - 1입니다. 함수 매개변수의 의미를 문장으로 고정하면 배열 범위를 헷갈릴 일이 줄어듭니다.

count가 0이면 포함할 원소가 하나도 없습니다. 이때 합은 0입니다. 빈 배열을 별도의 이상한 입력으로 취급하지 않고 자연스러운 가장 작은 문제로 삼으면, 한 원소짜리 배열도 같은 식으로 처리할 수 있습니다. solve(1)은 solve(0) + values[0]이므로 정확히 첫 값이 됩니다.

내려갈 때는 문제를 줄이고 돌아올 때 값을 더합니다

solve(3)에서 solve(0)까지 내려간 뒤 2, 6, 12를 반환하는 흐름
호출할 때는 더 작은 앞부분의 합을 기다립니다. 빈 합 0이 반환된 뒤 가장 최근에 기다린 덧셈부터 처리해 최종 12가 됩니다. 원본 보기 (새 탭)
[2, 4, 6]을 합하는 호출의 대기와 복귀
호출 기다리는 식 작은 답을 받은 뒤
solve(3) solve(2) + 6 6 + 6 = 12
solve(2) solve(1) + 4 2 + 4 = 6
solve(1) solve(0) + 2 0 + 2 = 2
solve(0) 더 작은 호출 없음 빈 합 0 반환

표는 위에서 아래로 호출을 읽고 아래에서 위로 반환을 읽습니다. solve(3)의 6과 solve(2)의 4는 사라진 값이 아닙니다. 각 호출이 어떤 계산을 마쳐야 하는지 기억한 채 작은 답을 기다립니다. 기저 조건이 반환되면 가장 최근에 대기한 덧셈부터 처리되어, 최종 호출에 12가 돌아옵니다.

재귀 호출 뒤에 더하기가 있으므로 이 코드는 누산기를 전달하는 꼬리 호출 형태가 아닙니다. 호출한 순간에 12를 안다고 생각하거나, 재귀 호출을 한 뒤의 문장이 실행되지 않는다고 생각하면 복귀 과정을 놓칩니다. 종이에 대기하는 식을 한 줄씩 적는 것이 실행 순서를 이해하는 데 도움이 됩니다.

같은 배열을 읽고 남은 크기만 바꿉니다

작은 배열을 만들기 위해 매번 마지막 값을 제외한 slice() 결과를 전달할 수도 있습니다. 그러나 길이 n, n – 1, n – 2인 배열을 계속 복사하면 원소 복사량의 합은 O(n²)이 됩니다. 재귀식 자체는 단순해 보여도 작은 문제를 만드는 데 드는 작업을 분석에서 빼면 안 됩니다.

count가 달라도 하나의 원본 배열을 참조하는 구현과 매번 부분 배열을 복사하는 구현의 비교
같은 배열을 읽으며 count만 줄이면 전체 시간은 O(n)입니다. 매번 부분 배열을 복사하면 누적 복사량이 O(n²)이 되며, 복사 여부와 호출 스택은 따로 셉니다. 원본 보기 (새 탭)

이 예제는 원래 배열을 그대로 읽고 count만 1씩 줄입니다. 배열에 값을 쓰지 않으므로 호출 뒤에도 입력은 그대로입니다. 각 단계의 인덱스 조회와 덧셈은 상수 작업이며, 검사까지 포함한 전체 시간은 O(n)입니다. 새 부분 배열은 만들지 않지만 대기 중인 함수 호출이 있으므로 추가 공간은 O(n)입니다.

원소를 뒤에서 더하는 것과 뒤에서 호출을 만드는 것은 다릅니다. 이 식은 앞부분의 합을 먼저 완성한 다음 뒤의 값을 붙입니다. Number 소수의 덧셈에는 반올림이 있으므로 순서를 바꾸면 아주 작은 오차가 달라질 수 있습니다. 정수 합이 안전한 정수 범위를 벗어나는 문제에서는 BigInt 같은 다른 수 표현이 필요합니다.

빈 배열은 답이고 잘못된 원소는 오류입니다

함수는 빈 칸 없는 유한 숫자 배열을 받고, 빈 배열에서는 0을 반환합니다. 문자열 숫자, NaN, Infinity와 빈 칸이 있는 희소 배열은 거절합니다. 검사 없이 문자열을 더하면 수의 합이 문자열 이어 붙이기로 바뀔 수 있습니다. 계산 중 합이 Infinity가 되는 경우도 오류로 알리지만, 유한한 결과라고 해서 임의의 큰 정수를 정확히 표현한다는 보장은 없습니다.

빈 배열은 합 0으로 처리하고 숫자 문자열, NaN, Infinity, 누락된 원소는 각각 거절하는 구분
빈 배열은 올바른 입력입니다. 각 잘못된 원소의 예와 구분하고, 256은 학습용 호출 길이 제한이라는 점을 함께 확인합니다. 원본 보기 (새 탭)

입력 길이는 학습용으로 256 이하로 제한합니다. 수학적으로는 길이가 줄어 언제나 끝나더라도 실행 환경의 호출 스택은 유한합니다. 긴 배열의 합은 반복문으로 구하면 호출 스택을 늘리지 않고 추가 공간 O(1)로 처리할 수 있습니다. 여기서는 가장 빠른 합산 도구를 만드는 것보다 작은 문제의 답을 재사용하는 흐름을 읽는 데 집중합니다.

전체 코드와 직접 실행

개념을 읽었다면 아래 예제를 직접 실행하며 상태 변화를 확인할 수 있습니다.

전체 JavaScript 코드와 실행 방법 펼치기

별도 패키지는 필요하지 않습니다. Node.js가 처음이라면 브라우저와 Node.js 시작 안내부터 확인하세요.

javascript-recursive-sum-call-return.mjs 파일을 만들고 아래 전체 코드를 저장합니다.

function sumArray(values) {
  if (!Array.isArray(values)) throw new TypeError('숫자 배열이 필요합니다.');
  if (values.length > 256) throw new RangeError('길이는 256 이하여야 합니다.');
  for (let i = 0; i < values.length; i++) {
    if (!Object.hasOwn(values, i) || !Number.isFinite(values[i])) {
      throw new TypeError('빈 칸 없는 유한 숫자 배열이 필요합니다.');
    }
  }
  function solve(count) {
    if (count === 0) return 0;
    const result = solve(count - 1) + values[count - 1];
    if (!Number.isFinite(result)) throw new RangeError('Number 범위를 벗어났습니다.');
    return result;
  }
  return solve(values.length);
}

console.log(sumArray([2, 4, 6]));
console.log(sumArray([]));
console.log(sumArray([-5, 2, 3]));

파일이 있는 폴더의 터미널에서 실행합니다.

node javascript-recursive-sum-call-return.mjs

기대 출력

12
0
0

비용과 적용 범위

시간: O(n)

각 원소를 한 번씩 읽는 시간과 대기 중인 호출 프레임의 공간 비용
추가 부분 배열은 없지만 호출이 작은 답을 기다리므로 스택 공간 O(n)이 남습니다. 입력을 복사하지 않는다는 이유로 추가 공간을 O(1)이라고 세지 않습니다. 원본 보기 (새 탭)

공간: O(n)

분석 조건: n은 0~256인 배열 길이입니다. 입력을 복사하지 않고 count만 줄이며 호출 스택을 공간에 포함합니다. Number의 표현 오차는 별도입니다.

직접 확인해 보기

sumArray([3, -1, 5])에서 solve(2)가 반환하는 값과 solve(3)가 그 값을 이용하는 식은 무엇인가요?

3, -1, 5 입력의 앞 두 원소 합과 최종 호출에 더할 값을 묻는 문제
앞의 두 값을 포함한 solve(2)부터 생각한 뒤, solve(3)가 어떤 원소를 더하는지 연결해 보세요. 원본 보기 (새 탭)
답과 이유 확인

solve(2)는 3 + (-1)인 2를 반환합니다. solve(3)는 그 결과에 인덱스 2의 값 5를 더하므로 2 + 5 = 7을 반환합니다. 빈 합 0에서 복귀가 시작됩니다.

참고 자료

  • MDN JavaScript Guide: Functions — 자기 자신을 호출하는 함수와 재귀 호출의 중첩, 기저 조건을 참고했습니다. 호출·복귀 추적과 입력 계약은 별도로 구성했습니다.
  • MDN Glossary: Call stack — 실행 중인 함수 호출을 쌓고 반환할 때 제거하는 호출 스택, 지나친 중첩의 제한을 참고했습니다.

이 글이 도움이 되었나요?

조회 중

이어서 공부하기

체크포인트: 예제의 상태 변화를 손으로 추적하고 아래 문제에 적용하세요. 값이 없는 경우와 원소 하나인 경우도 설명해 보세요. 기저 조건에 도달하는 이유와 호출 뒤 돌아와 처리하는 작업을 작은 입력으로 설명하세요.

다음 추천 · 알고리즘 개념: 하노이의 탑: 큰 원판 하나를 옮기기 위한 재귀적 순서

같은 주제에서 다음 개념과 예제로 이해를 넓힙니다.

복습 문제 보기 (선택)

앞에서 다룬 문제에 이 개념을 다시 적용해 보세요. 세 수 중 최솟값 조건문 풀이 정리

권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.

원래 과정 목록 순서 (선택)

읽음 기록

읽음 — / 전체 30개 · 필수 0개

이 글을 쓴 해비

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

새 글 받아보기

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

RSS 피드 구독하기

댓글 남기기