Skip to content

C6-DANDAN/ios-network-module

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

8 Commits
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

iOS JWT Network Layer

JWT 기반의 μžλ™ 토큰 κ°±μ‹  κΈ°λŠ₯을 κ°–μΆ˜ iOS λ„€νŠΈμ›Œν¬ λ ˆμ΄μ–΄μž…λ‹ˆλ‹€. 인터셉터 νŒ¨ν„΄μ„ ν™œμš©ν•˜μ—¬ 401 μ—λŸ¬ λ°œμƒ μ‹œ μžλ™μœΌλ‘œ 토큰을 κ°±μ‹ ν•˜κ³  μš”μ²­μ„ μž¬μ‹œλ„ν•©λ‹ˆλ‹€.

μ£Όμš” κΈ°λŠ₯

  • βœ… JWT 인증: Access Tokenκ³Ό Refresh Token 기반 인증
  • βœ… μžλ™ 토큰 κ°±μ‹ : 401 μ—λŸ¬ λ°œμƒ μ‹œ μžλ™μœΌλ‘œ 토큰 μž¬λ°œκΈ‰ 및 μš”μ²­ μž¬μ‹œλ„
  • βœ… 인터셉터 νŒ¨ν„΄: μš”μ²­ 전후에 μ»€μŠ€ν…€ 둜직 μ‚½μž… κ°€λŠ₯
  • βœ… 토큰 κ°±μ‹  동기화: Actor 기반 λ™μ‹œμ„± μ œμ–΄λ‘œ 쀑볡 κ°±μ‹  λ°©μ§€
  • βœ… 게슀트 읡λͺ… 둜그인: κ°„νŽΈν•œ 게슀트 인증 지원
  • βœ… Multipart/form-data μ—…λ‘œλ“œ: 이미지 μžλ™ 리사이징 및 μ••μΆ•
  • βœ… Keychain μ €μž₯μ†Œ: μ•ˆμ „ν•œ 토큰 μ €μž₯
  • βœ… Combine + async/await: λ‚΄λΆ€λŠ” async/await, μ™ΈλΆ€ APIλŠ” Combine 제곡
  • βœ… ν”„λ‘œν† μ½œ 기반 섀계: ν…ŒμŠ€νŠΈμ™€ μ˜μ‘΄μ„± μ£Όμž… 용이

μ•„ν‚€ν…μ²˜

계측 ꡬ쑰

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         UI Layer (SwiftUI)              β”‚
β”‚  ContentView, GuestLoginViewModel       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Service Layer (Combine)            β”‚
β”‚  GuestAuthService, MultipartUpload      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    Network Layer (async/await)          β”‚
β”‚  NetworkService + Interceptors          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Storage Layer (Keychain)           β”‚
β”‚  TokenManager, KeychainService          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

핡심 νŒ¨ν„΄

1. 인터셉터 νŒ¨ν„΄

λͺ¨λ“  λ„€νŠΈμ›Œν¬ μš”μ²­μ€ 인터셉터λ₯Ό 거치며, μš”μ²­ 전후에 μ»€μŠ€ν…€ λ‘œμ§μ„ μ‹€ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

protocol RequestInterceptor {
    func adapt(_ request: URLRequest, for endpoint: any APIEndpoint) async throws -> URLRequest
    func retry(_ request: URLRequest, for endpoint: any APIEndpoint, dueTo error: NetworkError) async throws -> URLRequest
}

2. Actor 기반 토큰 κ°±μ‹  동기화

μ—¬λŸ¬ μš”μ²­μ΄ λ™μ‹œμ— 401을 받아도 토큰 갱신은 ν•œ 번만 μ‹€ν–‰λ©λ‹ˆλ‹€.

actor TokenRefreshCoordinator {
    private var refreshTask: Task<(String, String), Error>?

    func refresh(using refreshToken: String, endpoint: AuthEndpoint) async throws -> (String, String) {
        if let existingTask = refreshTask {
            return try await existingTask.value
        }
        // μƒˆλ‘œμš΄ κ°±μ‹  μž‘μ—… μ‹œμž‘
    }
}

폴더 ꡬ쑰

