Skip to content

wontaeyoung/KeyCat

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

ํ”„๋กœ์ ํŠธ

์Šคํฌ๋ฆฐ์ƒท

Keycat_แ„…แ…ตแ„ƒแ…ณแ„†แ…ต แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ1

Keycat_แ„…แ…ตแ„ƒแ…ณแ„†แ…ต แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ2

ํ•œ ์ค„ ์†Œ๊ฐœ

ํ‚ค๋ณด๋“œ ์ƒํ’ˆ ํŠนํ™” ์ปค๋จธ์Šค ํ”Œ๋žซํผ ์•ฑ


์„œ๋น„์Šค ๊ธฐ๋Šฅ

  • ๋กœ๊ทธ์ธ / ํšŒ์›๊ฐ€์ž… / ํŒ๋งค์ž ์‚ฌ์—… ๊ถŒํ•œ ์ธ์ฆ
  • ์ƒํ’ˆ ํŒ๋งค๊ธ€ ์ž‘์„ฑ / ์กฐํšŒ / ์‚ญ์ œ ๊ธฐ๋Šฅ
  • ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ
  • PG์‚ฌ ์—ฐ๋™ ์ƒํ’ˆ ๊ฒฐ์ œ ๊ธฐ๋Šฅ
  • ์ƒํ’ˆ ๋ฆฌ๋ทฐ ์ž‘์„ฑ / ์กฐํšŒ / ์‚ญ์ œ ๊ธฐ๋Šฅ
  • ๋ถ๋งˆํฌ / ์žฅ๋ฐ”๊ตฌ๋‹ˆ
  • ํ”„๋กœํ•„ ์ˆ˜์ • ๊ธฐ๋Šฅ
  • ํŒ”๋กœ์šฐ ๊ธฐ๋Šฅ

ํ”„๋กœ์ ํŠธ ํ™˜๊ฒฝ

๊ฐœ๋ฐœ ์ธ์›
iOS/๊ธฐํš/๋””์ž์ธ 1์ธ(๋ณธ์ธ)
Backend 1์ธ

๊ฐœ๋ฐœ ๊ธฐ๊ฐ„
2024.04.10 ~ 2024.05.06 (3.5์ฃผ)


๊ฐœ๋ฐœ ํ™˜๊ฒฝ

iOS ์ตœ์†Œ ๋ฒ„์ „
16.0+

Xcode
15.3


๊ธฐ์ˆ  ์Šคํƒ

  • UIKit SnapKit UIHostingController

  • RxSwift Input&Output MVVM Coordinator Clean-Architecture

  • RxAlamofire RequestInterceptor EventMonitor

  • UserDefaults Kingfisher TabMan Toast


๊ตฌํ˜„ ๊ณ ๋ ค์‚ฌํ•ญ

  • ๋””์ž์ธ ์‹œ์Šคํ…œ์œผ๋กœ UI ์ผ๊ด€์„ฑ ์œ ์ง€
  • ๊ณตํ†ต ๋กœ์ง ์žฌ์‚ฌ์šฉ์„ ์œ„ํ•œ ํ”„๋กœํ† ์ฝœ๊ณผ ์ƒ์† ํ™œ์šฉ
  • Rx ์˜ต์ €๋ฒ„๋ธ”์˜ ์ฐธ์กฐ์„ฑ๊ณผ ๋ฐ”์ธ๋”ฉ์„ ํ™œ์šฉํ•˜์—ฌ ๋ทฐ ๊ณ„์ธต๊ฐ„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”
  • Input&Output ํŒจํ„ด์œผ๋กœ ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๊ตฌ์„ฑ
  • ๋„คํŠธ์›Œํฌ ๋‹จ์ ˆ ๋ฐ ์—๋Ÿฌ ์ƒํ™ฉ์— ๋Œ€ํ•œ ์‚ฌ์šฉ์ž ์•ˆ๋‚ด UI ํ‘œ์‹œ ์ฒ˜๋ฆฌ
  • ๋””๋ฐ”์ด์Šค ์‚ฌ์ด์ฆˆ ๊ธฐ๋ฐ˜ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ง•์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”

