Tong's Blog

[Swift] Combine Transforming Operator 정리 [map, tryMap, compactMap, flatMap, scan, replaceNil] + RxSwift 비교 본문

iOS/Combine

[Swift] Combine Transforming Operator 정리 [map, tryMap, compactMap, flatMap, scan, replaceNil] + RxSwift 비교

통스 2026. 1. 6. 00:03
반응형

안녕하세요.

지난 글에서는 Combine Publisher 종류를 정리하고, 각각 RxSwift의 어떤 컴포넌트와 대응되는지 살펴봤습니다.
이번 3편에서는 실무에서 가장 많이 사용되는 Combine Operator 들을 정리하고, RxSwift의 동일 기능과 어떤 차이가 있는지 비교해보려고 합니다.

 

Operator란?

Publisher에서 전달되는 값들을 변환 / 필터링 / 조합 / 제어하는 중간 처리 단위입니다.

즉, 이벤트 흐름을 원하는 형태로 가공하여 Subscriber가 필요한 데이터만 받을 수 있도록 만들어주는 역할입니다.

 

이번 포스팅에서는 그중에서도 Transforming Operator 중심으로 설명과 비교를 해보겠습니다.

 

변환(Transforming) Operator란?

이름 그대로, '들어온 값을 다른 값으로 바꿔서 흘려보내는 역할'을 하는 Operator 입니다.

 

  • 값 자체를 바꾸는 경우: map, tryMap, compactMap
  • 다른 Publisher로 바꾸는 경우: flatMap
  • 이전 값과 누적해서 새로운 값을 만드는 경우: scan
  • nil을 다른 값으로 치환하는 경우: replaceNil
Publisher
    .map { ... }        // 값 변환
    .flatMap { ... }    // 비동기 변환
    .scan(…)            // 누적 계산
    .sink { ... }       // 최종 소비

 

이제 하나씩, Combine 코드 → RxSwift 비교 순으로 보겠습니다.

 

1. map - element 값 변환

https://developer.apple.com/documentation/combine/publisher/map(_:)-99evh

 

map(_:) | Apple Developer Documentation

Transforms all elements from the upstream publisher with a provided closure.

developer.apple.com

 

들어온 값을 다른 값으로 '1:1 매핑' 하는 가장 기본적인 연산자입니다.

Combine 예제

[1, 2, 3].publisher
    .map { $0 * 10 }
    .sink { value in
        print("값: \(value)")
    }
    .store(in: &bag)
// 값: 10
// 값: 20
// 값: 30

RxSwift

Observable.from([1, 2, 3])
    .map { $0 * 10 }
    .subscribe(onNext: { value in
        print("값: \(value)")
    })
    .disposed(by: disposeBag)

 

2. tryMap - 에러를 던질 수 있는 map

map 내부에서 throw 를 하고 싶을 때 사용하는 버전입니다.
에러 타입이 Never → Error 로 바뀝니다.

Combine 예제

["1", "2", "a", "4"].publisher
    .tryMap { value -> Int in
        guard let intValue = Int(value) else {
            throw ParseError()
        }
        return intValue
    }
    .sink(
        receiveCompletion: { print("완료: \($0)") },
        receiveValue: { print("값: \($0)") }
    )
    .store(in: &bag)

// 값: 1
// 값: 2
// 완료: failure(CombineRxSwift.ViewController.ParseError())

RxSwift

RxSwift에서는 map 클로저 안에서 바로 throw 가 가능합니다.
Observable의 에러 타입이 항상 Error 이기 때문에 별도 tryMap 이 필요 없습니다.

Observable.from(["1", "2", "a", "4"])
    .map { value -> Int in
        guard let intValue = Int(value) else {
            throw ParseError()
        }
        return intValue
    }
    .subscribe(
        onNext: { print("값: \($0)") },
        onError: { print("에러: \($0)") }
    )
    .disposed(by: disposeBag)

3. compactMap – nil 제거하면서 변환

변환 결과가 Optional 일 수 있을 때 nil 은 버리고, Optional 의 wrapped 값만 흐르게 하는 연산자

 

Combine

["1", "2", "a", "4"].publisher
    .compactMap { Int($0) }
    .sink { value in
        print("값: \(value)")
    }
    .store(in: &bag)

// 출력: 1, 2, 4   ('a'는 Int로 변환 실패 → nil → 필터링)

 

 

 

RxSwift

