반응형
Swift에서 reduce(into:) vs reduce
Swift의 reduce 계열은 컬렉션을 하나의 누적 결과로 접어(fold) 만드는 핵심 API입니다. 비슷해 보이지만 reduce(into:)와 reduce는 메모리/성능 특성과 사용 의도가 다릅니다. 실무에서 어떤 것을 선택해야 하는지 예제와 함께 정리합니다.
1. 핵심 비교 요약
| 항목 | reduce(into:_:) | reduce(_:_:) |
| 누적자(accumulator) | 가변(inout) 누적자를 인자로 전달하여 제자리(in‑place) 수정 | 불변(immutable) 누적자를 매 단계 새로 생성하여 반환 |
| 메모리/성능 | 복사 최소화 (특히 Array/Dictionary/Set 등 COW 타입) | 누적자 복사가 잦아질 수 있어 상대적으로 느릴 수 있음 |
| 사용 목적 | 집계/그룹화/누적 갱신 등 상태를 바꾸며 쌓는패턴 | 수학적 합성, 순수 함수형 변환 등 새 값을 만드는 패턴 |
| 가독성 | 명령형(in‑place) 스타일에 명확 | 함수형(immutable) 스타일에 명확 |
| 예시 | 카운팅, 버킷/그룹핑, 딕셔너리/집합 빌드 | 합계, 곱, 문자열 결합(하지만 joined가 보통 더 효율적) |
2. 시그니처와 동작 차이
// 1) reduce(into:)
func reduce<Result>(into initialResult: Result, _ updateAccumulatingResult: (inout Result, Element) throws -> Void) rethrows -> Result
// 2) reduce
func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result
- reduce(into:): 누적자를 inout으로 받아 제자리 수정합니다. 클로저는 값을 반환하지 않고 누적자만 변경합니다.
- reduce: 누적자의 새 값을 매 스텝 반환합니다. 불변 값 조합에 적합합니다.
3. 왜 into가 더 빠를 수 있나? (COW와 복사 비용)
Swift 표준 컬렉션(Array/Dictionary/Set)은 Copy‑On‑Write(COW) 입니다. reduce는 단계마다 새 누적자를 만들어 반환하므로, 누적자가 커질수록 할당/복사 비용이 커질 수 있습니다. 반면 reduce(into:)는 단일 버퍼를 재사용하며 in‑place로 수정해 불필요한 복사/할당을 줄입니다.
큰 딕셔너리/배열을 누적 생성하는 경우 reduce(into:)가 체감상 유의미하게 빠른 경우가 많습니다.
4. 예제 모음
4.1 빈도수 카운트(히스토그램)
let words = ["a", "b", "a", "c", "b", "a"]
// 권장: reduce(into:)
let histogram1 = words.reduce(into: [:]) { acc, w in
acc[w, default: 0] += 1
}
// 대안: reduce (불변 누적자)
let histogram2 = words.reduce(into: [:]) { acc, w in // <- into가 훨씬 간결/효율적
acc[w, default: 0] += 1
}
같은 작업을 reduce로만 구현하려면 매 단계 새 딕셔너리를 만들어야 하므로 비효율적이고 장황해집니다.
4.2 그룹핑(카테고리 → 배열)
struct Item { let cat: String; let value: Int }
let items = [Item(cat: "A", value: 1), Item(cat: "B", value: 2), Item(cat: "A", value: 3)]
let buckets = items.reduce(into: [String: [Item]]()) { acc, it in
acc[it.cat, default: []].append(it)
}
4.3 집합(Set) 빌드
let names = ["kim", "lee", "kim", "park"]
let unique = names.reduce(into: Set<String>()) { acc, n in
acc.insert(n)
}
4.4 문자열/배열 합성 (하지만 표준 전용 API 권장)
let words = ["Hello", "Swift", "World"]
// reduce로 문자열 결합 (가능하지만 비권장)
let combined1 = words.reduce("") { $0 + ($0.isEmpty ? "" : " ") + $1 }
// 권장: joined (문자열 전용 고성능 API)
let combined2 = words.joined(separator: " ")
4.5 수치 집계는 reduce가 자연스러움
let nums = [1, 2, 3, 4]
// 합/곱/최댓값 등 순수 함수형 조합
let sum = nums.reduce(0, +)
let product = nums.reduce(1, *)
let maxVal = nums.reduce(Int.min, max)
4.6 커스텀 누적 타입 빌드 (성능상 into 선호)
struct Stats { var min = Int.max; var max = Int.min; var sum = 0; var count = 0 }
let stats = nums.reduce(into: Stats()) { s, x in
s.min = Swift.min(s.min, x)
s.max = Swift.max(s.max, x)
s.sum += x
s.count += 1
}
let avg = Double(stats.sum) / Double(stats.count)
5. 언제 무엇을 써야 하나? 선택 기준
| 상황 | 권장 API | 이유/비고 |
| 딕셔너리/집합/배열을 점진적으로 “쌓는” 작업 | reduce(into:) | in‑place 갱신으로 빠름/간결 |
| 카운팅/그룹핑/버킷화 | reduce(into:) | default: 키 경합에 적합 |
| 순수 수치 집계(합, 곱, min/max 등) | reduce | 불변 조합이 가독성↑ |
| 문자열 결합 | joined | 전용 최적화 API가 더 효율적 |
| 존재/첫 매칭 확인 | contains/first | 조기 종료 |
| 중간 자료구조 피하고 싶음 | lazy + reduce(into:) | 대용량에서 메모리 절감 |
6. Lazy와 함께 쓰기
let big = (1...2_000_000)
// 중간 배열 없이 필터 후 합계
let evenSum = big.lazy
.filter { $0.isMultiple(of: 2) }
.reduce(0, +) // 전체 순회 필요 (종단 연산)
// 누적 자료구조 빌드는 into 선호
let top100Buckets = Array(
big.lazy
.map { ($0 % 10, $0) } // (키, 값)
.prefix(1000) // 조기 종료
).reduce(into: [Int: [Int]]()) { acc, pair in
acc[pair.0, default: []].append(pair.1)
}
7. 흔한 안티패턴과 교정
1) 큰 누적자를 reduce로 생성
→ reduce(into:)로 전환하여 복사 비용 절감.
2) 문자열/배열 결합을 reduce로 구현
→ joined 또는 append(contentsOf:) 사용.
3) 존재 확인을 reduce로 계산
→ contains(where:)/first(where:)가 의미/성능 모두 우수.
4) 부수 효과 많은 클로저
→ 누적 로직은 순수하게 유지. I/O나 로그는 외부로 분리.
8. 간단 벤치마크 스케치 (개념 설명용)
import Foundation
let input = (0..<200_000).map { "v\($0)" }
func measure(_ name: String, _ block: () -> Void) {
let t1 = CFAbsoluteTimeGetCurrent(); block(); let t2 = CFAbsoluteTimeGetCurrent()
print(name, ":", t2 - t1, "sec")
}
// 1) Dictionary 누적: reduce(into:) vs reduce
measure("into-dict") {
_ = input.reduce(into: [:]) { (acc: inout [String: Int], v) in acc[v] = v.count }
}
measure("reduce-dict") {
_ = input.reduce([:]) { (acc: [String: Int], v) in
var next = acc; next[v] = v.count; return next
}
}
일반적으로 into-dict가 더 빠르고, 메모리 사용량도 적습니다(환경에 따라 다름).
9. 참조 링크
- Sequence.reduce(_:_:)
https://developer.apple.com/documentation/swift/sequence/reduce(_:_:) - Sequence.reduce(into:_:)
https://developer.apple.com/documentation/swift/sequence/reduce(into:_:) - Dictionary 서브스크립트 default:
https://developer.apple.com/documentation/swift/dictionary/3127163-subscript - Set 소개
https://developer.apple.com/documentation/swift/set
10. 결론
- 상태를 쌓는 누적(집계/그룹화/딕셔너리/집합 빌드) → reduce(into:)가 성능·가독성 모두 우세.
- 순수 함수형 조합(합/곱/최댓값 등) → reduce가 자연스럽고 간결.
- 문자열 결합/존재 확인/첫 매칭 등은 전용 API(joined, contains, first)를 우선 고려.
반응형
'Dev Study > Swift' 카테고리의 다른 글
| Swift에서 문자열 결합을 위해 joined(separator:) 사용하기 (0) | 2025.11.11 |
|---|---|
| Swift에서 guard let vs 중첩 if let (0) | 2025.11.11 |
| Swift에서 Set를 사용해서 포함(contains) 검사 (0) | 2025.11.11 |
| Swift에서 append(contentsOf:) vs 반복 append (0) | 2025.11.11 |
| Swift에서 compactMap vs map + filter (0) | 2025.11.11 |
| Swift에서 map vs forEach (0) | 2025.11.11 |
| Swift에서 Lazy 컬렉션 (0) | 2025.11.10 |
| Swift에서 contains(where:) vs filter().count > 0 (0) | 2025.11.10 |