network-module/
β”œβ”€β”€ NetworkLayer/
β”‚   β”œβ”€β”€ Core/                          # 핡심 λ„€νŠΈμ›Œν¬ 둜직
β”‚   β”‚   β”œβ”€β”€ APIEndpoint.swift          # μ—”λ“œν¬μΈνŠΈ ν”„λ‘œν† μ½œ
β”‚   β”‚   β”œβ”€β”€ NetworkService.swift       # λ„€νŠΈμ›Œν¬ μ„œλΉ„μŠ€ (메인)
β”‚   β”‚   β”œβ”€β”€ NetworkError.swift         # μ—λŸ¬ μ •μ˜
β”‚   β”‚   └── NetworkConfig.swift        # μ„€μ • (Base URL λ“±)
β”‚   β”‚
β”‚   β”œβ”€β”€ Interceptors/                  # 인터셉터
β”‚   β”‚   β”œβ”€β”€ RequestInterceptor.swift   # 인터셉터 ν”„λ‘œν† μ½œ
β”‚   β”‚   └── AuthenticationInterceptor.swift  # JWT 인증 인터셉터
β”‚   β”‚
β”‚   β”œβ”€β”€ Auth/                          # 인증 κ΄€λ ¨
β”‚   β”‚   β”œβ”€β”€ TokenManager.swift         # 토큰 관리
β”‚   β”‚   β”œβ”€β”€ TokenRefreshCoordinator.swift  # 토큰 κ°±μ‹  동기화
β”‚   β”‚   β”œβ”€β”€ KeychainService.swift      # Keychain μ €μž₯μ†Œ
β”‚   β”‚   β”œβ”€β”€ AuthEndpoint.swift         # 인증 API μ—”λ“œν¬μΈνŠΈ
β”‚   β”‚   └── GuestAuthService.swift     # 게슀트 인증 μ„œλΉ„μŠ€
β”‚   β”‚
β”‚   β”œβ”€β”€ Models/                        # 데이터 λͺ¨λΈ
β”‚   β”‚   β”œβ”€β”€ User.swift                 # μ‚¬μš©μž λͺ¨λΈ
β”‚   β”‚   β”œβ”€β”€ GuestRegisterResponse.swift  # 게슀트 등둝 응닡
β”‚   β”‚   β”œβ”€β”€ TokenResponse.swift        # 토큰 응닡
β”‚   β”‚   └── EmptyResponse.swift        # 빈 응닡
β”‚   β”‚
β”‚   └── Examples/                      # μ‚¬μš© 예제
β”‚       └── UsageExample.swift         # Combine 기반 μ‚¬μš© 예제
β”‚
β”œβ”€β”€ MultipartUploadHelper.swift        # 이미지 μ—…λ‘œλ“œ 헬퍼
β”œβ”€β”€ GuestLoginViewModel.swift          # 게슀트 둜그인 ViewModel
└── ContentView.swift                  # 데λͺ¨ UI

핡심 μ»΄ν¬λ„ŒνŠΈ

1. NetworkService (Core)

λͺ¨λ“  API μš”μ²­μ˜ μ§„μž…μ μž…λ‹ˆλ‹€. 인터셉터λ₯Ό μ μš©ν•˜κ³  λ‚΄λΆ€μ μœΌλ‘œ async/await을 μ‚¬μš©ν•˜λ©°, μ™ΈλΆ€μ—λŠ” Combine APIλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.

μ£Όμš” λ©”μ„œλ“œ:

func request<T: Decodable>(_ endpoint: any APIEndpoint) -> AnyPublisher<T, NetworkError>

νŠΉμ§•:

  • μ œλ„€λ¦­μ„ ν†΅ν•œ νƒ€μž… μ•ˆμ „μ„±
  • μžλ™ JSON λ””μ½”λ”©
  • 인터셉터 체인 μ‹€ν–‰
  • 401 μ—λŸ¬ μ‹œ μžλ™ μž¬μ‹œλ„

2. AuthenticationInterceptor (Interceptors)

JWT 토큰을 μžλ™μœΌλ‘œ μ£Όμž…ν•˜κ³ , 401 μ—λŸ¬ λ°œμƒ μ‹œ 토큰을 κ°±μ‹ ν•©λ‹ˆλ‹€.

adapt 단계:

func adapt(_ request: URLRequest, for endpoint: any APIEndpoint) async throws -> URLRequest {
    guard endpoint.requiresAuthentication else { return request }

    var authenticatedRequest = request
    authenticatedRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
    return authenticatedRequest
}

retry 단계:

