iOS

RxSwift flatMap vs map 차이

Thor_yeom 2023. 11. 17. 09:26
RxSwift를 사용할때 flatMap에 대해 어디까지 알고 계신가요?

 

 

이번 포스팅에서는  flatMap에 대해 뜯고 맛보고 즐겨보고 map과의 차이점에 대해서 알아보겠습니다.

 

 

그럼 시작 !!

하기에 앞서 

 

일단! 간단히 map부터 보겠습니다.

저희가 알고 있던 map에 대해 예시를 통해 복습해보겠습니다.

 

let exampleInt = [1, 2, 3]

let exampleString = exampleInt.map { value -> String in
    return String(value)
} 

// ["1", "2", "3"]

 

이렇게 배열에서 하나씩 뽑아서 String으로 만든 다음 배열에 담는것이 

map의 역할인데요 

 

RxSwift에서도 똑같은 역할을 합니다.

예시 코드를 보겠습니다 

현재 Observable.just(1)의 타입은 Observable<Int> 입니다. 여기서 map을 한다면 obser1은 어떤 타입이 될까요?

 

 

 

 

 

 

 

 

 

 

 

 

 

정답은 Observable<Strting>이 됩니다.

 

즉, return  값으로 나온 결과 값에 Observable이 추가로 붙는 겁니다. 

그럼 예시를 들어볼까요??

현재 코드에서 obser의 값의 타입은 무엇일까요??

 

 

 

 

 

 

 

 

혹시 답이 틀리신 분들은 직접 만들어보시고 map에 다시한번 복습해보시길 바랍니다.!

flatMap을 알기전에 정말 알고 넘어가야 됩니다!

 

정답은 Observable<Observable<ValidateEmailResponse>>

 

 

다시 한번더 말씀 드리면 

return  값으로 나온 결과 값에 Observable이 추가로 붙는 겁니다. 

 

 

그렇다면 왜?? Observable이 추가로 붙는걸까??

 

아주 간단합니다. RxSwift에서 map의 역할을 그렇게 만들었기 때문입니다.

map의 타입을 보면 Return 값으로 Observable<Result>을 return 합니다.

 

 

 

 

자 그렇다면 flatMap은 어떻게 사용하는 걸까요??

 

제가 내린 결론 부터 말씀드리면 

 

지정한 리턴값으로 커스텀하게 만들 수 있다.

 

 

엥? 이게 무슨 말이지?  

예시 코드를 보면서 알아보겠습니다

 

일단 네트워크 통신할 때 flatMap을 사용하지 않을때 어떻게 RxSwift로 만들수 있을까요?

저는 버튼을 탭 했을때 네트워크 통신을 하려고 이렇게 구성했었습니다.

 /// 이메일 검증
        // 회원가입 할때 Email TextField에서 검증해야함
        validEmailBtn.rx.tap
            .bind(with: self) { owner, _ in
                APIManager.shared.requestIsValidateEmail(api: Router.valid(emial: "aas1234@sdsc1aa.com"))
                    .asDriver(onErrorJustReturn: ValidateEmailResponse(message: ""))
                    .drive(with: self) { owner, response in
                        dump(response)
                    }
                    .disposed(by: owner.disposeBag)
            }
            .disposed(by: disposeBag)

 

 

 

코드에 대해 뜯어보면 

버튼 탭 -> 네트워크 통신 -> ( 에러 발생시 asDriver로 핸들링 )  -> drive를 통해 response ( 현재 코드에서는

ValidateEmailResponse) 방출 -> dispose -> dispose 이렇게 됩니다.

 

버튼 탭을 했을 떄도 dispose를 해줘야하고 API통신이 끝났을때도 dispose를 해주고 있습니다.

빌드를 해보면 동작하는데 문제가 되지 않습니다.

 

 

But dispose가 두번되기 때문에 문제가 생길 수 있는 확률이 있습니다. 

또한 RxSwift 공식문서에서도 dispose를 두 번 쓰지 말라고 적혀있습니다.

 

 

그렇다면 버튼을 탭하고 네트워크 통신을 할때
dispose가 두번 쓰이는데 구현 할 수 있는 방법은 없을까??

 

 

여기서 나온것이 flatMap입니다.

 

물논! flatMap이 이렇게 나왔다는것은 없고... 뇌피셜입니다 하핫!

하지만 제가 프로젝트를 구현할때 이런 느낌으로 많이 사용했습니다

 

 

