ConcatMap?

프로젝트내에 동영상 업로드 로직을 RxSwift로 교체하는 과정에서 concatMap연산자를 활용 하게 되어 한번 내용을 정리해 보려고 한다

What is ConcatMap?

concatMap

  • flatMap과 비슷한 함수, 파이프를 합쳐 새로운 시퀀스를 반환한다
  • flatMap은 시퀀스에서 방출되는 이벤트의 순서를 보장하지 않지만 concatMap은
    이벤트를 방출하는 순서를 보장한다

이번 동영상업로드 로직은 여러개의 동영상을 업로드 하는 과정에서 크기가 작은 영상의 업로드 가 먼저 진행되어 결과적으로 업로드된 동영상의 array 순서가 달라지는 일이 발생하였다
flatMap 으로 업로드 로직을 처리하고 영상의 path 값을 방출하는 방식이였는데 flatMap을 concatMap으로 바꿔주니 간단히 해결되었다
그럼 코드를 한번 살펴보자

example code

1. 동영상을 업로드할 s3 url 호출 함수


private let mediaProvider = APIService.shared.mediaAPI // media관련 network MoyaProvider

private func getPath(_ type: MediaType) -> Observable<MediaResponse> {
    let path =  mediaProvider.rx
        .request(.getMediaPath(type: type)) // getMediaPath를 통해 주소를 받음
        .filterSuccessfulStatusCodes()
        .map(MediaResponse.self) // path, url 로 나뉘어진 model
    return path.asObservable()
}

2. 동영상 URL을 받아 업로드 로직을 실행할 함수 생성

func uploadVideos(_ videos: [URL]) -> Observable<(Int, String?)> {
        
    return Observable.from(videos)
        .flatMap // -> concatMap으로 변경! 
        { video -> Observable<(URL, MediaResponse)> in
            Observable.zip(Observable.just(video), self.getPath(.video))
             // zip을 통해 video와 mediaResponse를 하나의 observable로 리턴시켜줌
        }
        .flatMap // -> concatMap으로 변경!
        { video, mediaResponse -> Observable<(Moya.Response, String)> in
            let uploadURL = URL(string: mediaResponse.url)!
            let uploadResponse = self.mediaProvider.rx.request(.uploadVideo(video, url: uploadURL))
            // mediaProvider를 통해 uploadResponse를 single형태로 리턴받는다
            return Observable.zip(
                uploadResponse.asObservable(), // single을 observable로 변환시켜줌
                Observable.just(mediaResponse.path)
            )
        }
        .enumerated() // index를 반환시키기 위한 연산자
        .map { index, response -> (Int, String?) in
            guard response.0.response?.statusCode ?? 500 == 200 else {
                return (index, nil) // 오류처리를 위한 nil 반환
            }
            return (index, response.1)
        }
    }

3. 동영상 Array를 파라미터로 전달할 업로드 함수 생성

private var videoPathList: [String] = [] //생성된 videoPath값을 저장할 Array

func upload(_ videos: [URL]) {
    MediaUploadManager().uploadVideos(videos).subscribe(onNext: { [weak self] index, path in
        guard let self = self else { return }
        guard let path = path else { // 네트워크 에러시 path값이 방출되지 않으므로 오류처리 
            print("uploadVideo_error: errorIndex = \(index)")
            self.videoPathList.removeAll()
            return
        }
            
        self.videoPathList.append(path)  //시퀀스가 진행됨에 따라 video의 path값을 저장해준다
        if index == videos.count - 1 { 
            // index와 업로드할 동영상갯수 - 1 이 같아지면 피드에 동영상 path어레이를 담아 업로드한다
            self.uploadFeed(videoPathList: self.videoPathList) // 피드 업로드 로직 실행
        }
    })
    .disposed(by: disposeBag)
}

4. 업로드 로직 호출하여 사용

private var selectedVideos: [URL] = [] // 선택한 동영상이 담길 Array

@objc func didTapDoneButton(_ sender: UIButton) {
    upload(selectedVideos)
}
  • 2번의 시퀀스에서 flatMap을 그대로 사용하면 업로드할 path값이 담겨있는 videoPathList의 순서가 처음에 담았던 동영상 array 의 순서와 다르게 뒤죽박죽이 되어버린다
  • 해당 로직에서 flatMap을 concatMap으로 변경만 시켜주면 내가 업로드시키고 싶었던 순서대로 잘 작동하게된다!
  • (해당 연산자를 몰랐더라면 아마 index까지 같이 반환받아 딕셔너리나 튜플형태로로 저장하여 업로드전에 index순서대로 정렬시키는 과정을 추가했을 것 같다… ㅠ)