func retry(_ request: URLRequest, for endpoint: any APIEndpoint, dueTo error: NetworkError) async throws -> URLRequest {
    guard case .unauthorized = error else { throw error }

    // 토큰 κ°±μ‹  ν›„ μž¬μ‹œλ„
    let (newAccessToken, newRefreshToken) = try await coordinator.refresh(...)
    // μƒˆ ν† ν°μœΌλ‘œ μš”μ²­ μž¬κ΅¬μ„±
}

3. TokenManager (Auth)

ν† ν°μ˜ μ €μž₯, 쑰회, μ‚­μ œλ₯Ό λ‹΄λ‹Ήν•©λ‹ˆλ‹€. Keychain을 톡해 μ•ˆμ „ν•˜κ²Œ 토큰을 κ΄€λ¦¬ν•©λ‹ˆλ‹€.

μ£Όμš” λ©”μ„œλ“œ:

func saveTokens(accessToken: String, refreshToken: String) throws
func getAccessToken() throws -> String
func getRefreshToken() throws -> String
func clearTokens() throws
func isAuthenticated() -> Bool

4. TokenRefreshCoordinator (Auth)

Actorλ₯Ό μ‚¬μš©ν•˜μ—¬ 토큰 κ°±μ‹  μž‘μ—…μ„ λ™κΈ°ν™”ν•©λ‹ˆλ‹€. μ—¬λŸ¬ μš”μ²­μ΄ λ™μ‹œμ— 401을 받아도 단 ν•œ 번만 κ°±μ‹ ν•©λ‹ˆλ‹€.

λ™μž‘ 방식:

actor TokenRefreshCoordinator {
    private var refreshTask: Task<(String, String), Error>?

    func refresh(...) async throws -> (String, String) {
        // μ§„ν–‰ 쀑인 κ°±μ‹  μž‘μ—…μ΄ 있으면 κ·Έ κ²°κ³Όλ₯Ό κΈ°λ‹€λ¦Ό
        if let existingTask = refreshTask {
            return try await existingTask.value
        }

        // μƒˆλ‘œμš΄ κ°±μ‹  μž‘μ—… μ‹œμž‘
        let task = Task { ... }
        refreshTask = task
        defer { refreshTask = nil }
        return try await task.value
    }
}

5. MultipartUploadHelper

이미지λ₯Ό ν¬ν•¨ν•œ multipart/form-data μ—…λ‘œλ“œλ₯Ό μ²˜λ¦¬ν•©λ‹ˆλ‹€. μžλ™ 리사이징 및 μ••μΆ• κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€.

μ£Όμš” κΈ°λŠ₯:

  • 이미지λ₯Ό μ΅œλŒ€ 1024x1024둜 리사이징
  • 점진적 μ••μΆ• (0.8 β†’ 0.1 ν’ˆμ§ˆ)
  • 5MB 크기 μ œν•œ μ€€μˆ˜

μ‚¬μš© 예:

let response = try await MultipartUploadHelper.uploadGuestRegister(
    name: "μ‚¬μš©μž",
    image: profileImage
)

6. KeychainService (Auth)

iOS Keychain을 톡해 μ•ˆμ „ν•˜κ²Œ 데이터λ₯Ό μ €μž₯ν•©λ‹ˆλ‹€.

μ£Όμš” λ©”μ„œλ“œ:

func save(key: String, data: Data) throws
func read(key: String) throws -> Data
func delete(key: String) throws

ν”Œλ‘œμš° μ„€λͺ…

1. 게슀트 등둝 ν”Œλ‘œμš°

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  μ‚¬μš©μž  β”‚ 이름 + 이미지 μž…λ ₯
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
     β”‚
     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ GuestLoginViewModel     β”‚ registerGuest()
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ MultipartUploadHelper   β”‚ 이미지 리사이징 + μ••μΆ•
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β–Ό POST /auth/guest/register
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Backend API        β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β–Ό 200 OK { data: { user, accessToken, refreshToken } }
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    TokenManager         β”‚ saveTokens()
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   KeychainService       β”‚ Keychain에 토큰 μ €μž₯
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. 인증이 ν•„μš”ν•œ API μš”μ²­ ν”Œλ‘œμš°

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  μ‚¬μš©μž  β”‚ API 호좜
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
     β”‚
     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   NetworkService        β”‚ request()
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ AuthenticationInterceptorβ”‚ adapt() - 토큰 μ£Όμž…
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β–Ό Authorization: Bearer <accessToken>
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Backend API        β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β”œβ”€β–Ί 200 OK β†’ 응닡 λ°˜ν™˜
     β”‚
     └─► 401 Unauthorized
          β”‚
          β–Ό
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚ AuthenticationInterceptorβ”‚ retry()
     β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚ TokenRefreshCoordinator β”‚ refresh() - 쀑볡 λ°©μ§€
     β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό POST /auth/refresh { refreshToken }
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚      Backend API        β”‚
     β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό 200 OK { accessToken, refreshToken }
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚    TokenManager         β”‚ saveTokens()
     β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό μ›λž˜ μš”μ²­ μž¬μ‹œλ„ (μƒˆ 토큰 μ‚¬μš©)
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚      Backend API        β”‚
     β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό 200 OK
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚   μ‚¬μš©μžμ—κ²Œ 응닡 λ°˜ν™˜   β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

