배열의 모든 요소를 순회하면서 누적된 결과값을 계산해야 할 때가 있다. 장바구니 총액 계산, 배열 요소 조건부 필터링 후 합산, 또는 복잡한 데이터 변환 같은 작업 말이다. 다만 대부분의 개발자들은 반복문(foreach)으로 변수를 하나하나 더하거나 빼면서 코드가 길어지고 유지보수하기 어려워진다는 걸 경험했을 것이다. 이번에는 PHP array_reduce()가 정확히 뭔지, 왜 필요한지, 어떻게 사용해야 실무에서 효율적으로 쓸 수 있는지 완벽하게 정리해서 소개하겠다.

 

1단계: array_reduce() 기초 이해하기

array_reduce()는 배열의 모든 요소를 반복 순회하면서 콜백 함수로 처리한 결과를 누적(축약)해서 최종적으로 단일 값을 반환하는 함수다. 함수형 프로그래밍의 핵심 개념 중 하나인 'Reduce' 패턴을 PHP에서 구현한 것이다.

기본 문법:

array_reduce(
    array $array,           // 처리할 배열
    callable $callback,     // 각 요소를 처리할 콜백 함수
    mixed $initial = null   // 초기 누적값 (선택사항)
): mixed

콜백 함수 구조:

function callback(
    mixed $carry,       // 누적된 결과값 (초기값은 $initial)
    mixed $item         // 현재 처리 중인 배열 요소
): mixed {
    // $carry를 $item으로 업데이트하고 반환
    return $carry;
}

동작 원리:

배열 [1, 2, 3]을 예로 들면:

  • 초기: $carry = 0 (초기값), $item = 1 → 계산 → $carry = 1
  • 2번째: $carry = 1, $item = 2 → 계산 → $carry = 3
  • 3번째: $carry = 3, $item = 3 → 계산 → $carry = 6
  • 최종 반환: 6

 

2단계: 실무에서 자주 쓰는 패턴 5가지
패턴 1) 배열 요소 합계 계산

가장 기본적인 사용 사례다. foreach와 비교해보자.

✗ 잘못된 코드 (foreach 사용)
$numbers = [10, 20, 30, 40];
$sum = 0;
foreach ($numbers as $num) {
    $sum += $num;  // 누적값을 수동으로 관리
}
echo $sum;  // 100
✓ 올바른 코드 (array_reduce 사용)
$numbers = [10, 20, 30, 40];
$sum = array_reduce($numbers, function($carry, $item) {
    return $carry + $item;  // 누적값 자동 관리
}, 0);  // 초기값: 0
echo $sum;  // 100

왜 이게 더 나은가: 상태 변수($sum)를 따로 관리할 필요 없고, 순수 함수처럼 동작해서 버그가 줄어든다.

패턴 2) 배열 요소의 특정 필드 합산 (연관배열)

실무에서 DB 쿼리 결과인 연관배열의 특정 키만 합산해야 할 때가 많다.

✗ 잘못된 코드 (foreach + 타입 체크 누락)
$items = [
    ['name' => '상품A', 'price' => 15000],
    ['name' => '상품B', 'price' => 25000],
    ['name' => '상품C', 'price' => 10000]
];

$total = 0;
foreach ($items as $item) {
    $total += $item['price'];  // 타입 검증 없음
}
echo $total;  // 50000
✓ 올바른 코드 (array_reduce + 타입 안전성)
$items = [
    ['name' => '상품A', 'price' => 15000],
    ['name' => '상품B', 'price' => 25000],
    ['name' => '상품C', 'price' => 10000]
];

$total = array_reduce($items, function($carry, $item) {
    return $carry + (int)$item['price'];  // 명시적 형변환
}, 0);
echo $total;  // 50000

왜 이게 더 나은가: 콜백 함수 내에서 타입을 명시적으로 변환하고 검증할 수 있다.

패턴 3) 조건부 합계 (일부 요소만 계산)

특정 조건을 만족하는 요소만 합산해야 할 때다.

✗ 잘못된 코드 (if 문으로 분기)
$sales = [
    ['product' => 'A', 'amount' => 50000, 'status' => 'completed'],
    ['product' => 'B', 'amount' => 30000, 'status' => 'pending'],
    ['product' => 'C', 'amount' => 70000, 'status' => 'completed']
];

