SwiftUI 팀 프로젝트 혼자 리팩토링하기 #3 : Apple Login 구현

·2024년 7월 8일
0

리팩토링 일지

목록 보기
4/13

GitHub - minnnidev/Wote at refactoring
refactor/#5 브랜치에서 삽질을 확인 가능!


<오늘의 목차>
1. Apple Login → 서버에 authoriazation code 포함하여 request → 토큰 받기까지는 완료 (아키텍처 구조 설계는 너무 어렵다!)
2. Clean Architecture에서 Error Handling은 어떻게 해야 할까?


로그인 구현: Apple Login

  1. swinject로 필요한 객체를 register하기

    final class AuthAssembly: Assembly {
    
        func assemble(container: Container) {
            container.register(LoginViewModel.self) { res in
                LoginViewModel(authUseCase: res.resolve(AuthUseCaseType.self)!)
            }
    
            container.register(AuthUseCaseType.self) { res in
                AuthUseCase(authRepository: res.resolve(AuthRepositoryType.self)!)
            }
    
            container.register(AuthRepositoryType.self) { _ in AuthRepository() }
        }
    }
  1. ViewModel: apple login 성공 시 authUseCase 접근

case let .appleLoginHandler(result):
    switch result {
    case let .success(authorization):
        authUseCase.loginWithApple(authorization)
            .sink { completion in
                print(completion)
            } receiveValue: { _ in
                print("헿")
            }
            .store(in: &cancellables)

    case let .failure(error):
        print(error.localizedDescription)
    }
  1. AuthUseCase: credential에서 authorizationCode 추출하여 repository 호출
func loginWithApple(_ authorization: ASAuthorization) -> AnyPublisher<Void, CustomError> {
    let code = getCredential(authorization)

    return authRepository.loginWithApple(code)
        .map { _ in }
        .eraseToAnyPublisher()
}

extension AuthUseCase {

    func getCredential(_ authorization: ASAuthorization) -> String {
        guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { return "" }
        guard let authorizationCode = credential.authorizationCode else { return "" }
        guard let authoriazationCodeString = String(data: authorizationCode, encoding: .utf8) else { return "" }

        return authoriazationCodeString
    }
}

// SwiftUI에서 Preview와 테스트를 위해 StubUseCase도 만들어 줬고, 내용은 추가 필요....
final class StubAuthUseCase: AuthUseCaseType {

    func loginWithApple(_ authorization: ASAuthorization) -> AnyPublisher<Void, CustomError> {
        Empty()
            .eraseToAnyPublisher()
    }
}
  1. AuthRepository: MoyaProvider 객체로 HTTP 요청, 결과 처리
final class AuthRepository: AuthRepositoryType {

    private let provider = MoyaProvider<AuthAPI>()

    func loginWithApple(_ authorizationCode: String) -> AnyPublisher<Tokens, CustomError> {
        let object: AppleUserRequestObject = .init(code: authorizationCode, state: "APPLE")

        return provider.requestPublisher(.loginWithApple(object))
            .tryMap { try JSONDecoder().decode(GeneralResponse<AppleUserResponseObject>.self, from: $0.data) }
            .compactMap { $0.data }
            .map { $0.jwtToken }
            .map { $0.toToken() }
            .mapError { CustomError.error($0) }
            .eraseToAnyPublisher()
    }
}

구현해 보면서 좋았던 점

  • 흐름을 알고 있으니, 차례차례 코드 작업을 진행할 수 있었음

아쉬웠던 점 / 고민한 점

  • provider 의존성 주입
    • DIP를 위해 이리저리 오래 고민해 봤는데 답을 내진 못했음.
  • 각 레이어의 역할을 정의해 놨음에도 불구하고, 어디에서 뭘 해야 할지 분리가 상당히 너무 완전히 겁나게 어려웠음
    • 이전 포스팅에서 정리해둔 걸 기반으로 하긴 했는데 애매함…
    • credential에서 authorization code를 추출하는 과정을 viewModel에서 할까 UseCase는 할까 고민했는데, viewModel은 뷰에 보여줄 데이터만을 처리하는 곳으로 정의했으니, UseCase에 작성했다.
    • Repository는 이전에 data source에서 받아온 데이터를 model로 mapping하는 곳으로 정의하였는데, 한번 더 분리해야 할까?
      • 자문자답하자면 분리해야 할 것 같아서 글쓰고 추가하려고 한다….

아니 아쉬운 점만 길고 좋았던 점이 없는데요…


Clean Architecture에서 Error Handling은 어떻게 해야 할까?

처음 든 생각은 당연히 Repository에서는 NetworkError를 처리해 준다.

Domain 계층에서는 따로 DomainError(가제?)을 정의해 준다. (유저가 마주하는 가장 직접적인 에러. 개발적으로는 표현하면 안 되지 않을까?)

이 과정을 계속해서 고민하고, 많은 포스팅을 읽어보고 있는데 아직 해결 방법을 생각해 보는 중이다.


자동로그인, 토큰 갱신 등등 할일이 산더미지만...
클린 아키텍처... 무습고도 재밋는 넘이다
고민하다가 머리 빠질 거 같다.

0개의 댓글

관련 채용 정보