์•„ํ‚คํ…์ฒ˜

image


๊ธฐ์ˆ  ํ™œ์šฉ

์ƒํ’ˆ ์กฐํšŒ ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ ์šฉ

  • ์ปค๋จธ์Šค ํ”Œ๋žซํผ ์ƒํ’ˆ์€ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€๊ฐ€ ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ฐœ์ƒํ•˜๊ณ , ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜ ํƒ์ƒ‰์ด ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์ฑ„ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์Šคํฌ๋กค ๋™์ž‘์œผ๋กœ ์ธํ•œ API ์ค‘๋ณต ํ˜ธ์ถœ ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด, 1์ดˆ์˜ ์“ฐ๋กœํ‹€๋ง์„ ์ ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.


HTTP Error -> ๋„๋ฉ”์ธ Error ๋งคํ•‘ ๊ณผ์ •

  • API ์‘๋‹ต์—์„œ ๋ฐœ์ƒํ•˜๋Š” HTTP ์ƒํƒœ์ฝ”๋“œ๋ฅผ ๋„๋ฉ”์ธ ์—๋Ÿฌ๋กœ ๋งคํ•‘ํ•˜๋Š” ๋กœ์ง์„ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์—๋Ÿฌ ๋งคํ•‘ ๋กœ์ง์„ ํ”„๋กœํ† ์ฝœ์— ๊ณตํ†ต ๊ตฌํ˜„ํ•˜๊ณ  ํ•„์š”ํ•œ Repository์— ์ฑ„ํƒํ•˜์—ฌ ์žฌ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.


์—๋Ÿฌ ๋งคํ•‘ ํ”Œ๋กœ์šฐ

๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ฃผ์ž… ์˜ˆ์‹œ

Router ๊ด€๋ฆฌ

  • URLRequestConvertible ํ”„๋กœํ† ์ฝœ์„ ๊ตฌํ˜„ํ•˜๋Š” Router ํ”„๋กœํ† ์ฝœ์„ ์ •์˜ํ•ด์„œ API Endpoint๋ฅผ ๊ด€๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.




์•ก์„ธ์Šค ํ† ํฐ ์ž๋™ ๊ฐฑ์‹ ์„ ์œ„ํ•œ Request Interceptor ๊ตฌํ˜„

  • ์•ก์„ธ์Šค ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ ์ž๋™์œผ๋กœ ํ† ํฐ์„ ๊ฐฑ์‹ ํ•˜๊ณ , ๊ฐฑ์‹ ๋œ ํ† ํฐ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด์ „์— ์‹คํŒจํ•œ API ์š”์ฒญ์„ ์ž๋™์œผ๋กœ ์žฌ์ˆ˜ํ–‰ํ•˜๋Š” ๋กœ์ง์„ Request Interceptor ์— ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • AF Session์— Interceptor๋ฅผ ์ฃผ์ž…ํ•ด์„œ, ๋ชจ๋“  API ์š”์ฒญ์— ํ† ํฐ ๊ด€๋ฆฌ ๋ฐ ์žฌ์š”์ฒญ ๋กœ์ง์ด ์ž๋™์œผ๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.


์†Œ์Šค์ฝ”๋“œ
final class APIRequestInterceptor: RequestInterceptor {
  