$completed_total = 0;
foreach ($sales as $sale) {
    if ($sale['status'] === 'completed') {
        $completed_total += $sale['amount'];
    }
}
echo $completed_total;  // 120000
✓ 올바른 코드 (array_reduce로 통일)
$sales = [
    ['product' => 'A', 'amount' => 50000, 'status' => 'completed'],
    ['product' => 'B', 'amount' => 30000, 'status' => 'pending'],
    ['product' => 'C', 'amount' => 70000, 'status' => 'completed']
];

$completed_total = array_reduce($sales, function($carry, $item) {
    return $item['status'] === 'completed' 
        ? $carry + $item['amount']
        : $carry;
}, 0);
echo $completed_total;  // 120000

왜 이게 더 나은가: 반복 로직과 조건 검사가 한 곳에 명확하게 보인다.

패턴 4) 배열을 새로운 구조로 변환 (그룹화)

배열 요소들을 특정 키로 그룹화해서 새로운 배열을 만드는 경우.

✗ 잘못된 코드 (foreach + isset 체크)
$orders = [
    ['customer' => 'John', 'amount' => 100],
    ['customer' => 'Jane', 'amount' => 200],
    ['customer' => 'John', 'amount' => 150]
];

$grouped = [];
foreach ($orders as $order) {
    if (!isset($grouped[$order['customer']])) {
        $grouped[$order['customer']] = 0;
    }
    $grouped[$order['customer']] += $order['amount'];
}
// 결과: ['John' => 250, 'Jane' => 200]
✓ 올바른 코드 (array_reduce로 선언적 표현)
$orders = [
    ['customer' => 'John', 'amount' => 100],
    ['customer' => 'Jane', 'amount' => 200],
    ['customer' => 'John', 'amount' => 150]
];

$grouped = array_reduce($orders, function($carry, $order) {
    $customer = $order['customer'];
    $carry[$customer] = ($carry[$customer] ?? 0) + $order['amount'];
    return $carry;
}, []);
// 결과: ['John' => 250, 'Jane' => 200]

왜 이게 더 나은가: Null Coalescing Operator(??)로 초기값 체크를 더 간결하게 처리한다.

패턴 5) 배열 요소를 문자열로 연결

배열 요소들을 특정 구분자로 연결해야 할 때 (implode()의 대안).

✓ 올바른 코드
$items = ['apple', 'banana', 'cherry'];

$result = array_reduce($items, function($carry, $item) {
    return $carry === '' ? $item : $carry . ', ' . $item;
}, '');
echo $result;  // apple, banana, cherry

참고: 이 경우 implode()가 더 간단하지만, 조건부 처리가 필요하면 array_reduce()를 쓴다.

 

3단계: 실전 예제 - 장바구니 총액 계산

실무에서 자주 마주치는 e-commerce 장바구니 시나리오를 구현해보자.

// DB에서 조회한 장바구니 상품 데이터
$cart_items = [
    [
        'product_id' => 1,
        'name' => '노트북',
        'price' => 1500000,
        'quantity' => 1,
        'discount_rate' => 0.1  // 10% 할인
    ],
    [
        'product_id' => 2,
        'name' => '마우스',
        'price' => 50000,
        'quantity' => 2,
        'discount_rate' => 0  // 할인 없음
    ],
    [
        'product_id' => 3,
        'name' => '키보드',
        'price' => 150000,
        'quantity' => 1,
        'discount_rate' => 0.05  // 5% 할인
    ]
];

// array_reduce로 총액 계산
$summary = array_reduce($cart_items, function($carry, $item) {
    // 상품별 소계 = 가격 × 수량
    $subtotal = $item['price'] * $item['quantity'];
    
    // 할인 적용
    $discount = $subtotal * $item['discount_rate'];
    $total = $subtotal - $discount;
    
    // 누적
    $carry['subtotal'] += $subtotal;
    $carry['total_discount'] += $discount;
    $carry['grand_total'] += $total;
    $carry['item_count']++;
    
    return $carry;
}, [
    'subtotal' => 0,
    'total_discount' => 0,
    'grand_total' => 0,
    'item_count' => 0
]);

echo "상품 수: " . $summary['item_count'] . "\n";           // 3
echo "원가 합계: " . $summary['subtotal'] . "\n";            // 1850000
echo "총 할인액: " . $summary['total_discount'] . "\n";       // 157500
echo "최종 결제액: " . $summary['grand_total'] . "\n";        // 1692500

