다크 모드
네트워크 레이어
Moya + Alamofire 기반 네트워크 구조
truloop iOS의 네트워크 레이어는 Moya 라이브러리를 기반으로 구축되어 있으며, Alamofire 위에서 동작합니다.
구성 요소
API Target 패턴
각 API 엔드포인트 그룹은 enum으로 정의되며, Moya의 TargetType 프로토콜을 채택합니다.
TargetType 기본 설정
모든 API Target은 공통 TargetType extension을 공유합니다:
swift
public extension TargetType {
var baseURL: URL {
URL(string: Constants.serverURLString)!
}
var commonHeaders: [String: String] {
return [
"X-Platform": "iOS",
"X-App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown",
"X-Local-Timezone": TimeZone.current.identifier,
]
}
}API Target 목록
| Target | 역할 | API 버전 |
|---|---|---|
AuthAPI | 인증, 회원가입, 전화번호 인증 | v1 |
HomeAPI | 룹 CRUD, 미디어, 리캡(내부명: Story), 미션, 약속, 커버, 차단 등 | v1 / v3 |
ContactAPI | 연락처 동기화 | v1 |
FriendAPI | 친구 관리 | v1 |
DeviceAPI | 디바이스 등록 (FCM), 채팅 세션 토큰, 앱 버전 체크 | v1 |
LooptAPI | AI 비서 에이전트 연동 (service: loopt) | v1 |
NotificationAPI | 알림 조회, 읽음 처리 | v1 |
LiveActivityAPI | Live Activity 토큰 | v1 |
API Target 구현 예시
swift
public enum AuthAPI {
case requestVerificationCode(phoneNumber: String)
case verify(phoneNumber: String, code: String)
case login(phoneNumber: String, verificationToken: String)
case refreshToken(refreshToken: String)
case logout(refreshToken: String)
case me
case signUp(userName: String, phoneNumber: String, name: String,
verificationToken: String, referralCode: String?, locale: String?)
case usernameCheck(userName: String)
case deleteAccount
case profileUploadURL(fileName: String, fileType: String)
}
extension AuthAPI: TargetType {
public var service: String { "core" }
public var apiVersion: String { "api/v1" }
public var path: String {
let path: String
switch self {
case .login: path = "auth/login"
case .refreshToken: path = "auth/refresh"
// ...
}
return "\(service)/\(apiVersion)/\(path)"
}
}정보
API 경로는 {service}/{apiVersion}/{path} 형식으로 구성됩니다. 대부분의 API Target은 service가 core이며, LooptAPI만 service가 loopt입니다. HomeAPI의 룹 관련 엔드포인트 대부분은 v3, 나머지는 v1을 사용합니다.
Request / Response 처리
Networking 프로토콜
swift
public protocol Networking {
func request<T: Decodable>(target: TargetType) async throws -> T
func request(target: TargetType) async throws
}두 가지 메서드를 제공합니다:
- 응답이 있는 요청:
request<T: Decodable>(target:)-- JSON 응답을T타입으로 디코딩 - 응답 없는 요청:
request(target:)-- 성공 여부만 확인 (DELETE 등)
NetworkingImpl
MoyaProvider<MultiTarget>을 내부적으로 사용하며, 모든 요청을 async/await으로 래핑합니다. 초기화 시 ValidatingSession과 NetworkLoggerPlugin을 설정합니다:
swift
final class NetworkingImpl: Networking {
private let provider: MoyaProvider<MultiTarget>
private let networkingErrorRouter: NetworkingErrorRouter
init(
authInterceptor: AuthInterceptor,
networkingErrorRouter: NetworkingErrorRouter
) {
provider = MoyaProvider<MultiTarget>(
session: ValidatingSession(interceptor: authInterceptor),
plugins: [
NetworkLoggerPlugin(configuration: .init(logOptions: .verbose))
]
)
self.networkingErrorRouter = networkingErrorRouter
}
func request<T: Decodable>(target: TargetType) async throws -> T {
do {
return try await provider.request(MultiTarget(target))
.filterSuccessfulStatusCodes()
.map(T.self)
} catch {
throw handleError(error)
}
}
}MoyaProvider async 확장
Moya의 콜백 기반 API를 async/await으로 변환하는 확장이 포함되어 있습니다:
swift
public extension MoyaProvider {
func request(_ target: Target, ...) async throws -> Response {
try await withCheckedThrowingContinuation { continuation in
self.request(target) { result in
switch result {
case .success(let response): continuation.resume(returning: response)
case .failure(let error): continuation.resume(throwing: error)
}
}
}
}
}에러 처리
에러 타입 계층
서버 에러 응답은 TruloopError 구조체로 파싱됩니다:
swift
public struct TruloopError: LocalizedError, Decodable {
let rawType: String // "ERROR_TYPE_NOT_FOUND" 등
public let displayType: DisplayType // toast, dialog, none 등
public let message: Message // locale, text
public var type: TruloopErrorType {
return TruloopErrorType(rawValue: self.rawType)
}
}TruloopErrorType은 rawType 문자열을 Swift enum으로 변환합니다:
.notFound--ERROR_TYPE_NOT_FOUND.userNotFound--ERROR_TYPE_USER_NOT_FOUND.invalidVerificationCode--ERROR_TYPE_INVALID_VERIFICATION.internalServerError--ERROR_TYPE_INTERNAL_SERVER_ERROR.unspecified(String)-- 그 외 모든 에러 타입
에러 파싱 흐름
NetworkingError.parseError에서 Moya 에러를 분석하여 적절한 NetworkingError 케이스로 변환합니다:
MoyaError.statusCode-- 응답 body를TruloopError로 디코딩 시도 →.serverError또는.objectMappingMoyaError.underlying-- underlying 응답에서TruloopError디코딩 시도MoyaError.objectMapping--.objectMapping- 그 외 --
.unknown
에러 표시 방식
서버에서 display_type 필드를 통해 클라이언트에서 에러를 어떻게 표시할지 지정합니다. NetworkingImpl의 handleError에서 NetworkingErrorRouter를 통해 에러를 자동으로 표시합니다:
| NetworkingError 케이스 | DisplayType | 표시 방식 |
|---|---|---|
.serverError | .dialog | Alert Popup |
.serverError | .toast | Toast |
.serverError | 그 외 | 표시하지 않음 |
.objectMapping | -- | Toast (파싱 실패 메시지) |
.unknown | -- | Alert Popup (에러 메시지) |
Authentication Interceptor
AuthInterceptor
Alamofire의 RequestInterceptor 프로토콜을 채택하여 인증 토큰 관리를 담당합니다. 프로토콜은 Networking 모듈에 선언되고, 구현체 AuthInterceptorImpl은 Repository 모듈에 위치합니다.
swift
// Networking 모듈
public protocol AuthInterceptor: RequestInterceptor { }adapt -- 토큰 주입
KeychainStore에서AuthToken을 로드- Access Token을 AES-256 복호화 후
Authorization: Bearer {token}헤더에 추가 - 토큰이 없는 경우 (비로그인 상태) 헤더 없이 요청 진행
retry -- 토큰 갱신
- 401 응답 시에만 동작
- URL별 재시도 횟수 추적 (최대 2회)
- 동시 다발 요청 시 하나의 refresh 요청만 실행하고, 나머지는 대기열에 추가
- Refresh Token을 AES-256 복호화 후
AuthAPI.refreshToken으로 재인증 - 성공: 새 토큰을 AES-256 암호화 후 Keychain에 저장, 대기 중인 모든 요청 재시도
- 실패: 대기 중인 모든 요청 취소,
RootSceneRouteUseCase를 통해 Splash 화면으로 이동
ValidatingSession
Alamofire Session을 상속한 커스텀 세션으로, 200-299 범위 외의 상태 코드를 자동으로 검증합니다:
swift
final class ValidatingSession: Session, @unchecked Sendable {
override func request(
_ convertible: any URLRequestConvertible,
interceptor: (any RequestInterceptor)? = nil
) -> DataRequest {
let dataRequest = super.request(convertible, interceptor: interceptor)
return dataRequest.validate(statusCode: 200..<300)
}
}공통 헤더
모든 API 요청에 다음 헤더가 포함됩니다:
| 헤더 | 값 |
|---|---|
X-Platform | "iOS" |
X-App-Version | 앱 버전 (CFBundleShortVersionString) |
X-Local-Timezone | 디바이스 타임존 (예: Asia/Seoul) |
Authorization | Bearer {accessToken} (AuthInterceptor가 주입) |
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-03-10 | 소스 코드 기반 검증: TruloopError 타입명 수정, NetworkLoggerPlugin 추가, 에러 파싱 흐름 상세화, AuthInterceptor 토큰 갱신 로직 상세화, LooptAPI 역할 명확화, ValidatingSession Sendable 반영 |