![](https://private-user-images.githubusercontent.com/114223423/329217548-0ce875af-7330-40a9-878e-7443d1578769.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjI3MTgyNDksIm5iZiI6MTcyMjcxNzk0OSwicGF0aCI6Ii8xMTQyMjM0MjMvMzI5MjE3NTQ4LTBjZTg3NWFmLTczMzAtNDBhOS04NzhlLTc0NDNkMTU3ODc2OS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwODAzJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDgwM1QyMDQ1NDlaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT00MGY1YjdhMjFhZjYwODBlMjk2YjZhYmY4NWE0ZDliZmYxYmIyZWZhNmQ1NDkyMTM2OTljOWUwMmE2ZmIwYzAyJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.pGA6dUCmFviJgqkU53e5hnZXLyDEqWV60G9zcqLXJps)
현재 기온에 맞는 옷 스타일을 공유하고 아이템을 구매할 수 있는 앱
(서버 개발자 협업)
24.04.12 ~ 24.05.05 (3주)
업데이트 진행 중
iOS 16.0
![](https://private-user-images.githubusercontent.com/114223423/334732326-7ad13fcd-ca48-4eae-b278-da9688852b8f.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjI3MTgyNDksIm5iZiI6MTcyMjcxNzk0OSwicGF0aCI6Ii8xMTQyMjM0MjMvMzM0NzMyMzI2LTdhZDEzZmNkLWNhNDgtNGVhZS1iMjc4LWRhOTY4ODg1MmI4Zi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwODAzJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDgwM1QyMDQ1NDlaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT04MDk3OGNiMDFlZjJlMmZhMWJjZjEwMTA1OTc1OTY4ZWI5MjRkZWNjZjdlM2E3NmYzNDc4OTdlN2Y2N2MyMjJlJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.5tuLrc7KyZyYpxT1nw4IfbhAF2Wd7J4l1iI884-5edE)
- 회원가입 / 로그인
- 현재 기온에 맞는 팔로잉 / 전체 사용자의 스타일 피드를 추천
- 나의 스타일을 해당 날의 기온과 함께 공유
- 마음에 드는 사용자 팔로우
- 코디 스타일 피드 기반 검색 / 댓글 / 좋아요 / 구매 기능 구현
UIKit
MVVM-C
DI
Router
CoreLocation
UserDefaults
WeatherKit
Codable
CodeBaseUI
CompositionalLayout
DiffableDataSource
SPM
Alamofire
URLRequestConvertible
interceptor
EventMonitor
RxSwift
SnapKit
Toast
IQKeyboardManager
Kingfisher
Tabman
네비게이션 로직을 분리하여 코드의 모듈화와 유지보수성을 높이기 위해 Coordinator 패턴 적용
반응형 프로그래밍을 하기 위해 RxSwif 사용
객체 간의 결합도를 낮추고 유연성을 높이기 위해 Dependency Injection 구현
MVVM Input-Output 패턴을 사용한 비즈니스 로직 분리로 데이터의 흐름 명확화
Coordinator 패턴으로 네비게이션 로직을 분리하여 단일 책임 원칙 준수
Dependency Injection을 통해 Testable 한 코드 작성, 각 컴포넌트 간의 의존성 감소
final 키워드와 접근제어자를 사용하여 컴파일 최적화
protocol 생성 시 AnyObject 채택을 통해 해당 protocol을 채택할 수 있는 객체의 타입을 제한하여 메모리 누수 방지
enum NameSpace를 통해 literal 값을 캡슐화하여 유지보수에 용이한 코드 구현
weak self 키워드를 사용하여 메모리 누수 방지
statusCode 관리로 에러 상황 별 다른 에러 처리
propertyWrapper를 통한 UserDefaults 관리
URLRequestConvertible protocol을 채택한 Router를 구현하여 API 요청 로직을 캡슐화
Alamofire interceptor를 통해 토큰 갱신 자동화
PG 연동을 통한 상품 카드결제 지원 및 영수증 검증 로직 구현
커서 기반 페이지네이션을 사용하여 안정적인 데이터 페이징
BaseView를 통해 일관된 ViewController 구조 형성
Compositional Layout을 통해 Section 별 다양한 Cell을 구성
DiffableDataSource를 사용하여 snapshot을 관리하고 이를 통해 효율적인 뷰 구성
1️⃣ 이미지로 인한 메모리 부족으로 앱이 강제 종료되는 문제
width를 받아 비율로 이미지를 resize하는 메서드를 구현하고, kingfisher로 setImage를 할 때, resize 메서드를 호출하여 메모리 문제를 해결
extension UIImage {
func resize(newWidth: CGFloat) -> UIImage {
let scale = newWidth / self.size.width
let newHeight = self.size.height * scale
let size = CGSize(width: newWidth, height: newHeight)
let render = UIGraphicsImageRenderer(size: size)
let renderImage = render.image { context in
self.draw(in: CGRect(origin: .zero, size: size))
}
return renderImage
}
}
extension UIImageView {
func loadImage(from path: String, placeHolderImage: UIImage? = nil) {
guard let url = URL(string: BaseURL.baseURL.rawValue + BaseURL.version.rawValue + "/" + path) else { return }
let modifier = AnyModifier { request in
var request = request
request.setValue(UserDefaultsManager.accessToken, forHTTPHeaderField: HTTPHeader.authorization.rawValue)
request.setValue(APIKey.key.rawValue, forHTTPHeaderField: HTTPHeader.sesacKey.rawValue)
return request
}
self.kf.setImage(with: url, placeholder: placeHolderImage, options: [.requestModifier(modifier)]) { result in
switch result {
case .success(let imageResult):
let resizedImage = imageResult.image.resize(newWidth: 150)
self.image = resizedImage
self.isHidden = false
case .failure(_):
self.image = UIImage.emptyProfile
self.isHidden = false
}
}
}
}
![](https://private-user-images.githubusercontent.com/114223423/335600054-85318ae4-510b-4e58-99b1-3561ff3aaa30.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjI3MTgyNDksIm5iZiI6MTcyMjcxNzk0OSwicGF0aCI6Ii8xMTQyMjM0MjMvMzM1NjAwMDU0LTg1MzE4YWU0LTUxMGItNGU1OC05OWIxLTM1NjFmZjNhYWEzMC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwODAzJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDgwM1QyMDQ1NDlaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1iZTMwYjFiM2U0ZjRiOTUxMDU5NWFhOWYxNjA5ZjAxMjkxOWI4MWNhYjZjNDA1YjQ1NDA2YTBiYmUxYzFiMTNmJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.mEfE7WtL_p1YNsWb6JXG9w5bITIB0XvOq3uVdCnRGT4)
2️⃣ EventMonitor를 사용한 디버깅
EventMonitor protocol을 사용하여 네트워크 통신 상태를 확인 (요청 URL, Method, Body, Header, 상태코드 등) → 문제점을 빠르게 파악하고 해결
final class APIMonitor: EventMonitor {
static let shared = APIMonitor()
private init() { }
// 요청 시작
func requestDidResume(_ request: Request) {
guard let request = request.request?.urlRequest else { return }
var body: String = "body 없음"
if let httpBody = request.httpBody {
body = toPrettyJsonString(data: httpBody)
}
let message = """
✅ 요청시작
[📍요청 URL]
\(request.url?.absoluteString ?? "URL 확인 불가")
[📍요청 메서드]
\(request.method?.rawValue ?? "HTTP 메서드 확인 불가")
[📍요청 헤더]
\(request.headers.dictionary.description)
[📍요청 바디]
\(body)
---
"""
print(message)
}
...
final class API {
static let session: Session = {
let configuration = URLSessionConfiguration.af.default
let apiLogger = APIMonitor.shared
return Session(configuration: configuration, eventMonitors: [apiLogger])
}()
}