반응형

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. 흔한 실수와 주의점

  1. filter { $0 != nil } 후 강제 언래핑
     compactMap이 더 안전·간결합니다.
  2. 불필요한 이중 map/filter 체인
    → 한 번의 compactMap으로 통합.
  3. 부수 효과를 동반한 클로저
    → 변환/정제 로직에는 부수 효과를 넣지 마세요(지연 평가/테스트 어려움).
  4. 문자열 결합·존재 확인 등은 전용 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. 참조 링크

 

13. 결론

  • 옵셔널 제거 + 변환이 필요하면 항상 compactMap.
  • 대용량 체인은 lazy.compactMap으로 중간 자료구조 없이 처리.
  • 존재 확인이나 첫 매칭은 전용 API(contains, first)로 목적 지향적으로 작성.
반응형
Posted by 까칠코더
,