  func adapt(
    _ urlRequest: URLRequest,
    for session: Session,
    completion: @escaping (Result<URLRequest, any Error>) -> Void
  ) {
    
    /// ๋กœ๊ทธ์ธ ์š”์ฒญ์€ Intercept ํ•˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ์‹คํ–‰
    guard
      let urlString = urlRequest.url?.absoluteString,
      urlString.hasPrefix(APIKey.baseURL),
      UserInfoService.hasSignInLog
    else {
      completion(.success(urlRequest))
      return
    }
    
    /// ๊ฐฑ์‹ ๋œ ํ† ํฐ์œผ๋กœ Header ์žฌ์„ค์ •
    let urlRequest = urlRequest.applied {
      $0.setValue(UserInfoService.accessToken, forHTTPHeaderField: KCHeader.Key.authorization)
    }
    
    completion(.success(urlRequest))
  }
  
  func retry(
    _ request: Request,
    for session: Session,
    dueTo error: any Error,
    completion: @escaping (RetryResult) -> Void
  ) {
    
    /// ์—๋Ÿฌ ์ผ€์ด์Šค๊ฐ€ 419๋ฉด ํ† ํฐ ๋ฆฌํ”„๋ ˆ์‹œ, ์•„๋‹ˆ๋ฉด ํ•จ์ˆ˜ ์ข…๋ฃŒ
    guard 
      let statusCode = request.response?.statusCode,
      statusCode == HTTPStatusError.accessTokenExpired.statusCode
    else {
      return completion(.doNotRetry)
    }
      
    /// ํ† ํฐ ๋ฆฌํ”„๋ ˆ์‹œ ์š”์ฒญ
    AF.request(AuthRouter.tokenRefresh)
      .validate()
      .responseDecodable(of: RefreshTokenResponse.self) { response in
        
        switch response.result {
            /// ์‘๋‹ต ํ† ํฐ์œผ๋กœ ๊ฐฑ์‹  ํ›„ ๊ธฐ์กด API ์žฌ์š”์ฒญ
          case .success(let tokenResponse):
            UserInfoService.renewAccessToken(with: tokenResponse.accessToken)
            completion(.retry)
            
            /// ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜๋ฉด ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ๋Œ์•„๊ฐ€๋„๋ก ์—๋Ÿฌ ๋ฐฉ์ถœ
          case .failure:
            completion(.doNotRetryWithError(HTTPStatusError.refreshTokenExpired))
        }
      }
  }
}

HTTP ์ด๋ฒคํŠธ ๋ชจ๋‹ˆํ„ฐ๋ง ๋กœ์ง ๊ตฌํ˜„

  • API EventMonitor ๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ HTTP ๋ผ์ดํ”„์‚ฌ์ดํด์— ๋Œ€ํ•œ ๋กœ๊น… ํ”„๋กœ์„ธ์Šค๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • HTTP ์š”์ฒญ ๋ฐ ์‘๋‹ต ๊ณผ์ •์„ ๊ตฌ์กฐํ™”๋œ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋””๋ฒ„๊น…์— ํ™œ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.


์†Œ์Šค์ฝ”๋“œ ์˜ˆ์‹œ

์ฝ˜์†” ๋กœ๊ทธ ์˜ˆ์‹œ

์ปค์Šคํ…€ UI ์ปดํฌ๋„ŒํŠธ ์„ค๊ณ„

  • ๊ธฐ๋Šฅ์ด ํฌํ•จ๋œ ์ปค์Šคํ…€ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์šฉํ•ด์„œ ๋กœ์ง ์žฌ์‚ฌ์šฉ์„ฑ ๋ฐ UI ์ผ๊ด€์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค.

์ปค์Šคํ…€ UI ์ปดํฌ๋„ŒํŠธ ์˜ˆ์‹œ

ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

์ƒํ’ˆ ์ด๋ฏธ์ง€ ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”

  • ์ƒํ’ˆ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋Š” UI์—์„œ ๊ณ ํ•ด์ƒ๋„์˜ ์›๋ณธ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋”ฉํ•จ์— ๋”ฐ๋ผ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ์ฆ๊ฐ€ํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ๊ฒฝํ—˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • Kingfisher ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ DownsamplingImageProcessor ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜์—ฌ, ๋””์Šคํ”Œ๋ ˆ์ด ํ•ด์ƒ๋„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ค์šด์ƒ˜ํ”Œ๋ง์„ ์ ์šฉํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์•ฝ 61.3% ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.



