API Response with RxSwift

임승섭·2024년 9월 21일
0

API 통신

  • RxSwift, CleanArchitecture, Reactor, Moya

궁금증을 가진 코드

  • 기능 : 닉네임 입력 후 확인 버튼 클릭 시, 중복된 닉네임인지 확인하는 API 통신

  • SetNicknameReactor

    // SetNicknameReactor.swift
    
    enum Action {
        /* ... */
        case confirmButtonTap(text: String)
    }
    
    enum Mutation {
        /* ... */
        case isValid(status: SetNicknameUseCase.TextFieldStatus)
    }
    
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
            /* ... */
            case .confirmButtonTap(let text):
                let dto = ...
                return setNickNameUseCase.isValidText(dto: dto)
                    .map { Mutation.isValid(status: $0) }
    }

  • SetNickNameUseCase

    // SetNickNameUseCase.swift
    
    enum TextFieldStatus {
        case duplicationNickname // 중복된 닉네임
        case unknownError // 알 수 없는 에러
        case validNickname // 사용 가능한 닉네임
        case readyToRequest // API 요청 전
    }
    
    func isValidText(dto: PostIsValidNickNameRequestDTO) -> Observable<TextFieldStatus> {
        let request = setNickNameRepository
                        .postIsValidNickName(dto: dto)
                        .share()
    
        let success = request
                        .compactMap { $0.element }
                        .map { $0 ? TextFieldStatus.validNickname : TextFieldStatus.duplicationNickname } 
    
        let fail = request
                    .compactMap { $0.error }
                    .map { _ in TextFieldStatus.unknownError }
    
        return Observable.merge(success, fail)
    }

  • SetNickNameRepository
    // SetNickNameRepository.swift
    func postIsValidNickName(dto: PostIsValidNickNameRequestDTO) -> Observable<Event<Bool>> {
        return provider.log.rx.request(.postIsValidNickName(dto: dto))
            .map(ResponseDTO<PostRefreshTokenResponseDTO>.self)
            .map{ $0.isSuccess }
            .asObservable()
            .materialize()
    }

  • 위 코드에서, 응답값이 어떤 방식으로 저장되는지 궁금해서 질문을 드렸다.
    • 응답값의 종류는 다음과 같다.
      • 성공
      • 실패 (닉네임 중복 or 기타 에러)
  • 놓치고 있었던 부분은, Observable은 값을 저장하는 역할이 아닌, 값이 흐를 수 있는 파이프라인, 즉 stream을 의미한다.

코드 해석

  • SetNickNameRepository

    // SetNickNameRepository.swift
    func postIsValidNickName(dto: PostIsValidNickNameRequestDTO) -> Observable<Event<Bool>> {
        return provider.log.rx.request(.postIsValidNickName(dto: dto))
            .map(ResponseDTO<PostRefreshTokenResponseDTO>.self)	// 응답 DTO로 parsing
            .map{ $0.isSuccess }	// 파싱한 데이터의 isSuccess 필드 확인
            .asObservable()		// Single을 Observable<Bool>로 변환
            .materialize()		// Observable<Event<Bool>>로 변환
    }
    • 서버 응답값은 다음과 같고, statusCode 200으로 통신이 성공하게 되면 isSuccess 값에 true을 보내준다.

      struct ResponseDTO<T: Codable>: Codable {
          let isSuccess: Bool
          let code: String
          let message: String
          let data: T?
      }
    • 닉네임 중복 API의 경우, 응답값이 따로 없기 때문에 Observable<Bool> 타입으로 받게 된다. 성공/실패에 따라 Bool에 흘러가는 값이 달라지게 된다.

    • materialize : Event 형태로 바꿔준다.

      • 나는 개인적으로 Single을 이용해서 응답값을 주로 래핑했는데, 팀원분은 materalize를 사용했다.
      • error, completed를 사용하면 스트림이 끊기는 이슈가 발생할 수 있기 때문에 래핑은 필요하다.
      • Single을 활용하는 것과 materialize를 활용하는 것의 장단점은 따로 공부할 예정이다.

  • SetNickNameUseCase

    func isValidText(dto: PostIsValidNickNameRequestDTO) -> Observable<TextFieldStatus> {
      let request = setNickNameRepository
                      .postIsValidNickName(dto: dto)
                      .share()	// 응답 결과는 아직 모른다. Observable<Event<Bool>> 타입
    
      let success = request
                      .compactMap { $0.element }
                      .map { $0 ? TextFieldStatus.validNickname : TextFieldStatus.duplicationNickname }
    
      let fail = request
                  .compactMap { $0.error }
                  .map { _ in TextFieldStatus.unknownError }
    
      return Observable.merge(success, fail)
    }

    • share() 사용
      • Observable은 subscribe할 때마다 create을 통해 새로운 Observable을 생성한다.
        즉, subscribe를 하는 횟수마다 새로운 Observable이 생성된다.
      • 이를 방지하고자 share를 사용하면, subscribe할 때마다 새로운 Observable이 생성되지 않고, 하나의 Observable 시퀀스에서 방출된 아이템을 공유해서 사용할 수 있다.
      • 여기서는 success와 fail이 하나의 request에 대한 값만 받을 수 있도록 share()를 활용한다.

    • compactMap { } 사용
      • compactMap은 해당 값이 존재하면 해당 값을 방출하고, 값이 nil이면 해당 이벤트를 무시한다.

      • 즉, compactMap { $0.element } 는 element가 있는 이벤트만을 방출하고, compactMap { $0.error }는 오류가 발생했을 때만 그 오류를 스트림으로 방출하게 된다.

      • compactMap 사용 예시

        let observable: Observable<Int?> = Observable.of(1, nil, 3, nil, 5)
        observable
        	.compactMap { $0 }
          .subscribe(onNext: { print($0) })
        
        // 출력 : 1, 3, 5

    • enum Event

        // Event.swift (RxSwift)
        @frozen public enum Event<Element> {
            case next(Element)
            case error(Swift.Error)
            case completed
        }
      
        extension Event {
            // If 'next' event, returns element value
            public var element: Element? {
                if case .next(let value) = self {
                    return value
                }
                return nil
            }
      
            // If `error` event, returns error.
            public var error: Swift.Error? {
                if case .error(let error) = self {
                    return error
                }
                return nil
            }
        }

    • merge() : success 또는 fail 둘 중 하나만 방출될 수 있따.

  • 데이터의 흐름

    1. Repository -> 네트워크 통신 -> 응답
      • 네트워크 통신 성공 (isSuccess == true) -> Observable<true> : 사용 가능한 닉네임
      • 네트워크 통신 성공 (isSuccess == false) ->Observable<false> : 중복된 닉네임 (사용 불가)
      • 네트워크 통신 실패Observable<Error> : 알 수 없는 에러

    1. UseCase -> Repo 메서드 실행 -> 응답
      • Observable<true> => .validNickname
      • Observable<false> => .duplicationNickname
      • Observable<Error> => .unknownError

0개의 댓글