ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Result (With. Composable Architecture)
    Swift 2022. 4. 25. 10:03

    안녕하세요. 그린입니다🟢

    이번 포스팅에서는 Result에 대해 학습해보겠습니다🙋🏻

    더불어 컴포저블 아키텍쳐에서 이런 Result 타입을 어떻게 활용하는지 보시죠!

     

    Result?

    우선 선언부는 이렇습니다.

    @frozen enum Result<Success, Failure> where Failure : Error

    일반적인 열거형이며 각 경우에 연결된 값을 포함해 성공 혹은 실패를 나타내는 열거형 타입입니다!

     

    Result의 필요성?

    우리는 이미 do, catch, try, throws와 같은 에러 처리에 대한 문법을 알고 있다고 가정합시다!

    해당 문법들로 에러를 처리할 수 있지만 모든 경우에서 다 처리하기에 어렵고 복잡합니다.

    이러한 친구들은 에러의 동기적인 처리를 하는데 도움이 됩니다.

    다만 우리는 통신과 같은 상황에서 에러를 비동기 처리해야할 때도 많습니다.

    요럴때 Result를 활용하면 깔끔하고 편하게 에러를 처리할 수 있습니다👍

     

    감이 잘 안오실 수 있으니 이제 Result의 사용을 보시죠!

     

    Result 사용

    예를 들기 위해 카카오SDK를 활용해 카카오톡 로그인을 한다는 가정하에 코드로 예시를 들겠습니다!

    func loginWithKakaoTalk(completion: @escaping (Result<[String: String], Error>) -> Void) {
      UserApi.shared.loginWithKakaoTalk { token, error in
        if let error = error {
          completion(.failure(error))
        }
        UserApi.shared.me { user, error in
          if let error = error {
            completion(.failure(error))
          }
          if let user = user {
            let nickName = user.kakaoAccount?.profile?.nickname ?? ""
            let email = user.kakaoAccount?.email ?? ""
            completion(.success(["nickName": nickName, "email": email]))
          }
        }
      }
    }

    자 해당 코드를 보시죠!

    우선 간단히 설명드리면 카카오톡 로그인을 하려고하는 메서드입니다.

    해당 메서드의 탈출 클로저로 completion이라고 지칭했으며 Result 타입을 받는걸 볼 수 있습니다!

    그럼 2번째 라인을 보시죠!

    카카오톡이 설치되어 있을경우 해당 라인을 타서 로그인을 시도합니다.

    token과 error를 인자로 받아 에러이면 컴플리션 핸들러에 Result 타입중 하나의 케이스인 failure를 error를 담아 실행해줍니다.

    만약 에러가 아니라면 쭉 넘어갑니다.

    User의 정보를 받아와 클로저로 user와 error를 가지고 놉니다.

    마찬가지로 여기서도 error가 난다면 위와 동일하게 failure를 뱉어줍니다.

    그리고 아니라면 원하는 데이터를 뽑아온뒤, 최종적으로 Result의 타입 중 성공인 success에다 정의된 딕셔너리 값을 넣어줍니다.

    잘 보시면 UserApi.xxx 되는 부분들도 모두 탈출 클로저로 구현된 부분에서 놀아준겁니다.

    즉 비동기적 처리를 해야하는 부분에서 do catch같은 동기적 에러 처리를 사용한다면 정확한 의도대로 되지 않을겁니다.

    이 경우 Result를 활용할 수 있겠습니다!

     

    Result를 Composable Architecture와 같이 사용해보기

    우선 카카오 로그인을 하는 서비스 자체를 만들어보겠습니다.

    해당 카카오 로그인을 성공하게 되면 요 서비스를 부른 액션에다 값을 전달해 다음 어떤 액션을 호출할지 그런 흐름을 구현해볼께요!

     

    <KakaoLoginService>

    import Combine
    import KakaoSDKUser
    
    public class KakaoLoginService {
      public init() {}
    
      public func getKakaoUserInfo() -> AnyPublisher<[String: String], Error> {
        return Publishers.Create<[String: String], Error>(factory: { [unowned self] subscribers -> Cancellable in
          if UserApi.isKakaoTalkLoginAvailable() {
            self.loginWithKakaoTalk { result in
              switch result {
              case let .success(info):
                subscribers.send(info)
              case let .failure(error):
                subscribers.send(completion: .failure(error))
              }
              subscribers.send(completion: .finished)
            }
          } else {
            self.loginWithKakaoAccont { result in
              switch result {
              case let .success(info):
                subscribers.send(info)
              case let .failure(error):
                subscribers.send(completion: .failure(error))
              }
              subscribers.send(completion: .finished)
            }
          }
          return AnyCancellable({})
        })
        .eraseToAnyPublisher()
      }
    
      private func loginWithKakaoTalk(completion: @escaping (Result<[String: String], Error>) -> Void) {
        UserApi.shared.loginWithKakaoTalk { _, error in
          if let error = error {
            completion(.failure(error))
          }
          UserApi.shared.me { user, error in
            if let error = error {
              completion(.failure(error))
            }
            if let user = user {
              let nickName = user.kakaoAccount?.profile?.nickname ?? ""
              let email = user.kakaoAccount?.email ?? ""
              completion(.success(["nickName": nickName, "email": email]))
            }
          }
        }
      }
    
      private func loginWithKakaoAccont(completion: @escaping (Result<[String: String], Error>) -> Void) {
        UserApi.shared.loginWithKakaoAccount { _, error in
          if let error = error {
            completion(.failure(error))
          }
          UserApi.shared.me { user, error in
            if let error = error {
              completion(.failure(error))
            }
            if let user = user {
              let nickName = user.kakaoAccount?.profile?.nickname ?? ""
              let email = user.kakaoAccount?.email ?? ""
              completion(.success(["nickName": nickName, "email": email]))
            }
          }
        }
      }
    }

    우선 카카오 로그인 서비스라는 카카오 로그인을 해줄 수 있도록 도와주는 Environment의 객체입니다.

    위에서 loginWithKakaoTalk 메서드를 결국 이 서비스를 불러 사용될 getKakaoUserInfo라는 메서드에서 호출해줍니다.

    잘 보면 loginWithKakaoTalk의 클로저에 switch 분기를 태워 성공일때와 실패일때로 나눠 subscribers에 각기 다른 흐름을 전달해줍니다.

    (combine의 코드들은 별도로 설명하지 않겠습니다!)

    정리하자면 getKakaoUserInfo를 호출하면 내부에서 카카오톡 설치/미설치에 따라 loginWithKakaoTalk/loginWithKakaoAccount 메서드를 호출합니다.

    해당 메서드에서는 탈출 클로저로 Result타입을 사용하고 에러를 잡아줍니다.

    그리고 getKakaoUserInfo에서 해당 클로저를 받아와 적절하게 분기를 태워 subscribers에게 결과를 전달하고 종료시킵니다.

     

    자 그럼 이 메서드를 호출한 코어 액션으로 가보시죠!

     

    <LoginCore>

    let loginReducer = Reducer<WithSharedState<LoginState>, LoginAction, LoginEnvironment> {
      state, action, env in
      switch action {
      case .kakaoLoginButtonTapped:
        return env.kakaoLoginService.getKakaoUserInfo()
          .catchToEffect()
          .flatMapLatest({ result -> Effect<LoginAction, Never> in
            switch result {
            case .failure:
              return .none
            case let .success(userInfo):
              state.local.nickName =  userInfo["nickName", default: ""]
              state.local.email =  userInfo["email", default: ""]
              return .none
            }
          })
          .eraseToEffect()
      }
    }

    Core에서 Reducer 부분만 가져왔습니다!

    보시면 kakaoLoginButton이 탭되었을때 카카오로그인서비스에 정의된 getKakaoUserInfo 메서드를 호출합니다.

    catchToEffect 오퍼레이터를 이용해 받아온 리턴값을 Effect Result타입으로 만들어줍니다.

    그러면 우리는 해당 리턴값을 Result처럼 에러와 데이터로 처리할 수 있습니다.

    그리고 flatMapLatest 오퍼레이터를 이용합니다.

    (해당 오퍼레이터는 CombineExt 라이브러리에 있는것입니다!)

    결국 위에서 받아온 result 즉 Result<[String : String], Error> 값을 이용해 해당 위치한 Effect<LoginAction, Never> 이펙트 타입으로 반환 시켜주려는 겁니다.

    해당 result값에는 성공과 실패가 들어있겠죠?

    실패 일때 원하는 액션 혹은 다른 로직들을 넣어주면 됩니다.

    성공일때 저는 우선 state에 값을 저장할 수 있도록만 해뒀는데 필요하다면 다음 액션을 리턴시켜줘도 됩니다!

     

    자 간단하죠..?
    근데 여기서 catchToEffect라는 오퍼레이터가 나왔어요.
    아주아주 잠깐 살펴보고 가시죠!

     

    catchToEffect?

    Composable Architecture 소스 코드에 정의는 이렇습니다.

    public func catchToEffect() -> Effect<Result<Output, Failure>, Never> {
      self.map(Result.success)
        .catch { Just(.failure($0)) }
        .eraseToEffect()
    }

    map을 해서 Result의 성공 타입으로 거르되 failure는 캐치해서 이펙트로 감싼다.

    뭐 이정도로 간단히 해석할 수 있을것 같아요.

    해당 들어온 친구를 Result의 Effect 타입으로 만들어 반환하는것 같습니다.

     

    catchToEffect 예시

    case .buttonTapped:
      return environment.fetchUser(id: 1)
        .catchToEffect()
        .map(ProfileAction.userResponse)

    공식 소스코드에는 이렇게 나와있어요.

    해석해보죠!

    우선 우리는 위에서 서비스라 칭한것이 여기선 environment의 fetchUser 메서드가 되겠네요!

    해당 메서드에 id 1의 값을 주고 돌아온 Effect 타입의 무언가를 Result로 바꾸기위해 catchToEffect로 사용합니다.

    그리고 결과값을 가지고 다른 액션으로 매핑해줬네요!

    이렇듯 정리하자면 들어온 값의 결과값 Result로 만들어 성공과 실패로 구분지어 사용하고 싶을때 catchToEffect 오퍼레이터로 변환해 사용하게 됩니다!

     

    마무리

    자.. 어쩌다보니 Result에 대한 학습보다 약간 Composable Architecture에 대한 고찰이 된것 같긴합니다..?

    그래도 Result를 딥하게는 아니지만 알고 지나갑니다~

     

    [참고자료]

    https://developer.apple.com/documentation/swift/result

     

    Apple Developer Documentation

     

    developer.apple.com

     

    'Swift' 카테고리의 다른 글

    WWDC 2022 - Swift 5.7  (0) 2022.06.13
    Swift5.6 - existential any  (0) 2022.05.02
    Property Wrapper  (0) 2022.04.21
    Method Swizzling  (0) 2022.01.14
    Swift - Markdown  (2) 2021.10.20
Designed by Tistory.