๋„คํŠธ์›Œํฌ ์ƒํƒœ ์ธํ„ฐํŽ˜์ด์Šค ์ฒ˜๋ฆฌ ๋ฌธ์ œ

  • ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ ๋ณ€๋™ ์‹œ UI์— ์ƒํƒœ ์•ˆ๋‚ด๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•˜์—ฌ, NWPathMonitor ๋ฅผ ํ™œ์šฉํ•ด ๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ๋ชจ๋“  VC๊ฐ€ ์ƒ์†๋ฐ›๋Š” Base VC ๋‚ด์— ์ƒํƒœ ํ‘œ์‹œ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • addSubview ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ˜„์žฌ ๋ทฐ ๊ณ„์ธต์˜ ๊ฐ€์žฅ ํ•˜์œ„์— ์•ˆ๋‚ด UI๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค ํ–ˆ์œผ๋‚˜ ์˜๋„์™€ ๋‹ฌ๋ฆฌ UI๊ฐ€ ํ™”๋ฉด์„ ์™„์ „ํžˆ ๊ฐ€๋ฆฌ์ง€ ๋ชปํ•˜๊ณ , Base VC๋ฅผ ์ƒ์†๋ฐ›์€ ๋ชจ๋“  ๋ทฐ ์ธ์Šคํ„ด์Šค์— ์ค‘๋ณต์œผ๋กœ ์ถ”๊ฐ€๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.



UIWindow๋ฅผ ์ด์šฉํ•œ ๊ฐœ์„ 

  • ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด UIWindow ๋ฅผ ํ™œ์šฉํ•˜๋Š” ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • SceneDelegate์—์„œ ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ๋ฉ”์ธ Window๋ณด๋‹ค ์ƒ์œ„ ๋ ˆ๋ฒจ์ธ ErrorWindow๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ UI๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ , ๋„คํŠธ์›Œํฌ ์ƒํƒœ๊ฐ€ ์ •์ƒ์œผ๋กœ ๋Œ์•„์˜ค๋ฉด ์›๋ž˜ ํ™”๋ฉด์„ ์žฌ๊ฐœํ•˜๋Š” ๋กœ์ง์„ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

  • Scene์˜ ์—ฐ๊ฒฐ / ๋Š๊น€ ์ƒ๋ช…์ฃผ๊ธฐ ์ด๋ฒคํŠธ์— ๋ชจ๋‹ˆํ„ฐ๋ง ๋กœ์ง์„ ์—ฐ๋™ํ•˜์—ฌ, ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฆฌ์†Œ์Šค๊ฐ€ ํšŒ์ˆ˜๋˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.



๋ชจ๋‹ˆํ„ฐ๋ง ์†Œ์Šค์ฝ”๋“œ
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

  ...
    
  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    
    ...
    
    startMonitoring(scene: scene)
  }
  
  private func startMonitoring(scene: UIScene) {
    NetworkMonitor.shared.start { [weak self] path in
      guard let self else { return }
      
      switch path.status {
        case .satisfied:
          setErrorWindowOff(scene: scene)
        default:
          setErrorWindowOn(scene: scene)
      }
    }
  }
  
  private func setErrorWindowOn(scene: UIScene) {
    guard let windowScene = scene as? UIWindowScene else { return }
    
    self.errorWindow = UIWindow(windowScene: windowScene).configured {
      $0.windowLevel = .statusBar
      $0.addSubview(NetworkUnsatisfiedView(frame: $0.bounds))
      $0.makeKeyAndVisible()
    }
  }
  
  private func setErrorWindowOff(scene: UIScene) {
    errorWindow?.resignKey()
    errorWindow = nil
  }
    
  ...
  
}



About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages