ํค๋ณด๋ ์ํ ํนํ ์ปค๋จธ์ค ํ๋ซํผ ์ฑ
- ๋ก๊ทธ์ธ / ํ์๊ฐ์ / ํ๋งค์ ์ฌ์ ๊ถํ ์ธ์ฆ
- ์ํ ํ๋งค๊ธ ์์ฑ / ์กฐํ / ์ญ์ ๊ธฐ๋ฅ
- ์ํ ์ด๋ฏธ์ง ์ ๋ก๋ ๊ธฐ๋ฅ
- PG์ฌ ์ฐ๋ ์ํ ๊ฒฐ์ ๊ธฐ๋ฅ
- ์ํ ๋ฆฌ๋ทฐ ์์ฑ / ์กฐํ / ์ญ์ ๊ธฐ๋ฅ
- ๋ถ๋งํฌ / ์ฅ๋ฐ๊ตฌ๋
- ํ๋กํ ์์ ๊ธฐ๋ฅ
- ํ๋ก์ฐ ๊ธฐ๋ฅ
๊ฐ๋ฐ ์ธ์
iOS/๊ธฐํ/๋์์ธ 1์ธ(๋ณธ์ธ)
Backend 1์ธ
๊ฐ๋ฐ ๊ธฐ๊ฐ
2024.04.10 ~ 2024.05.06 (3.5์ฃผ)
iOS ์ต์ ๋ฒ์
16.0+
Xcode
15.3
-
UIKitSnapKitUIHostingController -
RxSwiftInput&OutputMVVMCoordinatorClean-Architecture -
RxAlamofireRequestInterceptorEventMonitor -
UserDefaultsKingfisherTabManToast
- ๋์์ธ ์์คํ ์ผ๋ก UI ์ผ๊ด์ฑ ์ ์ง
- ๊ณตํต ๋ก์ง ์ฌ์ฌ์ฉ์ ์ํ ํ๋กํ ์ฝ๊ณผ ์์ ํ์ฉ
- Rx ์ต์ ๋ฒ๋ธ์ ์ฐธ์กฐ์ฑ๊ณผ ๋ฐ์ธ๋ฉ์ ํ์ฉํ์ฌ ๋ทฐ ๊ณ์ธต๊ฐ ๋ฐ์ดํฐ ๋๊ธฐํ
- Input&Output ํจํด์ผ๋ก ๋จ๋ฐฉํฅ ๋ฐ์ดํฐ ํ๋ฆ ๊ตฌ์ฑ
- ๋คํธ์ํฌ ๋จ์ ๋ฐ ์๋ฌ ์ํฉ์ ๋ํ ์ฌ์ฉ์ ์๋ด UI ํ์ ์ฒ๋ฆฌ
- ๋๋ฐ์ด์ค ์ฌ์ด์ฆ ๊ธฐ๋ฐ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ์ต์ ํ
-
์ปค๋จธ์ค ํ๋ซํผ ์ํ์ ๋ฐ์ดํฐ ์ถ๊ฐ๊ฐ ๋น๋ฒํ๊ฒ ๋ฐ์ํ๊ณ , ํ์ด์ง ๋ฒํธ ๊ธฐ๋ฐ ํ์์ด ํ์ํ์ง ์๋ค๊ณ ํ๋จํ์ฌ ์ปค์ ๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์ ์ ์ฑํํ์ต๋๋ค.
-
์คํฌ๋กค ๋์์ผ๋ก ์ธํ API ์ค๋ณต ํธ์ถ ํธ๋ฆฌ๊ฑฐ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด, 1์ด์ ์ฐ๋กํ๋ง์ ์ ์ฉํ์์ต๋๋ค.
-
API ์๋ต์์ ๋ฐ์ํ๋ HTTP ์ํ์ฝ๋๋ฅผ ๋๋ฉ์ธ ์๋ฌ๋ก ๋งคํํ๋ ๋ก์ง์ ๊ตฌ์ฑํ์ต๋๋ค.
-
์๋ฌ ๋งคํ ๋ก์ง์ ํ๋กํ ์ฝ์ ๊ณตํต ๊ตฌํํ๊ณ ํ์ํ Repository์ ์ฑํํ์ฌ ์ฌ์ฌ์ฉํ์ต๋๋ค.
URLRequestConvertibleํ๋กํ ์ฝ์ ๊ตฌํํ๋ Router ํ๋กํ ์ฝ์ ์ ์ํด์ API Endpoint๋ฅผ ๊ด๋ฆฌํ์ต๋๋ค.
-
์ก์ธ์ค ํ ํฐ ๋ง๋ฃ ์ ์๋์ผ๋ก ํ ํฐ์ ๊ฐฑ์ ํ๊ณ , ๊ฐฑ์ ๋ ํ ํฐ์ ์ฌ์ฉํ์ฌ ์ด์ ์ ์คํจํ API ์์ฒญ์ ์๋์ผ๋ก ์ฌ์ํํ๋ ๋ก์ง์
Request Interceptor์ ๊ตฌํํ์ต๋๋ค. -
AF Session์ Interceptor๋ฅผ ์ฃผ์ ํด์, ๋ชจ๋ API ์์ฒญ์ ํ ํฐ ๊ด๋ฆฌ ๋ฐ ์ฌ์์ฒญ ๋ก์ง์ด ์๋์ผ๋ก ์ ์ฉ๋ฉ๋๋ค.
์์ค์ฝ๋
final class APIRequestInterceptor: RequestInterceptor {
func adapt(
_ urlRequest: URLRequest,
for session: Session,
completion: @escaping (Result<URLRequest, any Error>) -> Void
) {
/// ๋ก๊ทธ์ธ ์์ฒญ์ Intercept ํ์ง ์๊ณ ๋ฐ๋ก ์คํ
guard
let urlString = urlRequest.url?.absoluteString,
urlString.hasPrefix(APIKey.baseURL),
UserInfoService.hasSignInLog
else {
completion(.success(urlRequest))
return
}
/// ๊ฐฑ์ ๋ ํ ํฐ์ผ๋ก Header ์ฌ์ค์
let urlRequest = urlRequest.applied {
$0.setValue(UserInfoService.accessToken, forHTTPHeaderField: KCHeader.Key.authorization)
}
completion(.success(urlRequest))
}
func retry(
_ request: Request,
for session: Session,
dueTo error: any Error,
completion: @escaping (RetryResult) -> Void
) {
/// ์๋ฌ ์ผ์ด์ค๊ฐ 419๋ฉด ํ ํฐ ๋ฆฌํ๋ ์, ์๋๋ฉด ํจ์ ์ข
๋ฃ
guard
let statusCode = request.response?.statusCode,
statusCode == HTTPStatusError.accessTokenExpired.statusCode
else {
return completion(.doNotRetry)
}
/// ํ ํฐ ๋ฆฌํ๋ ์ ์์ฒญ
AF.request(AuthRouter.tokenRefresh)
.validate()
.responseDecodable(of: RefreshTokenResponse.self) { response in
switch response.result {
/// ์๋ต ํ ํฐ์ผ๋ก ๊ฐฑ์ ํ ๊ธฐ์กด API ์ฌ์์ฒญ
case .success(let tokenResponse):
UserInfoService.renewAccessToken(with: tokenResponse.accessToken)
completion(.retry)
/// ๋ฆฌํ๋ ์ ํ ํฐ์ด ๋ง๋ฃ๋๋ฉด ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ๋์๊ฐ๋๋ก ์๋ฌ ๋ฐฉ์ถ
case .failure:
completion(.doNotRetryWithError(HTTPStatusError.refreshTokenExpired))
}
}
}
}-
API EventMonitor๋ฅผ ๊ตฌํํ์ฌ HTTP ๋ผ์ดํ์ฌ์ดํด์ ๋ํ ๋ก๊น ํ๋ก์ธ์ค๋ฅผ ์ถ๊ฐํ์ต๋๋ค. -
HTTP ์์ฒญ ๋ฐ ์๋ต ๊ณผ์ ์ ๊ตฌ์กฐํ๋ ๋ก๊ทธ ๋ฉ์์ง๋ก ๋ณํํ์ฌ ๋๋ฒ๊น ์ ํ์ฉํ์ต๋๋ค.
- ๊ธฐ๋ฅ์ด ํฌํจ๋ ์ปค์คํ UI ์ปดํฌ๋ํธ๋ฅผ ํ์ฉํด์ ๋ก์ง ์ฌ์ฌ์ฉ์ฑ ๋ฐ UI ์ผ๊ด์ฑ์ ํ๋ณดํ์ต๋๋ค.
- ์ํ ๋ชฉ๋ก์ ์กฐํํ๋ UI์์ ๊ณ ํด์๋์ ์๋ณธ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ฉํจ์ ๋ฐ๋ผ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ์ฆ๊ฐํ๋ ๋ฌธ์ ๋ฅผ ๊ฒฝํํ์ต๋๋ค.
- Kingfisher ๋ผ์ด๋ธ๋ฌ๋ฆฌ์
DownsamplingImageProcessor๊ธฐ๋ฅ์ ํ์ฉํ์ฌ, ๋์คํ๋ ์ด ํด์๋ ๊ธฐ๋ฐ์ผ๋ก ๋ค์ด์ํ๋ง์ ์ ์ฉํ์ฌ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ์ฝ 61.3% ๊ฐ์ ํ์ต๋๋ค.
-
๋คํธ์ํฌ ์ฐ๊ฒฐ ์ํ ๋ณ๋ ์ UI์ ์ํ ์๋ด๋ฅผ ํ์ํ๊ธฐ ์ํ์ฌ,
NWPathMonitor๋ฅผ ํ์ฉํด ๋คํธ์ํฌ ์ํ๋ฅผ ๋ชจ๋ํฐ๋งํ๊ณ ๋ชจ๋ VC๊ฐ ์์๋ฐ๋ Base VC ๋ด์ ์ํ ํ์ ๋ก์ง์ ๊ตฌํํ์์ต๋๋ค. -
addSubview๋ฅผ ์ฌ์ฉํ์ฌ ํ์ฌ ๋ทฐ ๊ณ์ธต์ ๊ฐ์ฅ ํ์์ ์๋ด UI๋ฅผ ์ถ๊ฐํ๋ ค ํ์ผ๋ ์๋์ ๋ฌ๋ฆฌ UI๊ฐ ํ๋ฉด์ ์์ ํ ๊ฐ๋ฆฌ์ง ๋ชปํ๊ณ , Base VC๋ฅผ ์์๋ฐ์ ๋ชจ๋ ๋ทฐ ์ธ์คํด์ค์ ์ค๋ณต์ผ๋ก ์ถ๊ฐ๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์์ต๋๋ค.
UIWindow๋ฅผ ์ด์ฉํ ๊ฐ์
-
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด
UIWindow๋ฅผ ํ์ฉํ๋ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝํ์์ต๋๋ค. -
SceneDelegate์์ ๋คํธ์ํฌ ์ํ ๋ณ๊ฒฝ ์ด๋ฒคํธ๋ฅผ ๊ฐ์งํ์ฌ ๋ฉ์ธ Window๋ณด๋ค ์์ ๋ ๋ฒจ์ธ ErrorWindow๋ฅผ ์ถ๊ฐํ๋ ๋ฐฉ์์ผ๋ก UI๋ฅผ ๊ตฌํํ๊ณ , ๋คํธ์ํฌ ์ํ๊ฐ ์ ์์ผ๋ก ๋์์ค๋ฉด ์๋ ํ๋ฉด์ ์ฌ๊ฐํ๋ ๋ก์ง์ ์ ์ฉํ์ต๋๋ค.
-
Scene์ ์ฐ๊ฒฐ / ๋๊น ์๋ช ์ฃผ๊ธฐ ์ด๋ฒคํธ์ ๋ชจ๋ํฐ๋ง ๋ก์ง์ ์ฐ๋ํ์ฌ, ๋ชจ๋ํฐ๋ง ๋ฆฌ์์ค๊ฐ ํ์๋๋๋ก ์ค์ ํ์ต๋๋ค.
๋ชจ๋ํฐ๋ง ์์ค์ฝ๋
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
...
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
...
startMonitoring(scene: scene)
}
private func startMonitoring(scene: UIScene) {
NetworkMonitor.shared.start { [weak self] path in
guard let self else { return }
switch path.status {
case .satisfied:
setErrorWindowOff(scene: scene)
default:
setErrorWindowOn(scene: scene)
}
}
}
private func setErrorWindowOn(scene: UIScene) {
guard let windowScene = scene as? UIWindowScene else { return }
self.errorWindow = UIWindow(windowScene: windowScene).configured {
$0.windowLevel = .statusBar
$0.addSubview(NetworkUnsatisfiedView(frame: $0.bounds))
$0.makeKeyAndVisible()
}
}
private func setErrorWindowOff(scene: UIScene) {
errorWindow?.resignKey()
errorWindow = nil
}
...
}







