/OOTD

코디 정보를 공유하는 커뮤니티 앱

Primary LanguageSwift

image

OOTD

코디 정보를 공유하는 커뮤니티 앱



프로젝트 소개

개발 환경

  • 최소 버전 : 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 만료 시 대응
  • RxSwiftSingle Trait를 활용한 네트워크 요청 로직 구현
  • Moya를 활용한 Router Pattern 구성
  • Custom ResultCustom Error 구성을 통한 에러 핸들링
  • Cursor Based Pagination을 통한 데이터 갱신
  • multipart/form-data 를 통한 이미지 및 텍스트 데이터 업로드


트러블 슈팅

1. 네트워크 에러 핸들링 해결법

문제점

서버에서 오는주는 에러에 유연하게 대응하기 위해 프로토콜을 정의하고 이를 채택하는 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()
    }
}


2. RxSwift Single의 failure로 에러 방출 시 스트림 종료 문제

문제점

네트워크 요청 과정에서 성공과 실패에 대해서만 관리할 수 있는 RxSwiftSingle을 활용하였으나 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()
    }
}