Skip to content

Conversation

@seokki2
Copy link
Contributor

@seokki2 seokki2 commented Dec 8, 2025

📟 연결된 이슈

closed #198

👷 작업한 내용

  • Apple 소셜 로그인 API 연동
  • 토큰 재발급 API 연동
  • AuthInterceptor 추가 및 토큰 재발급 로직 구현
  • KeychainManager 추가 및 토큰 저장

⌨️ 주요 코드 설명

  • 앱 최초 실행 시 Keychain에서 accessToken을 불러와 AuthManager에 로드하도록 구현했습니다.
  • Keychain의 I/O 작업이 백그라운드 스레드에서 실행되기에 불필요한 접근을 줄이기 위해 최초 1회만 읽어온 뒤 AuthManager에 저장하여 사용합니다.
  • 토큰이 만료된 경우 refreshToken을 사용해 자동으로 토큰을 재발급하도록 AuthInterceptor를 추가했습니다.

AuthManager

    /// 로그인 여부 체크
    func checkLogin() {
        do {
            self.accessToken = try KeychainManager.read(.accessToken)
            self.refreshToken = try KeychainManager.read(.refreshToken)
            authStatus = .loggedIn            
        } catch {
            authStatus = .loggedOut
            print(error.localizedDescription)
        }
    }
    
    /// AppleLogin API
    func loginWithApple(_ idToken: String, deviceId: String) async {
        do {
            let appleLoginReqDto = AppleLoginRequestDTO(idToken: idToken, deviceId: deviceId)
            let response: AppleLoginResponseDTO = try await provider.async.request(.appleLogin(appleLoginReqDto: appleLoginReqDto))
            try KeychainManager.create(.accessToken, response.accessToken)
            try KeychainManager.create(.refreshToken, response.refreshToken)
            self.accessToken = response.accessToken
            self.refreshToken = response.refreshToken
            
            authStatus = .loggedIn
        } catch {
            print(error.localizedDescription)
        }
    }
    /// 로그아웃 & 토큰 제거
    func logout() {
        do {
            try KeychainManager.delete(.accessToken)
            try KeychainManager.delete(.refreshToken)
            authStatus = .loggedOut
        } catch {
            print(error.localizedDescription)
        }
    }

    /// 토큰 재발급시 토큰 저장
    func reissueToken(accessToken: String, refreshToken: String) {
        do {
            try KeychainManager.create(.accessToken, accessToken)
            try KeychainManager.create(.refreshToken, refreshToken)
            self.accessToken = accessToken
            self.refreshToken = refreshToken
        } catch {
            print(error.localizedDescription)
        }
    }
  • statusCode가 401인 경우 refreshToken으로 토큰을 재발급받아 기존 토큰을 갱신하고 원래 요청을 재시도 합니다.
  • 토큰 재발급에 실패하거나 refreshToken이 존재하지 않는 경우 로그아웃 처리합니다.

AuthInterceptor

unc retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping (RetryResult) -> Void) {
        // 401인 경우가 아니라면 종료
        guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
            completion(.doNotRetryWithError(error))
            return
        }
        
        // refreshToken 가져오기 없다면 종료
        guard let refreshToken = AuthManager.shared.refreshToken?.replacingOccurrences(of: "\"", with: "") else {
            completion(.doNotRetry)
            AuthManager.shared.logout()
            return
        }
        
        // 토큰 재발급 API 호출 & 토큰 교체
        var refreshRequest = URLRequest(url: URL(string: Config.baseURL + "auth/refresh")!)
        refreshRequest.httpMethod = "POST"
        refreshRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
                
        let requestBody = try? JSONSerialization.data(withJSONObject: ["refreshToken": refreshToken, "deviceId": "doki-service"])
        refreshRequest.httpBody = requestBody
        let defaultSession = URLSession(configuration: .default)
        
        defaultSession.dataTask(with: refreshRequest) { (data: Data?, response: URLResponse?, error: Error?) in
            // 에러 발생시 재요청x
            guard error == nil else {
                completion(.doNotRetry)
                AuthManager.shared.logout()
                return
            }
            
            guard let data, let response = response as? HTTPURLResponse, (200..<300) ~= response.statusCode else {
                completion(.doNotRetry)
                AuthManager.shared.logout()
                return
            }
            
            // 토큰 재발급 요청 성공
            do {
                let response = try JSONDecoder().decode(AppleLoginResponseDTO.self, from: data)
                // 토큰 재발급
                AuthManager.shared.reissueToken(
                    accessToken: response.accessToken,
                    refreshToken: response.refreshToken
                )
                // 재요청
                print("토큰 재발급 성공 - 재요청")
                completion(.retry)
            } catch {
                print("토큰 재발급 실패 - 로그아웃")
                completion(.doNotRetryWithError(error))
                AuthManager.shared.logout()
            }
        }.resume()
    }

사용 방법

Provider 설정에 AuthInterceptor.shared 추가

MoyaProvider<RegionAPI>(session: .init(interceptor: AuthInterceptor.shared), plugins: [NetworkLoggerPlugin()])

⚠️ 참고 사항

  • 참고 사항

📸 스크린샷

구현 내용 SE 13 mini 15 pro
GIF

@seokki2 seokki2 requested review from sem-git and w0o0kgit December 8, 2025 19:11
@seokki2 seokki2 self-assigned this Dec 8, 2025
@seokki2 seokki2 added the Feat UI 및 기능 구현 label Dec 8, 2025
Copy link
Contributor

Choose a reason for hiding this comment

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

userId 안 쓰는 거 마음이 편안해지네요

@seokki2 seokki2 merged commit 551d005 into develop Jan 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feat UI 및 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Apple 로그인 연동

4 participants