개발/Swift
Swift에서 map vs forEach
까칠코더
2025. 11. 11. 12:03
반응형
Swift에서 map vs forEach
Swift 컬렉션을 순회할 때 자주 쓰는 두 메서드가 있습니다.
- map : 각 요소를 변환하여 새 배열을 반환
- forEach : 각 요소에 대해 동작(사이드 이펙트) 을 수행, 반환값 없음
용도와 동작이 다르므로 상황에 맞게 선택해야 합니다. 아래에 개념, 성능, 예제, 주의사항까지 정리했습니다.
1. 핵심 비교 요약
| 항목 | map | forEach |
| 목적 | 변환(Transformation) | 동작(Side effect) |
| 반환값 | 변환된 새 배열 | 없음(Void) |
| 중간 자료구조 | 생성됨(새 배열) | 생성 안 함 |
| 조기 종료 | 불가(일반적) | 불가(내장 break/continue 없음) |
| 가독성 | “변환해서 쓰려는 의도”에 명확 | “순회하면서 무언가 실행”에 명확 |
| Lazy 결합 | lazy.map으로 지연 변환 가능 | lazy.forEach 없음 → forEach는 항상 즉시 실행 |
| 예외 처리 | rethrows (클로저가 throws면 던질 수 있음) | rethrows (동일) |
존재 여부만 확인 · 첫 매칭 찾기 등은 contains(where:), first(where:) 같은 전용 API를 쓰는 것이 더 효율적입니다.
2. 언제 map을 쓰나? — “변환 결과가 필요할 때”
기본 예제
let numbers = [1, 2, 3]
let squared = numbers.map { $0 * $0 } // [1, 4, 9]
구조 변환
struct User { let id: Int; let name: String }
let users = [User(id: 1, name: "A"), User(id: 2, name: "B")]
let names = users.map(\.name) // ["A", "B"]
Optional 변환은 compactMap
let raw = ["1", "two", "3"]
let ints = raw.compactMap(Int.init) // [1, 3] (nil 제거)
Lazy와 결합 (중간 배열 제거)
let big = Array(0..<1_000_000)
let doubledPrefix100 = Array(big.lazy.map { $0 * 2 }.prefix(100))
3. 언제 forEach를 쓰나? — “반환값이 필요 없고 동작만 할 때”
로그/통계/누적 갱신 등
let names = ["Kim", "Lee", "Park"]
names.forEach { print($0) } // 출력만
inout/외부 상태 갱신(신중히)
var sum = 0
[1,2,3].forEach { sum += $0 } // 누적
state 변경이 필요하면 reduce(into:)가 더 명료·효율적인 경우가 많습니다.
let sum2 = [1,2,3].reduce(0, +) // 합
let counts = ["a","b","a"].reduce(into: [:]) { $0[$1, default: 0] += 1 }
4. 조기 종료(early exit)가 필요할 땐?
- map/forEach는 내장된 break/continue가 없습니다.
- 조기 종료 목적이라면 아래 대안을 우선 고려하세요.
권장 대안
// 존재 여부만 확인
if arr.contains(where: { $0.isPrime }) { /* ... */ }
// 첫 매칭만 필요
if let first = arr.first(where: { $0.isPrime }) { /* ... */ }
// 일반 for 루프 (break/continue 사용 가능)
for x in arr {
if predicate(x) { break }
}
예외로 탈출하는 패턴(가능하지만 권장 X)
enum Break: Error { case stop }
do {
try arr.forEach {
if shouldStop($0) { throw Break.stop }
// ...
}
} catch Break.stop {
// 중단
}
5. 성능/메모리 관점
- map은 새 배열을 만든다 → 메모리 사용 증가. 큰 컬렉션에서는 lazy.map + 종단(materialize) 지점 최소화.
- forEach는 반환이 없고 배열을 만들지 않는다 → 부작용만 수행.
- “변환 결과가 필요 없는데 map을 쓰는 것”은 반복적인 실수입니다.
비교 예제
let users = Array(repeating: "user", count: 100_000)
// 나쁜 예: 결과를 쓰지 않는데 map 사용 → 큰 임시 배열 생성
_ = users.map { $0.uppercased() } // 🚫
// 좋은 예: 단순 로그/사이드 이펙트면 forEach
users.forEach { _ = $0.uppercased() } // ✅ (반환 버림, 임시 배열 없음)
// 더 좋은 예: 정말로 결과 문자열이 필요 없다면 변환도 피하기
users.forEach { _ in /* 로그 등 필요한 동작만 */ }
6. 문자열/조인 시 주의 — joined가 최적
let words = ["Hello", "Swift", "World"]
// 문자열 결합은 joined가 가장 효율적
let s1 = words.joined(separator: " ") // ✅
let s2 = words.reduce("") { $0 + ($0.isEmpty ? "" : " ") + $1 } // 🚫
7. 사전(Dictionary)과 순회
let dict = ["a": 1, "b": 2, "c": 3]
// 변환 결과가 필요하면 map
let keys = dict.map { $0.key } // ["a","b","c"] (순서는 해시 특성상 비결정적일 수 있음)
// 단순 출력/사이드 이펙트면 forEach
dict.forEach { k, v in print(k, v) }
주의: Dictionary는 해시 기반이라 삽입 순서를 보장하지 않습니다(버전에 따라 구현 세부가 달라질 수 있음). 순서가 필요하면 sorted(by:) 후 처리하세요.
8. 흔한 안티 패턴 → 올바른 대체
8.1 “map으로 부수 효과”
// 🚫 반환값을 사용하지 않는 map
_ = items.map { print($0) }
// ✅ forEach 사용
items.forEach { print($0) }
8.2 “filter + first” 대신 first(where:)
// 🚫 불필요한 중간 배열 생성
let firstBig = items.filter { $0 > 10 }.first
// ✅ 바로 첫 매칭
let firstBig2 = items.first(where: { $0 > 10 })
8.3 “filter + count > 0” 대신 contains(where:)
let hasBig = items.contains(where: { $0 > 10 }) // ✅
9. Lazy와의 조합 팁
변환 체인 길고 결과를 정말 마지막에만 쓰고 싶다면
let output = Array(
data.lazy
.map(expensiveTransform)
.filter(isNeeded)
.prefix(100) // 조기 종료 유도
)
forEach는 지연되지 않으므로, 측정 비용이 큰 변환을 섞을 땐 map + 다른 종단 연산(first/contains/prefix/Array)로 설계하는 편이 예측 가능합니다.
10. 예제 모음
10.1 URL 문자열을 URL로 변환 (실패 제외)
let urls = ["https://apple.com", "invalid", "https://swift.org"]
let valid = urls.compactMap(URL.init(string:)) // [URL]
10.2 사용자 이름을 대문자로 변환하여 화면에 표시
let upper = users.map(\.name.uppercased) // 변환 결과가 필요
render(upper)
10.3 서버 로그 전송 (반환 불필요)
events.forEach { sendLog($0) }
10.4 합계/그룹핑은 reduce(into:)
let total = prices.reduce(0, +)
let buckets = values.reduce(into: [:]) { $0[$1.category, default: []].append($1) }
11. 선택 가이드
| 상황 | 권장 메서드 | 이유 |
| 변환 결과를 사용 | map / compactMap | 새 배열 반환 |
| 단순 동작 수행 | forEach | 반환 없음, 임시 배열 없음 |
| 존재 여부 확인 | contains(where:) | 조기 종료 |
| 첫 매칭 요소 | first(where:) | 조기 종료 |
| 누적/집계 | reduce / reduce(into:) | in-place 업데이트 |
| 정렬 후 순회 | sorted(by:) + 루프 | 결정적 순서 |
12. 참조 링크
- Sequence.map(:)
https://developer.apple.com/documentation/swift/sequence/map(:) - Sequence.forEach(:)
https://developer.apple.com/documentation/swift/sequence/foreach(:) - Sequence.compactMap(:)
https://developer.apple.com/documentation/swift/sequence/compactmap(:) - Sequence.first(where:) / contains(where:)
https://developer.apple.com/documentation/swift/sequence/first(where:)https://developer.apple.com/documentation/swift/sequence/contains(where:) - Dictionary 순회
https://developer.apple.com/documentation/swift/dictionary
13. 결론
- 결과가 필요하면 map, 행동만 필요하면 forEach
- 조기 종료가 필요하면 contains/first 또는 일반 for 루프 사용
- 대용량 변환 체인은 lazy + 종단 연산 패턴으로 중간 배열을 피하고 메모리/시간을 절약
반응형