Observable.from(["1", "2", "a", "4"])
    .compactMap { Int($0) }
    .subscribe(onNext: { value in
        print("값: \(value)")
    })
    .disposed(by: disposeBag)

4. flatMap – 비동기 스트림을 평탄화

map 은 '값 → 값' 변환이라면,
flatMap 은 '값 → Publisher' 로 바꾸고, 그 Publisher의 결과를 다시 한 스트림으로 평탄화합니다.

Combine

func fetchUser(id: Int) -> AnyPublisher<String, Never> {
    Just("User_\(id)")
        .delay(for: .milliseconds(100), scheduler: RunLoop.main)
        .eraseToAnyPublisher()
}

[1, 2, 3].publisher
    .flatMap { id in
        fetchUser(id: id)
    }
    .sink { value in
        print("결과:", value)
    }
    .store(in: &bag)
// 출력: User_1, User_2, User_3 (비동기 순서에 따라 달라질 수 있음)

 

RxSwift

func fetchUser(id: Int) -> Observable<String> {
    Observable.just("User_\(id)")
        .delay(.milliseconds(100), scheduler: MainScheduler.instance)
}

Observable.from([1, 2, 3])
    .flatMap { id in
        fetchUser(id: id)
    }
    .subscribe(onNext: { value in
        print("결과:", value)
    })
    .disposed(by: disposeBag)

5. scan – 이전 값을 누적하면서 새로운 값 생성

'누적 합' 같은 상황에서 자주 쓰입니다.
reduce 가 '끝나고 한 번만 결과를 내보내는' 것이라면,
scan 은 매 단계마다 중간 누적값을 모두 흘려보냅니다.

Combine

[1, 2, 3, 4].publisher
    .scan(0) { accumulated, newValue in
        accumulated + newValue
    }
    .sink { value in
        print("누적값:", value)
    }
    .store(in: &bag)
// 출력: 1, 3, 6, 10

RxSwift

Observable.from([1, 2, 3, 4])
    .scan(0) { accumulated, newValue in
        accumulated + newValue
    }
    .subscribe(onNext: { value in
        print("누적값:", value)
    })
    .disposed(by: disposeBag)

6. replaceNil – nil을 기본값으로 치환

Combine

let values: [Int?] = [1, nil, 3]

values.publisher
    .replaceNil(with: 0)
    .sink { value in
        print("값:", value)
    }
    .store(in: &bag)
    
// 값: Optional(1)
// 값: Optional(0)
// 값: Optional(3)

RxSwift

RxSwift에는 replaceNil 이라는 이름의 기본 연산자는 없고, 대신 아래처럼 조합해서 만듭니다.

Observable.from([1, nil, 3] as [Int?])
    .map { $0 ?? 0 }
    .subscribe(onNext: { value in
        print("값:", value)
    })
    .disposed(by: disposeBag)
    
// 값: 1
// 값: 0
// 값: 3

7.  한 번에 비교

기능 Combine RxSwift 설명
단순 값 변환 map map 가장 기본적인 값 변환
에러 던지는 변환 tryMap map + throw Combine은 에러 타입이 Never가 아닐 수 있음
Optional 제거 변환 compactMap compactMap nil 제거 후 wrapped 값만 전달
Publisher 평탄화 flatMap flatMap 값 → Publisher → 다시 단일 스트림
누적값 생성 scan scan 단계별 누적 결과 스트림
nil 치환 replaceNil map { $0 ?? default } Combine 전용 편의 연산자

네트워크 사용 예시

Combine

struct SearchResponse: Decodable {
    let items: [Item]
}

struct Item: Decodable {
    let title: String
}

func search(query: String) -> AnyPublisher<[String], Error> {
    api.search(query: query) // AnyPublisher<Data, Error>
        .decode(type: SearchResponse.self, decoder: JSONDecoder())
        .map { $0.items } // [Item]
        .map { items in items.map { $0.title } } // [String]
        .eraseToAnyPublisher()
}

RxSwift

func search(query: String) -> Observable<[String]> {
    api.search(query: query) // Observable<Data>
        .map { data -> SearchResponse in
            try JSONDecoder().decode(SearchResponse.self, from: data)
        }
        .map { $0.items.map { $0.title } }
}

 

이번 3편에서는 Combine의 변환 Operator 들을 중심으로 살펴봤습니다.

그리고 각각이 RxSwift에서는 어떤 연산자로 대응되는지도 함께 비교해봤습니다.

읽어주셔서 감사합니다.

반응형
Comments