반응형
Swift에서 compactMap vs map + filter
Swift에서 옵셔널을 걸러내며 변환할 때 가장 흔히 비교되는 두 패턴이 있습니다.
- 권장: compactMap — 변환과 nil 제거를 한 번에
- 대안: map { … } + filter { $0 != nil } — 변환 후 수동으로 nil 제거
두 방식은 결과가 같을 수 있지만, 의도 표현·성능·메모리 사용에서 의미 있는 차이가 납니다.
1. 핵심 결론
- 옵셔널을 제거하며 변환하려면 compactMap이 표준적이고 효율적입니다.
- map + filter는 동등 동작이지만 임시 배열을 만들 가능성이 커 비효율적입니다.
- 대용량·체인 처리에선 lazy.compactMap으로 중간 배열 없이 처리하세요.
2. 의미 차이
| 목적 | 권장 표현 | 비권장/대체 |
| 변환 + nil 제거 | array.compactMap(transform) | array.map(transform).filter { $0 != nil }.map { $0! } |
| 변환만 | array.map(transform) | 동일 |
| 존재 여부 확인 | array.contains(where:) | filter.count > 0는 비권장 |
| 첫 매칭 요소 | array.first(where:) | filter.first는 중간 배열 발생 |
compactMap은 “nil 제거를 동반한 변환”이 목적일 때 의도를 가장 명확히 드러내는 표준 API입니다.
3. 성능·메모리 관점
| 항목 | compactMap | map + filter |
| 순회 횟수 | 1회 통합 처리 | 보통 2~3단계 (map → filter → (unwrap)) |
| 메모리 | O(k) (최종 결과) | O(n) 중간 배열(옵셔널 배열) + O(k) |
| 가독성 | 변환+제거를 한 눈에 | 스텝이 분리되어 장황 |
| Lazy 결합 | lazy.compactMap지원 | lazy로도 단계가 늘어남 |
compactMap은 내부적으로 변환과 nil 제거를 한 바퀴에 처리하도록 고안되어, 불필요한 중간 배열 생성을 피합니다.
4. 기본 예제
문자열을 정수로 변환 (실패한 변환은 버림)
let raw = ["1", "two", "3", "04"]
let ints = raw.compactMap(Int) // [1, 3, 4]
동일 동작을 map + filter로 작성하면:
let ints2 = raw
.map(Int.init) // [Optional(1), nil, Optional(3), Optional(4)]
.compactMap { $0 } // 또는 .filter { $0 != nil }.map { $0! }
두 결과는 동일하지만 compactMap(Int)가 간단하고 효율적입니다.
5. 변환이 무거울 때: Lazy와 결합
let big: [String] = loadHugeStrings()
// 중간 배열 없음, 필요한 시점에만 계산
let top100 = Array(
big.lazy
.compactMap(URL.init(string:)) // 변환 + nil 제거
.prefix(100) // 조기 종료
)
6. Optional 중첩(옵셔널의 옵셔널) 처리
let nested: [Int?] = [1, nil, 3, nil]
// 단순 제거
let flat = nested.compactMap { $0 } // [1, 3]
// map + filter 형태 (덜 권장)
let flat2 = nested.map { $0 }.compactMap { $0 }
7. 에러 처리와의 조합
compactMap 클로저에서 try?를 사용하면 실패 시 nil로 흡수되어 자연스럽게 걸러집니다.
struct User: Decodable { let id: Int; let name: String }
let blobs: [Data] = loadRawUserData()
let users = blobs.compactMap { try? JSONDecoder().decode(User.self, from: $0) }
에러를 구체적으로 다루고 싶다면 Typed throws 혹은 Result 타입으로 변환 후 분기하는 패턴을 쓰세요.
let decoded: [Result<User, Error>] = blobs.map {
Result { try JSONDecoder().decode(User.self, from: $0) }
}
let onlySuccess = decoded.compactMap { try? $0.get() }
let onlyErrors = decoded.compactMap { result -> Error? in
if case .failure(let e) = result { return e } else { return nil }
}
8. 흔한 실수와 주의점
- filter { $0 != nil } 후 강제 언래핑
→ compactMap이 더 안전·간결합니다. - 불필요한 이중 map/filter 체인
→ 한 번의 compactMap으로 통합. - 부수 효과를 동반한 클로저
→ 변환/정제 로직에는 부수 효과를 넣지 마세요(지연 평가/테스트 어려움). - 문자열 결합·존재 확인 등은 전용 API
→ 결합은 joined, 존재 확인은 contains(where:), 첫 매칭은 first(where:).
9. 실무 예제 모음
9.1 URL 변환 + 유효 URL만 수집
let lines = ["https://apple.com", "not_url", "https://developer.apple.com"]
let urls = lines.compactMap(URL.init(string:)) // [URL, URL]
9.2 파일 경로 → 데이터 로드 (실패 무시)
let paths: [String] = loadPaths()
let files: [Data] = paths.compactMap { try? Data(contentsOf: URL(fileURLWithPath: $0)) }
9.3 JSON 배열 파싱 (부분 실패 허용)
let rawObjects: [Data] = fetchRawJSONs()
let models = rawObjects.compactMap { try? JSONDecoder().decode(Model.self, from: $0) }
9.4 ID 문자열 → Int 변환 후 상위 10개만
let ids = ["10","11","foo","12","bar","99"]
let top10 = Array(ids.lazy.compactMap(Int).sorted().prefix(10))
10. flatMap과의 관계 (컬렉션 평탄화)
- Swift 4.1 이후 옵셔널 제거용 flatMap은 compactMap으로 역할 분리
- 현재 flatMap은 “시퀀스의 시퀀스를 평탄화”하는 의미로 사용합니다.
let matrix = [[1,2], [3,nil,4] as [Int?]]
let flattened = matrix.flatMap { $0 } // [[Int?]] → [Int?]
let intsOnly = flattened.compactMap { $0 } // [Int]
11. 간단 벤치마크 스케치 (개념 설명용)
import Foundation
let raw = (0..<1_000_00).map { String($0) + ($0.isMultiple(of: 7) ? "x" : "") }
func measure(_ name: String, _ block: () -> Void) {
let t1 = CFAbsoluteTimeGetCurrent()
block()
let t2 = CFAbsoluteTimeGetCurrent()
print(name, ":", t2 - t1, "sec")
}
measure("compactMap(Int)") {
_ = raw.compactMap(Int.init)
}
measure("map+filter+unwrap") {
_ = raw.map(Int.init).compactMap { $0 } // or .filter { $0 != nil }.map { $0! }
}
실제 수치는 환경에 따라 달라지지만, 원리상 compactMap이 중간 배열을 줄여더 효율적입니다.
12. 참조 링크
- Sequence.compactMap(_:)
https://developer.apple.com/documentation/swift/sequence/compactmap(_:) - Sequence.map(_:)
https://developer.apple.com/documentation/swift/sequence/map(_:) - Sequence.filter(_:)
https://developer.apple.com/documentation/swift/sequence/filter(_:) - Sequence.first(where:)
https://developer.apple.com/documentation/swift/sequence/first(where:) - Sequence.contains(where:)
https://developer.apple.com/documentation/swift/sequence/contains(where:) - Sequence.flatMap(_:) (평탄화 문맥)
https://developer.apple.com/documentation/swift/sequence/flatmap(_:)
13. 결론
- 옵셔널 제거 + 변환이 필요하면 항상 compactMap.
- 대용량 체인은 lazy.compactMap으로 중간 자료구조 없이 처리.
- 존재 확인이나 첫 매칭은 전용 API(contains, first)로 목적 지향적으로 작성.
반응형
'Dev Study > Swift' 카테고리의 다른 글
| 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에서 reduce(into:) vs reduce (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 |
| Swift에서 for-in vs 인덱스 기반 반복(0..<array.count) (0) | 2025.11.10 |

