배열의 모든 요소를 순회하면서 누적된 결과값을 계산해야 할 때가 있다. 장바구니 총액 계산, 배열 요소 조건부 필터링 후 합산, 또는 복잡한 데이터 변환 같은 작업 말이다. 다만 대부분의 개발자들은 반복문(foreach)으로 변수를 하나하나 더하거나 빼면서 코드가 길어지고 유지보수하기 어려워진다는 걸 경험했을 것이다. 이번에는 PHP 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
가장 기본적인 사용 사례다. 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)를 따로 관리할 필요 없고, 순수 함수처럼 동작해서 버그가 줄어든다.
실무에서 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
왜 이게 더 나은가: 콜백 함수 내에서 타입을 명시적으로 변환하고 검증할 수 있다.
특정 조건을 만족하는 요소만 합산해야 할 때다.
✗ 잘못된 코드 (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
왜 이게 더 나은가: 반복 로직과 조건 검사가 한 곳에 명확하게 보인다.
배열 요소들을 특정 키로 그룹화해서 새로운 배열을 만드는 경우.
✗ 잘못된 코드 (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(??)로 초기값 체크를 더 간결하게 처리한다.
배열 요소들을 특정 구분자로 연결해야 할 때 (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()를 쓴다.
실무에서 자주 마주치는 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
이 예제의 장점:
- 복잡한 계산을 선언적으로 표현
- 상태 관리가 콜백 함수 내에 격리됨
- 여러 누적값을 배열로 한번에 반환 가능
- 로직 수정이 한 곳에서만 이루어짐
✗ 잘못된 코드
$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 (예상한 결과)
✗ 잘못된 코드 (변수 재할당 필요)
$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]
✗ 가독성 떨어지는 코드
$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;
}, []);
| 함수 | 용도 | 반환값 | 사용 시기 |
|---|---|---|---|
| array_reduce() | 배열 → 단일값/배열로 축약 | 모든 타입 가능 | 누적 계산, 그룹화, 합계 |
| array_map() | 배열의 각 요소 변환 | 새로운 배열 | 각 요소를 독립적으로 처리 |
| array_filter() | 조건에 맞는 요소만 추출 | 필터된 배열 | 조건부 필터링 |
| foreach | 반복 처리 | 없음 (사이드 이펙트) | 복잡한 로직, 중단 필요시 |
array_reduce()는 배열을 단일값으로 축약하는 가장 강력한 도구다. 합계 계산에서 시작해 복잡한 데이터 변환까지, 반복문 대신 선언적인 함수형 프로그래밍으로 코드를 짤 수 있다. 초기값을 명시하고, 복잡한 로직은 별도 함수로 분리하고, 반드시 $carry를 반환하는 세 가지 습관만 지키면, 버그 없고 유지보수하기 좋은 코드를 만들 수 있다.
이 글의 '패턴 4 (그룹화)' 또는 '장바구니 예제'를 참고해 당신의 프로젝트에서 복잡하게 얽힌 foreach 반복문을 array_reduce()로 리팩토링해보면, 코드 가독성과 안정성이 한 단계 업그레이드될 것이다.