3. λ™μ‹œ λ‹€λ°œμ  401 μ—λŸ¬ 처리 ν”Œλ‘œμš°

μ—¬λŸ¬ API μš”μ²­μ΄ λ™μ‹œμ— 401을 받은 경우:

Request A ──┐
            β”‚
Request B ──┼─► 401 Unauthorized
            β”‚
Request C β”€β”€β”˜
     β”‚
     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   TokenRefreshCoordinator (Actor)   β”‚
β”‚                                     β”‚
β”‚  Request A: refresh() μ‹œμž‘          β”‚
β”‚  Request B: μ§„ν–‰ 쀑인 Task λŒ€κΈ°     β”‚
β”‚  Request C: μ§„ν–‰ 쀑인 Task λŒ€κΈ°     β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β–Ό 단 ν•œ 번만 토큰 κ°±μ‹  μš”μ²­
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Backend API        β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β–Ό μƒˆλ‘œμš΄ 토큰
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Request A, B, C λͺ¨λ‘   β”‚
β”‚  μƒˆ ν† ν°μœΌλ‘œ μž¬μ‹œλ„     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

μ‚¬μš© 예제

1. 게슀트 등둝

import Combine

class GuestLoginViewModel: ObservableObject {
    @Published var userName = ""
    @Published var selectedImage: UIImage?
    @Published var isAuthenticated = false

    private let tokenManager: TokenManagerProtocol

    func registerGuest() {
        Task {
            do {
                // multipart/form-data둜 μ—…λ‘œλ“œ
                let response = try await MultipartUploadHelper.uploadGuestRegister(
                    name: userName,
                    image: selectedImage
                )

                // 토큰 μ €μž₯
                try tokenManager.saveTokens(
                    accessToken: response.data.accessToken,
                    refreshToken: response.data.refreshToken
                )

                isAuthenticated = true
                print("βœ… 등둝 성곡: \(response.data.user.name)")
            } catch {
                print("❌ 등둝 μ‹€νŒ¨: \(error)")
            }
        }
    }
}

2. 인증이 ν•„μš”ν•œ API 호좜

import Combine

class ArticleService {
    private let networkService: NetworkServiceProtocol
    private var cancellables = Set<AnyCancellable>()

    init(networkService: NetworkServiceProtocol = NetworkService()) {
        self.networkService = networkService
    }

    func fetchArticles() {
        networkService.request(ArticleEndpoint.getArticles)
            .receive(on: DispatchQueue.main)
            .sink { completion in
                if case .failure(let error) = completion {
                    print("❌ μ—λŸ¬: \(error)")
                }
            } receiveValue: { (articles: [Article]) in
                print("βœ… κ²Œμ‹œκΈ€ \(articles.count)개 λ‘œλ“œ")
            }
            .store(in: &cancellables)
    }
}

3. μ»€μŠ€ν…€ μ—”λ“œν¬μΈνŠΈ μ •μ˜

enum ArticleEndpoint: APIEndpoint {
    case getArticles
    case getArticle(id: Int)
    case createArticle(title: String, content: String)

    var path: String {
        switch self {
        case .getArticles:
            return "/articles"
        case .getArticle(let id):
            return "/articles/\(id)"
        case .createArticle:
            return "/articles"
        }
    }

    var method: HTTPMethod {
        switch self {
        case .getArticles, .getArticle:
            return .get
        case .createArticle:
            return .post
        }
    }

    var headers: [String: String]? {
        return ["Content-Type": "application/json"]
    }

