개발/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. 참조 링크

 

13. 결론

  • 결과가 필요하면 map, 행동만 필요하면 forEach
  • 조기 종료가 필요하면 contains/first 또는 일반 for 루프 사용
  • 대용량 변환 체인은 lazy + 종단 연산 패턴으로 중간 배열을 피하고 메모리/시간을 절약
반응형