각설 하고 다시 본론으로 돌아오면 

여타 블로그에서 보면

"평탄화 작업이다"라고 표현되는 부분이 있는데 

 

왜 평탄화 작업이이라고 부르냐면

현재 코드에서의 두 개의 dispose한개의 dispose를 사용해서 구현 할 수 있습니다.

 

 

이제 코드를 보면서 지정한 커스텀 리턴값으로 반환 할 수 있다 에 대해서 알아 보겠습니다.

 

1. duplicatdTapped : 버튼을 터치했을때  타입 ControlProperty<Void>

2. withLatestFrom : 사용자가 적는 emailText를 나타냅니다. 타입은 ControlProperty<String>

 

 

 

자 그렇다면 현재 value의 타입은 ControlProperty<String> 이것이 되겠죠?

저는 controlProperty<String>값을 가지고 Observable<ValidateEmailResponse>로 만들고 싶습니다.

 

이럴때 flatMap을 사용합니다.  flatMap의 정의에 대해 다시 한번 말씀드리면 

 

지정한 리턴값으로 커스텀하게 만들 수 있다.

 

 

 

아니 그렇다면 map을 쓰면 어떻게 될까요??

저희가 지정한 return 값에 Observable이 감싸줘서 return이 됩니다...

 

 

여기가 이해되셨으면 다 이해하신겁니다!!!

 

정리를 해보자면 

아! flatMap은 사용자가 지정한 return 값으로 그대로 나오고

map은 지정한 return 값에 Observable이 감싸줘서 나오는군아

 

 

여기까지는 이해 완료 ! 그렇다면 문제가 되었던 코드를 다시보겠습니다.

 /// 이메일 검증
        // 회원가입 할때 Email TextField에서 검증해야함
        validEmailBtn.rx.tap
            .bind(with: self) { owner, _ in
                APIManager.shared.requestIsValidateEmail(api: Router.valid(emial: "aas1234@sdsc1aa.com"))
                    .asDriver(onErrorJustReturn: ValidateEmailResponse(message: ""))
                    .drive(with: self) { owner, response in
                        dump(response)
                    }
                    .disposed(by: owner.disposeBag)
            }
            .disposed(by: disposeBag)

 

 

현재 코드에서 네트워크 통신하는 APIManager의 return 값은

Observable<ValidationResponse>입니다.

 

 

이것을 flatMap으로 바꿔보면 이렇게 바꿀 수 있습니다.

/// 이메일 검증
        //  회원가입 할때 Email TextField에서 검증해야함
        duplicateBtn.rx.tap
            .flatMap { touchEvent -> Observable<ValidateEmailResponse> in
                return APIManager.shared.requestIsValidateEmail(api: Router.valid(email: "aas1234@sdsc1aa.com"))
                
            }
            .bind(with: self) { owner, response in
               print(response)
            }
            .disposed(by: disposeBag)

 

 

 

자 그럼 위의 코드의 타입이 어떤 타입인지 확인 해보겠습니다.

이제 왜 flatMap을 사용하는지 아시겠죠?

 

 

이걸 꼭 아셔야 됩니다.

flatMap은 사용자가 지정한 return 값으로 방출함!!

즉, 기존 스트림를 가공해서 -> 새로운 스트림을 만든다.

위의 플로우로 봤을때는 이렇게 진행됩니다.

버튼 탭 -> flatMap -> ControlEvent<Void> -> 사용자가 만든 Return

 

 

flatMap에 대해 뜯어보겠습니다.

 

 

return 값을 보면 where이 보입니다.

" ~ 이러한 조건일때 -> Observable<Source.Element>로 return 해줘" 

 

 

 

 

map과 비교해서 봐보겠습니다.

어떤점이 다른지 보이시나요??

map은 Return값을 Observable로 감싼다

flatMap은 조건을 만족하면 Observable<Source.Element>로 만들어준다.

왼 map 오 flatMap

 

 

그렇다면 코드에 적용해 보면서 map과 flatMap의 return 값을 비교해보겠습니다.

차이점이 보이시나요??

 

 

오늘 이렇게 map 과 flatMap에 대해 알아봤습니다.

이렇게 차이점을 확인해가니 왜 flatMap에 대해 쓰는지에 대해 바로 알 수 있었습니다.

소스를 던져주신 새싹 멘토님 ㅋㅋㅈ님께 이 영광을 돌리겠습니다 ^^