이 예제의 장점:

  • 복잡한 계산을 선언적으로 표현
  • 상태 관리가 콜백 함수 내에 격리됨
  • 여러 누적값을 배열로 한번에 반환 가능
  • 로직 수정이 한 곳에서만 이루어짐

 

4단계: 주의사항과 흔한 실수
❌ 실수 1) 초기값(initial)을 생략하면 첫 요소가 $carry로 시작
✗ 잘못된 코드
$numbers = [1, 2, 3, 4];
$result = array_reduce($numbers, function($carry, $item) {
    return $carry + $item;  // $carry가 정수인지 배열인지 불명확
});
echo $result;  // 10 (우연히 작동)

// 다만 빈 배열이면 null 반환
$empty = [];
$result = array_reduce($empty, function($carry, $item) {
    return $carry + $item;
});
var_dump($result);  // NULL (예상치 못한 결과)
✓ 올바른 코드
$numbers = [1, 2, 3, 4];
$result = array_reduce($numbers, function($carry, $item) {
    return $carry + $item;
}, 0);  // 초기값 명시
echo $result;  // 10

// 빈 배열 안전성
$empty = [];
$result = array_reduce($empty, function($carry, $item) {
    return $carry + $item;
}, 0);
echo $result;  // 0 (예상한 결과)
❌ 실수 2) 연관배열에서 $carry를 배열로 처리할 때 참조 반환 잊기
✗ 잘못된 코드 (변수 재할당 필요)
$data = ['a' => 1, 'b' => 2];
$result = array_reduce($data, function($carry, $item) {
    $carry[] = $item * 2;  // 이 라인이 실행되지만
    // return이 없으면 누적되지 않음
}, []);
var_dump($result);  // NULL 또는 처음 값
✓ 올바른 코드
$data = ['a' => 1, 'b' => 2];
$result = array_reduce($data, function($carry, $item) {
    $carry[] = $item * 2;
    return $carry;  // 반드시 $carry를 반환
}, []);
var_dump($result);  // [2, 4]
❌ 실수 3) 복잡한 로직에서 콜백 함수가 너무 길어지기
✗ 가독성 떨어지는 코드
$result = array_reduce($data, function($c, $i) {
    if ($i['status'] === 'active') {
        $i['amount'] = $i['price'] * $i['qty'];
        if ($i['amount'] > 1000000) $i['tax'] = $i['amount'] * 0.1;
        $c[] = $i;
    }
    return $c;
}, []);
✓ 가독성 좋은 코드
$process_item = function($item) {
    if ($item['status'] !== 'active') return null;
    $item['amount'] = $item['price'] * $item['qty'];
    if ($item['amount'] > 1000000) $item['tax'] = $item['amount'] * 0.1;
    return $item;
};

$result = array_reduce($data, function($carry, $item) use ($process_item) {
    $processed = $process_item($item);
    if ($processed) $carry[] = $processed;
    return $carry;
}, []);

 

5단계: array_reduce() vs 다른 배열 함수와의 비교
함수 용도 반환값 사용 시기
array_reduce() 배열 → 단일값/배열로 축약 모든 타입 가능 누적 계산, 그룹화, 합계
array_map() 배열의 각 요소 변환 새로운 배열 각 요소를 독립적으로 처리
array_filter() 조건에 맞는 요소만 추출 필터된 배열 조건부 필터링
foreach 반복 처리 없음 (사이드 이펙트) 복잡한 로직, 중단 필요시

 

결론

array_reduce()는 배열을 단일값으로 축약하는 가장 강력한 도구다. 합계 계산에서 시작해 복잡한 데이터 변환까지, 반복문 대신 선언적인 함수형 프로그래밍으로 코드를 짤 수 있다. 초기값을 명시하고, 복잡한 로직은 별도 함수로 분리하고, 반드시 $carry를 반환하는 세 가지 습관만 지키면, 버그 없고 유지보수하기 좋은 코드를 만들 수 있다.

이 글의 '패턴 4 (그룹화)' 또는 '장바구니 예제'를 참고해 당신의 프로젝트에서 복잡하게 얽힌 foreach 반복문을 array_reduce()로 리팩토링해보면, 코드 가독성과 안정성이 한 단계 업그레이드될 것이다.