Skip to content

Conversation

@youz2me
Copy link
Member

@youz2me youz2me commented Mar 5, 2025

👻 PULL REQUEST

📄 작업 내용

  • 로그인, 온보딩, 커뮤니티 화면에 해당하는 UseCase를 정의하고 구현했어요.
  • 소셜 로그인(애플, 카카오) 로직을 구현하고 AuthProvider를 통해 추상화를 진행했어요.
  • 앱 진입 시 자동 로그인 처리 로직을 구현하고 토큰 유무에 따른 분기 처리를 진행했어요.
  • 이후 작업 시 충돌 방지를 위해 UseCase 폴더링을 진행했어요.
  • CombineCocoa 라이브러리를 추가했어요.
  • Bundle 익스텐션에 kakaoAppKey, amplitudeAppKey가 추가 선언됨에 따라 Bundle 폴더를 Core 레이어로 이동했어요.
    • kakaoAppKeyAppDelegate에서 SDK 초기화 시 사용되기 때문에 익스텐션이 Core로 이동되어야 한다고 판단했어요.
    • 레거시 버전에서 kakaoAppKey를 옮겨오는 과정에서 amplitudeAppKey도 함께 선언했어요.

💻 주요 코드 설명

애플 로그인, 카카오 로그인 구현 (FetchUserAuthUseCase 구현)

  • 초기 구현 시 애플 로그인과 카카오 로그인에 대한 UseCase를 분리해 구현했으나 아래와 같은 이유로 UseCase를 합치고 해당 로직들을 각각 Infra 레이어의 KakaoAuthProviderAppleAuthProvider로 숨겨 구현했습니다.
    1. 로그인 시 동일한 서버 API를 사용하기 때문에 레포지토리 단에서 해당 로직을 알 필요가 없습니다.
    2. 로그인 로직 구현 시 KakaoSDKAuth, KakaoSDKUser, AuthenticationServices를 import해야 하기 때문에 Domain 레이어에서 추가적인 의존성을 가지는 것이 클린 아키텍처에 맞지 않다고 생각했습니다.
  • 두 로그인 모두 인증 -> 결과 반환이라는 같은 로직을 가지고 있기 때문에 AuthProvider라는 공통 프로토콜을 선언해 Data 레이어에 추상화한 후 Infra 레이어에서 각각의 ProviderAuthProvider 프로토콜을 채택하도록 구현했습니다.
import Combine
import Foundation

protocol AuthProvider {
    func authenticate() -> AnyPublisher<String?, WableError>
}
  • 카카오 로그인은 디바이스에 카카오톡이 설치되어 있을 경우 카카오톡 애플리케이션으로 로그인을 시도하고, 그렇지 않을 경우에는 계정 로그인을 통해 로그인을 시도합니다. (KakaoAuthProvider 파일의 authenticate() 메서드)
func authenticate() -> AnyPublisher<String?, WableError> {
        let UserAPI = UserApi.self
        
        return Future<String?, WableError> { promise in
            if UserAPI.isKakaoTalkLoginAvailable() {
                UserAPI.shared.loginWithKakaoTalk { [weak self] (oauthToken, error) in
                    self?.handleKakaoAuthResult(oauthToken: oauthToken, error: error, promise: promise)
                }
            } else {
                UserAPI.shared.loginWithKakaoAccount { [weak self] (oauthToken, error) in
                    self?.handleKakaoAuthResult(oauthToken: oauthToken, error: error, promise: promise)
                }
            }
        }
        .eraseToAnyPublisher()
    }
  • 이후 받아온 결과(카카오 엑세스 토큰)를 handleKakaoAuthResult() 메서드 내에서 KeychainStorage에 저장합니다.
    • 카카오 엑세스 토큰과 자체 엑세스 토큰은 서버 통신 시 구별되어야 하므로 TokenType.kakaoAccessToken이라는 enum 케이스를 추가해 저장했습니다.
  • 애플 로그인은 로그인 시 서버에 userName을 넘겨야 하기 때문에 유저의 이름을 받아오도록 구현했습니다.
  • 로그인 성공/실패 시 결과 처리와 로그인 화면 표시를 위해 각각 ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding 프로토콜을 채택해 필수적으로 요구되는 메서드들을 구현했습니다.