    var body: Data? {
        switch self {
        case .createArticle(let title, let content):
            let params = ["title": title, "content": content]
            return try? JSONEncoder().encode(params)
        default:
            return nil
        }
    }

    var requiresAuthentication: Bool {
        return true  // λͺ¨λ“  κ²Œμ‹œκΈ€ APIλŠ” 인증 ν•„μš”
    }
}

4. λ‘œκ·Έμ•„μ›ƒ

func logout() {
    do {
        try tokenManager.clearTokens()
        isAuthenticated = false
        print("βœ… λ‘œκ·Έμ•„μ›ƒ 성곡")
    } catch {
        print("❌ λ‘œκ·Έμ•„μ›ƒ μ‹€νŒ¨: \(error)")
    }
}

μ„€μΉ˜ 및 μ‚¬μš©

1. ν”„λ‘œμ νŠΈμ— μΆ”κ°€

NetworkLayer 폴더λ₯Ό Xcode ν”„λ‘œμ νŠΈμ— λ“œλž˜κ·Έ μ•€ λ“œλ‘­ν•˜μ—¬ μΆ”κ°€ν•©λ‹ˆλ‹€.

2. Base URL μ„€μ •

NetworkConfig.swiftμ—μ„œ μ„œλ²„ URL을 μ„€μ •ν•©λ‹ˆλ‹€:

struct NetworkConfig {
    static let baseURL = "https://your-server.com/api/v1"
    static let timeout: TimeInterval = 30
}

3. μ‚¬μš©

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

ν…ŒμŠ€νŠΈ

ν”„λ‘œν† μ½œ 기반 μ„€κ³„λ‘œ μ‰½κ²Œ ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€:

// Mock TokenManager
class MockTokenManager: TokenManagerProtocol {
    var savedAccessToken: String?
    var savedRefreshToken: String?

    func saveTokens(accessToken: String, refreshToken: String) throws {
        savedAccessToken = accessToken
        savedRefreshToken = refreshToken
    }

    func getAccessToken() throws -> String {
        return savedAccessToken ?? ""
    }

    // ...
}

// Mock NetworkService
class MockNetworkService: NetworkServiceProtocol {
    var mockResponse: Any?
    var mockError: NetworkError?

    func request<T: Decodable>(_ endpoint: any APIEndpoint) -> AnyPublisher<T, NetworkError> {
        if let error = mockError {
            return Fail(error: error).eraseToAnyPublisher()
        }
        if let response = mockResponse as? T {
            return Just(response)
                .setFailureType(to: NetworkError.self)
                .eraseToAnyPublisher()
        }
        return Fail(error: .invalidResponse).eraseToAnyPublisher()
    }
}

μ„œλ²„ 응닡 ν˜•μ‹

이 ν”„λ‘œμ νŠΈλŠ” λ‹€μŒκ³Ό 같은 ν‘œμ€€ 응닡 ν˜•μ‹μ„ κΈ°λŒ€ν•©λ‹ˆλ‹€:

{
  "success": true,
  "code": "OK",
  "message": "μš”μ²­μ΄ μ„±κ³΅μ μœΌλ‘œ μ²˜λ¦¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.",
  "data": {
    "user": {
      "id": "uuid-string",
      "name": "μ‚¬μš©μž",
      "profileUrl": "https://...",
      "profileImageKey": "profiles/...",
    },
    "accessToken": "eyJ...",
    "refreshToken": "eyJ..."
  },
  "errors": [],
  "meta": {
    "timestamp": "2025-11-02T06:18:09.844Z",
    "path": "/api/v1/auth/guest/register",
    "requestId": "uuid-string",
    "durationMs": 115
  }
}

μ£Όμš” 기술 μŠ€νƒ

  • Swift 5.5+ (async/await, Actor)
  • Combine (μ™ΈλΆ€ API)
  • SwiftUI (UI)
  • Keychain (λ³΄μ•ˆ μ €μž₯μ†Œ)
  • URLSession (λ„€νŠΈμ›Œν¬)

λΌμ΄μ„ μŠ€

MIT License

κΈ°μ—¬

μ΄μŠˆμ™€ PR을 ν™˜μ˜ν•©λ‹ˆλ‹€!

문의

λ¬Έμ œκ°€ μžˆκ±°λ‚˜ 질문이 μžˆμœΌμ‹œλ©΄ 이슈λ₯Ό λ“±λ‘ν•΄μ£Όμ„Έμš”.


Made with ❀️ for iOS Developers

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages