Skip to content

Conversation

@seokki2
Copy link

@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 안 쓰는 거 마음이 편안해지네요

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