func authenticate() -> AnyPublisher<String?, WableError> {
        return Future<String?, WableError> { promise in
            self.promise = promise
            
            let request = ASAuthorizationAppleIDProvider().createRequest().then {
                $0.requestedScopes = [.fullName]
            }
            
            let authorizationController = ASAuthorizationController(authorizationRequests: [request]).then {
                $0.delegate = self
                $0.presentationContextProvider = self
            }
            
            authorizationController.performRequests()
        }
        .eraseToAnyPublisher()
    }
  • 이후 LoginRepository의 실구현체인 LoginRepositoryImpl에서 해당 provider를 SocialPlatform: AuthProvider의 배열 형식으로 초기화하도록 했습니다.
    • fetchUserAuth에서는 provider를 통해 인증 로직을 수행한 후 받아온 결과값을 이용해 서버 통신을 수행합니다.

자동 로그인 로직 구현 (미완성)

  • UserSessionRepository에서 자동 로그인 여부를 체크할 수 있는 checkAutoLogin() 메서드를 추가했습니다.
    • 해당 메서드는 토큰 존재 여부를 판단해 SceneDelegate의 scene(_ scene: UIScene, willConnectTo... 메서드에서 호출되어 토큰 존재 확인 시 메인 화면으로 전환되도록 구현되어 있습니다.
    • 기타 더 이야기해볼 점에 작성해놓았듯 미완성인 부분이 존재합니다. 현재는 조건을 체크해 화면이 전환되도록 한다 정도로만 구현되어 있고, 추후 필요한 조건이나 로직을 재검토해 기능을 마저 완성할 계획입니다.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        self.window = UIWindow(windowScene: windowScene)
        
        userSessionRepository.checkAutoLogin()
            .sink { result in
                switch result {
                case .finished:
                    break
                case .failure(_):
                    self.configureLoginScreen()
                }
            } receiveValue: { isAutoLoginEnabled in
                if isAutoLoginEnabled {
                    self.configureMainScreen()
                } else {
                    self.configureLoginScreen()
                }
            }
            .store(in: cancelBag)
    }

📚 참고자료

버터플라이 아키텍처를 소개합니다

👀 기타 더 이야기해볼 점

  • 현재 자동 로그인 로직 중 리프레시 토큰 만료 시 로그인 화면으로 이동하는 로직이 구현되어 있지 않은 상태입니다.
    • 해당 로직을 구현하려면 OAuth 폴더가 같이 수정되어야 해서 PR 단위가 너무 커질 것을 우려해 일단 TODO로 남겨두었습니다. 최대한 빠르게 이슈 생성해 반영하겠습니다!

🔗 연결된 이슈

@youz2me youz2me added ✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌 labels Mar 5, 2025
@youz2me youz2me requested a review from JinUng41 March 5, 2025 21:00
@youz2me youz2me self-assigned this Mar 5, 2025
@youz2me youz2me changed the title [Feat] 로그인, 온보딩, 커뮤니티 UseCase 구현 및 소셜 로그인, 자동 로그인 로직 구현 [Feat] 로그인, 온보딩, 커뮤니티 UseCase 및 소셜 로그인, 자동 로그인 로직 구현 Mar 5, 2025
Copy link
Collaborator

@JinUng41 JinUng41 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다.

나날이 성장해 가시는 유진님의 모습이 이젠 무섭네요..!
멋있으세요. 저를 뛰어넘으시고, 리드까지 하시면 좋을 것 같아요! 👍

Comment on lines 27 to 33
.sink { result in
switch result {
case .finished:
break
case .failure(_):
self.configureLoginScreen()
}
Copy link
Collaborator

@JinUng41 JinUng41 Mar 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 알기로 receiveCompletion으로 알고 있는데, 변수명을 completion으로 하는 건 어떻게 생각하세요?
물론 result도 비슷한 느낌이긴 하나, 애초 메서드명을 따라가는 것이 더 낫다고 판단되어서 말씀드려 봅니다.


if case let을 사용하면 아래와 같이 표현할 수도 있어요.

.sink { [weak self] result in
    if case let .failure(error) = result {
        self?.configureLoginScreen()
    }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로직이 추가로 구현될거라 일단 임시로 저렇게 처리해두었습니다 !! 다른 곳에서 한번 활용해보도록 하겠습니닷 ... 감사합니다!!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Core로 옮긴 것이 맞는 판단 같습니다. 굿잡 👍

Comment on lines +12 to +14
protocol AuthProvider {
func authenticate() -> AnyPublisher<String?, WableError>
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authenticate 메서드명 좋은 것 같습니다.

Comment on lines 97 to 101
func updateActiveUserID(forUserID userID: Int?) {
if let userID = userID {
try? userDefaults.setValue(userID, for: Keys.activeUserID)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateActiveUserID(_:)으로 메서드 명을 바꾸는 것은 어떤가요?
메서드 명에서 이미 UserID가 있기 때문에 파라미터 레이블로 forUserID는 중복되는 느낌이 들었습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다!

Comment on lines 11 to 23
final class UpdateUserProfileUseCase {
let repository: ProfileRepository

init(repository: ProfileRepository) {
self.repository = repository
}
}

extension UpdateUserProfileUseCase {
func execute(profile: UserProfile, isPushAlarmAllowed: Bool) -> AnyPublisher<Void, WableError> {
return repository.updateUserProfile(profile: profile, isPushAlarmAllowed: isPushAlarmAllowed)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프로필을 수정하는 화면은 사용자 프로필을 보여주는 탭에서 사용되는게 맞는 부분인 것 같아요.
온보딩에서 사용자의 프로필을 구성하도록 사용되는 행위니까 CreateUserProfile은 어떨까 싶어요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다! 감사합니닷

Comment on lines 17 to 18
func authenticate() -> AnyPublisher<String?, WableError> {
let UserAPI = UserApi.self
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typealias를 이용하여 UserApi를 KakaoUserAPI로 선언하는 것은 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문과세요? 캬...

Comment on lines 20 to 31
return Future<String?, WableError> { promise in
if UserAPI.isKakaoTalkLoginAvailable() {
UserAPI.shared.loginWithKakaoTalk { [weak self] (oauthToken, error) in
self?.handleKakaoAuthResult(oauthToken: oauthToken, error: error, promise: promise)
}
} else {
UserAPI.shared.loginWithKakaoAccount { [weak self] (oauthToken, error) in
self?.handleKakaoAuthResult(oauthToken: oauthToken, error: error, promise: promise)
}
}
}
.eraseToAnyPublisher()
Copy link
Collaborator

@JinUng41 JinUng41 Mar 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래와 같이 삼항 연산자를 이용해 보는 것은 어떠세요?

return Future<String?, WableError> { promise in
    let loginMethod = UserApi.isKakaoTalkLoginAvailable()
        ? UserApi.shared.loginWithKakaoTalk
        : UserApi.shared.loginWithKakaoAccount
    
    loginMethod { [weak self] oauthToken, error in
        self?.handleKakaoAuthresult(oauthToken: oauthToken, error: error, promise: promise)
    }
}
.eraseToAnyPulisher()

Comment on lines 48 to 50
if let token = oauthToken?.accessToken {
try? tokenStorage.save(token, for: .kakaoAccessToken)
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try에 대한 에러처리를 하지 않아도 괜찮은지 궁금합니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다!

@youz2me youz2me merged commit e36b856 into develop Mar 6, 2025
@youz2me youz2me deleted the feat/#113-usecase branch March 6, 2025 15:59
@youz2me youz2me added this to Wable-iOS Mar 6, 2025
@youz2me youz2me moved this to Done in Wable-iOS Mar 6, 2025
@youz2me youz2me removed this from Wable-iOS Mar 6, 2025
youz2me added a commit that referenced this pull request Oct 26, 2025
[Feat] 로그인, 온보딩, 커뮤니티 UseCase 및 소셜 로그인, 자동 로그인 로직 구현
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 로그인, 온보딩, 커뮤니티 UseCase 구현하기

3 participants