코디 정보를 공유하는 커뮤니티 앱
개발 환경
- 최소 버전 : iOS 16.0
- 개발 인원: 1인
- 개발 기간: 2023.11.22 ~ 2023.12.17
앱 소개
- 회원가입, 로그인 기능
- 사진과 텍스트 기반 게시글 업로드
- 유저들이 올린 게시물 확인
기술 스택
- UIKit(CodeBase), PhotosUI, KeyChain
- MVVM, Input/Output, Singleton, DTO
- RxSwift, Moya, Alamofire, Snapkit, Kingfisher
주요 기능
- Regular Experession을 사용하여 회원가입 유효성 검증 및 RxSwift 활용한 반응형 UI 구현
- JWT Token 기반 로그인 구현 및 KeyChain을 통한 Token 관리
- RequestInterceptor를 통한 AccessToken 갱신 및 RefreshToken 만료 시 대응
- RxSwift의 Single Trait를 활용한 네트워크 요청 로직 구현
- Moya를 활용한 Router Pattern 구성
- Custom Result와 Custom Error 구성을 통한 에러 핸들링
- Cursor Based Pagination을 통한 데이터 갱신
- multipart/form-data 를 통한 이미지 및 텍스트 데이터 업로드
문제점
서버에서 오는주는 에러에 유연하게 대응하기 위해 프로토콜을 정의하고 이를 채택하는 enum을 활용하여 에러 케이스들을 정의하였으나 이를 Result타입과 함께 사용하지 못하는 문제
protocol LoggableError: Error {
var rawValue: Int { get }
var errorDescription: String { get }
}
enum CommonError: Int, LoggableError {
case requiredValue = 400
case noUserWrongPassword = 401
case forbiden = 403
...
var errorDescription: String {
switch self {
case .serverKey:
return "This service sesac_memolease only"
case .overCall:
return "과호출입니다."
...
default:
return ""
}
}
}
...
해결법
Generic을 활용한 Custom Type을 정의하여 각기 다른 API 호출의 응답에 따른 반환 값에 대응할 수 있게 만듦
모든 API에서 공통적으로 오는 에러에 대해서 1차적으로 필터링을 하고 만약 공통 에러에서 걸러지지 않는 에러인 경우 에러 자체를 던져주어 활용할 수 있도록 설계함
enum CustomResult<T, E> {
case success(T)
case failure(E)
}
func loginRequest(api: API) -> Single<CustomResult<LogInResponse, LoggableError>>{
return Single.create { single in
self.provider.request(api) { result in
switch result {
case.success(let value):
print("Success", value.statusCode, value.data)
guard let data = try? JSONDecoder().decode(LogInResponse.self, from: value.data) else {
return
}
single(.success(.success(data)))
case .failure(let error):
print("Failure", error)
guard let statusCode = error.response?.statusCode, let commonError = CommonError(rawValue: statusCode) else {
single(.success(.failure(CommonError.unknownError)))
return
}
single(.success(.failure(commonError)))
}
}
return Disposables.create()
}
}
문제점
네트워크 요청 과정에서 성공과 실패에 대해서만 관리할 수 있는 RxSwift의 Single을 활용하였으나 single(.failure(error))를 return 하는 경우 flatMap으로 연결된 Stream이 dispose되어 다시 요청할 수 없는 문제가 발생
func signUpRequest(api: API) -> Single<CustomResult<JoinResponse, LoggableError>> {
return Single.create { single in
self.provider.request(api) { result in
switch result {
case.success(let value):
...
single(.success(.success(data)))
case.failure(let error):
...
single(.failure(.failure(commonError)))
}
}
return Disposables.create()
}
}
해결법
네트워크 요청 시 성공, 실패 여부와 상관 없이 Single의 success 이벤트를 발행하여 구독을 유지
func signUpRequest(api: API) -> Single<CustomResult<JoinResponse, LoggableError>> {
return Single.create { single in
self.provider.request(api) { result in
switch result {
case.success(let value):
...
single(.success(.success(data)))
case.failure(let error):
...
single(.success(.failure(commonError)))
****
}
}
return Disposables.create()
}
}