곡곡λ°μ΄ν° κΈ°λ° λ¬Έννμ¬ μ 보λ₯Ό ν°μΌ νμμΌλ‘ μμΉ΄μ΄λΉνκ³ , μ¬μ©μ μ·¨ν₯μ λ§λ λ¬Έννμ¬λ₯Ό μΆμ²νλ iOS μ±
CulLectingμ κ΄λν λ¬Έννμ¬λ₯Ό ν°μΌ νμμΌλ‘ μμ§νκ³ , μ·¨ν₯ λΆμμ ν΅ν΄ κ°μΈ λ§μΆ€ν λ¬Έννμ¬λ₯Ό μΆμ²νλ iOS μ΄ν리μΌμ΄μ μ λλ€.
- πΈ ν°μΌ μμΉ΄μ΄λΉ: κ΄λν λ¬Έννμ¬λ₯Ό ν°μΌ νμμΌλ‘ μ μ₯
- π μ·¨ν₯ μΉ΄λ: ν°μΌ μμ§ μ μ·¨ν₯ λΆμμ ν΅ν 'μ·¨ν₯ μΉ΄λ' μμ±
- π λ§μΆ€ν μΆμ²: μ¬μ©μ μ νΈλ κΈ°λ° λ¬Έννμ¬ μΆμ²
- π νν° κ²μ: μ§μ, μΉ΄ν κ³ λ¦¬, μ°λ Ή, λΉμ© κΈ°λ° κ²μ
- π€ μ¬μ©μ κ΄λ¦¬: μ΄λ©μΌ μΈμ¦ κΈ°λ° νμκ°μ λ° λ‘κ·ΈμΈ
| ν, κ²μ | ||
|---|---|---|
![]() |
![]() |
![]() |
| μμΉ΄μ΄λΉ | ||
|---|---|---|
![]() |
![]() |
![]() |
| νλ μ΄ν | ||
|---|---|---|
![]() |
![]() |
![]() |
- μμΈ μ΄λ¦°λ°μ΄ν°κ΄μ₯ 곡λͺ¨μ μΆν
- App Store μ μ λ°°ν¬
- Clean Architecture (Data-Domain-Presentation 3κ³μΈ΅)
- MVVM-C (Model-View-ViewModel-Coordinator)
- Coordinator Pattern (νλ©΄ μ ν λ‘μ§ μ€μ κ΄λ¦¬)
- Repository Pattern (λ°μ΄ν° μμ€ μΆμν)
- Language: Swift
- UI: UIKit, FlexLayout, PinLayout, SnapKit
- Reactive: RxSwift, RxCocoa
- DI: Swinject
- Network: Alamofire
- Database: CoreData
- Security: Keychain Services
- Image: Kingfisher
- Dependency Manager: Swift Package Manager
βββββββββββββββββββββββββββββββββββββββββββ
β Presentation Layer β
β (ViewController, ViewModel, View) β
ββββββββββββββββ¬βββββββββββββββββββββββββββ
β depends on
ββββββββββββββββΌβββββββββββββββββββββββββββ
β Domain Layer β
β (Entity, UseCase, Repository Protocol) β
ββββββββββββββββ¬βββββββββββββββββββββββββββ
β implements
ββββββββββββββββΌβββββββββββββββββββββββββββ
β Data Layer β
β (Repository, API Client, DTO) β
βββββββββββββββββββββββββββββββββββββββββββ
- Domain Layerμ Repository Protocolμ Data Layerκ° κ΅¬ν
- ViewModel β UseCase β Repository Protocol μμΌλ‘ μμ‘΄
- λλ©μΈ λ‘μ§μ λ λ¦½μ± ν보
λ¬Έμ μΈμ
λ°±μλ κ°λ°μμ νμ νλ©΄μ λ€μν API μλν¬μΈνΈκ° μΆκ°λκ³ , μ΄μ λ°λΌ Network κ΄λ ¨ νμΌ, API μ μ, DTO λ±μ΄ κΈκ²©ν μ¦κ°νμ΅λλ€. λ¨μν ViewControllerλ§ λΉλν΄μ§λ κ²μ΄ μλλΌ, ViewModelμλ λ€νΈμν¬ λ‘μ§, λ°μ΄ν° λ³ν λ‘μ§, λΉμ¦λμ€ λ‘μ§μ΄ νΌμ¬λμ΄ μν μ΄ λΆλͺ νν΄μ§κ³ μμμ΅λλ€.
λν μλ‘μ΄ νλ©΄μ μΆκ°ν λλ§λ€ "μ΄ λ‘μ§μ ViewModelμ λ¬μΌ νλ, λ³λ Managerμ λ¬μΌ νλ?"μ κ°μ κ³ λ―Όμ΄ λ°λ³΅λμκ³ , νμ κ° μ½λ μμ± λ°©μμ΄ ν΅μΌλμ§ μμ μΌκ΄μ±μ΄ λ¨μ΄μ§λ λ¬Έμ κ° λ°μνμ΅λλ€. νλ‘μ νΈ μ΄κΈ°μ λͺ νν μν€ν μ² κ·μΉμ μ 립νμ§ μμΌλ©΄, νλ‘μ νΈκ° 컀μ§μλ‘ μ μ§λ³΄μκ° μ΄λ €μμ§ κ²μ΄λΌ νλ¨νμ΅λλ€.
ν΄κ²° λ°©μ
Clean Architectureλ₯Ό λμ νμ¬ Data-Domain-Presentation 3κ³μΈ΅μΌλ‘ λͺ νν λΆλ¦¬νμ΅λλ€.
- Data Layer: API ν΅μ , DTO μ μ, Repository ꡬν체λ₯Ό λ΄λΉ. λ€νΈμν¬ κ΄λ ¨ λ‘μ§μ λͺ¨λ μ΄ κ³μΈ΅μμ μ²λ¦¬
- Domain Layer: λΉμ¦λμ€ λ‘μ§μ UseCaseλ‘ μΆμννκ³ , Repository Protocolλ‘ λ°μ΄ν° μμ€λ₯Ό μΆμν. μΈλΆ μμ‘΄μ± μμ΄ μμν λλ©μΈ λ‘μ§λ§ ν¬ν¨
- Presentation Layer: ViewModelμ UseCaseλ§ μμ‘΄νκ³ , UI μν κ΄λ¦¬μλ§ μ§μ€. Viewλ ViewModelμ ν΅ν΄μλ§ λ°μ΄ν°μ μ κ·Ό
μ΄λ₯Ό ν΅ν΄ "μ΄λμ μ΄λ€ λ‘μ§μ λ¬μΌ νλκ°"μ λν λͺ νν κΈ°μ€μ΄ μκ²Όκ³ , μλ‘μ΄ κΈ°λ₯ μΆκ° μ μΌκ΄λ ꡬ쑰λ₯Ό μ μ§ν μ μμμ΅λλ€. λν UseCase λ¨μλ‘ λΉμ¦λμ€ λ‘μ§μ λΆλ¦¬νμ¬ ν μ€νΈ κ°λ₯ν μ½λλ₯Ό μμ±ν μ μμμ΅λλ€.
κ²°κ³Ό
- ViewModelμ μν μ UI μν κ΄λ¦¬λ‘ μ ννμ¬ λΉλν λ°©μ§
- μλ‘μ΄ API μΆκ° μ Data Layerλ§ μμ νλ©΄ λμ΄ λ³κ²½ μν₯ λ²μ μ΅μν
- νμ κ° μ½λ μμ± λ°©μ ν΅μΌλ‘ μ½λ 리뷰 λ° νμ ν¨μ¨μ± ν₯μ
λ¬Έμ μΈμ
AccessTokenμ 보μμ μν΄ μ§§μ λ§λ£ μκ°(μ: 1μκ°)μ κ°μ§λλ°, λ§λ£λ λλ§λ€ μ¬μ©μκ° λ€μ λ‘κ·ΈμΈν΄μΌ νλ€λ©΄ μ¬μ©μ κ²½νμ΄ ν¬κ² μ νλ©λλ€. λν μ± μ¬μ© μ€ κ°μκΈ° λ‘κ·ΈμΈ νλ©΄μΌλ‘ νλ κ²μ μ¬μ©μμκ² νΌλμ μ€ μ μμ΅λλ€.
ν΄κ²° λ°©μ
RefreshTokenμ νμ©ν μλ ν ν° κ°±μ λ©μ»€λμ¦μ ꡬννμ΅λλ€.
- 401 μλ¬ κ°μ§: λ€νΈμν¬ μμ² μ 401 Unauthorized μλ¬κ° λ°μνλ©΄ ν ν° λ§λ£λ‘ νλ¨
- μλ κ°±μ : RefreshTokenμ μ¬μ©ν΄ μλ‘μ΄ AccessTokenμ λ°κΈλ°λ API νΈμΆ
- μμ² μ¬μλ: μ ν ν°μ Keychainμ μ μ₯ ν, μ€ν¨νλ μλ μμ²μ μλμΌλ‘ μ¬μλ
- RxSwift catch μ°μ°μ: μλ¬ μ€νΈλ¦Όμ μΊμΉνμ¬ ν ν° κ°±μ λ‘μ§μ μ½μ νκ³ , flatMapμΌλ‘ μλ μμ² μ¬μ€ν
func request<T: Decodable>(endpoint: URLRequestConvertible) -> Single<T> {
return networkManager.request(endpoint)
.catch { error in
if error.isTokenExpired {
return self.refreshToken()
.flatMap { _ in self.networkManager.request(endpoint) }
}
return Single.error(error)
}
}κ²°κ³Ό
- μ¬μ©μλ ν ν° λ§λ£λ₯Ό μΈμ§νμ§ λͺ»νκ³ λκΉ μμ΄ μ± μ¬μ© κ°λ₯
- μλ λ‘κ·ΈμΈ μ μ§λ‘ μ¬μ©μ κ²½ν ν¬κ² κ°μ
- RefreshToken λ§λ£ μμλ§ λ‘κ·ΈμΈ νλ©΄μΌλ‘ μ΄λνλλ‘ μ²λ¦¬
λ¬Έμ μΈμ
μ΄κΈ°μλ ViewControllerμμ λ€μ νλ©΄μ μ§μ pushνλ λ°©μμΌλ‘ κ°λ°νμ΅λλ€. κ·Έλ¬λ νλ‘μ νΈκ° 컀μ§λ©΄μ νλ©΄ μ ν λ‘μ§μ΄ μ¬λ¬ ViewControllerμ λΆμ°λμ΄ μ 체 νλ‘μ°λ₯Ό νμ νκΈ° μ΄λ €μ κ³ , ViewController κ° κ²°ν©λκ° λμ μ μ§λ³΄μκ° μ΄λ €μμ‘μ΅λλ€.
νΉν "λ‘κ·ΈμΈ β μ¨λ³΄λ© β λ©μΈ νλ©΄" κ°μ 볡μ‘ν νλ‘μ°μμ κ° νλ©΄μ΄ λ€μ νλ©΄μ μμμΌ νλ ꡬ쑰λ λ³κ²½μ μ·¨μ½νμ΅λλ€.
ν΄κ²° λ°©μ
Coordinator ν¨ν΄μ λμ νμ¬ λͺ¨λ νλ©΄ μ ν λ‘μ§μ μ€μμμ κ΄λ¦¬νλλ‘ κ°μ νμ΅λλ€.
- FirstCoordinator: μ± μμ μ ν ν° λ° μ¨λ³΄λ© μνλ₯Ό κ²μ¦νκ³ , LoginCoordinator/OnboardingCoordinator/TabbarCoordinatorλ‘ λΆκΈ°
- κΈ°λ₯λ³ Coordinator: LoginCoordinatorλ λ‘κ·ΈμΈ/νμκ°μ /λΉλ°λ²νΈ μ¬μ€μ νλ‘μ°λ₯Ό λ΄λΉ, TabbarCoordinatorλ ν λ€λΉκ²μ΄μ κ΄λ¦¬
- ViewController λ
립μ±: ViewControllerλ Coordinatorμκ² μ΄λ²€νΈλ§ μ λ¬νκ³ (μ:
coordinator.didLoginSuccess()), λ€μ νλ©΄μ λν΄ μμ§ λͺ»ν¨
κ²°κ³Ό
- μ 체 μ± νλ‘μ°λ₯Ό Coordinator μ½λλ§ λ³΄κ³ νμ κ°λ₯
- ViewController κ° κ²°ν©λ μ κ±°λ‘ μ¬μ¬μ©μ± ν₯μ
- νλ©΄ μ ν λ‘μ§ λ³κ²½ μ Coordinatorλ§ μμ νλ©΄ λμ΄ μ μ§λ³΄μ μ©μ΄
λ¬Έμ μΈμ
μμΉ΄μ΄λΉ νλ©΄μμ ν°μΌμ μΉ΄λ ννμ Carouselλ‘ λ³΄μ¬μ£Όλ UIκ° νμνμ΅λλ€. λμμ΄λλ λ€μκ³Ό κ°μ μꡬμ¬νμ μ μνμ΅λλ€:
- 첫 λ²μ§Έ μΉ΄λμ λ§μ§λ§ μΉ΄λλ νλ©΄ μ€μμ μ λ ¬
- μ€ν¬λ‘€ μ μ€μ μΉ΄λλ ν¬κ², μμ μΉ΄λλ μκ² λ³΄μ΄λ 거리 κΈ°λ° transform ν¨κ³Ό
- μ€ν¬λ‘€ μ’ λ£ μ κ°μ₯ κ°κΉμ΄ μΉ΄λλ‘ λΆλλ½κ² μ€λ
UICollectionViewλ‘ κ΅¬νμ κ³ λ €νμΌλ, λ€μκ³Ό κ°μ λ¬Έμ κ° μμμ΅λλ€:
- μ μ¬μ¬μ©μΌλ‘ μΈν΄ νλ©΄ λ° μ μ κ·Όμ΄ μ νμ μ΄μ΄μ μ€μκ° transform ν¨κ³Ό ꡬνμ΄ λ³΅μ‘ν¨
- 첫/λ§μ§λ§ μΉ΄λ μ€μ μ λ ¬μ μν 컀μ€ν λ μ΄μμ ꡬνμ΄ λ²κ±°λ‘μ
ν΄κ²° λ°©μ
UIScrollViewλ₯Ό μ§μ νμ©νμ¬ μ»€μ€ν CarouselViewλ₯Ό ꡬννμ΅λλ€.
1. μ€μ μ λ ¬ λ μ΄μμ
let cardHeight = bounds.height * 0.85
let cardWidth = cardHeight * 0.62
let spacer = (bounds.width - cardWidth) / 2 // μμͺ½ μ¬λ°± κ³μ°μμͺ½μ spacerλ₯Ό μΆκ°νμ¬ μ²«/λ§μ§λ§ μΉ΄λλ νλ©΄ μ€μμ μμΉνλλ‘ κ΅¬ν
2. μ€μκ° κ±°λ¦¬ κΈ°λ° Transform ν¨κ³Ό
private func updateTransforms() {
let centerX = scrollView.contentOffset.x + bounds.width / 2
for cardView in cardViews {
let distance = abs(centerX - cardView.center.x)
let maxDistance = bounds.width / 2 + cardView.bounds.width / 2
let scale = max(0.9, 1 - distance / maxDistance * 0.1)
let alpha = max(0.5, 1 - distance / maxDistance)
cardView.transform = CGAffineTransform(scaleX: scale, y: scale)
cardView.alpha = alpha
}
}scrollViewDidScroll λΈλ¦¬κ²μ΄νΈμμ λͺ¨λ μΉ΄λμ μ€μ 거리λ₯Ό κ³μ°νμ¬ scaleκ³Ό alphaλ₯Ό μ€μκ° μ
λ°μ΄νΈ. UIScrollViewλ λͺ¨λ μΉ΄λμ μ§μ μ κ·Ό κ°λ₯νμ¬ UICollectionViewλ³΄λ€ κ΅¬νμ΄ λ¨μν¨
3. 컀μ€ν μ€λ λ‘μ§
private func snapToNearestCard() {
let totalWidth = cardWidth + cardSpacing
let centerOffset = scrollView.contentOffset.x + bounds.width / 2
let adjustedOffset = centerOffset - spacer
let index = Int(round((adjustedOffset - cardWidth / 2) / totalWidth))
scrollToIndex(index: index, animated: true)
}μ€ν¬λ‘€ μ’ λ£ μ μ€μμ κ°μ₯ κ°κΉμ΄ μΉ΄λ μΈλ±μ€λ₯Ό κ³μ°νμ¬ λΆλλ½κ² μ€λ
UIScrollView μ ν κ·Όκ±°
- μ€μκ° μ μ΄: λͺ¨λ μΉ΄λμ λν μ§μ μ κ·ΌμΌλ‘ 거리 κΈ°λ° transform ν¨κ³Ό ꡬν μ©μ΄
- μ νν λ μ΄μμ: 첫/λ§μ§λ§ μΉ΄λ μ€μ μ λ ¬μ spacer κ³μ°λ§μΌλ‘ κ°λ¨ν ꡬν
- λ¨μν ꡬ쑰: μ λ±λ‘, DataSource/Delegate λΆνμ, ν°μΌ μκ° μ νμ μ΄μ΄μ λ©λͺ¨λ¦¬ ν¨μ¨μ± λ¬Έμ μμ
- 컀μ€ν μ€λ : μ€λ μμΉμ λμμ μ νν μ μ΄ κ°λ₯
κ²°κ³Ό
- λμμ΄λκ° μμ²ν μ€μ μ λ ¬ λ° κ±°λ¦¬ κΈ°λ° transform ν¨κ³Ό μλ²½ ꡬν
- λΆλλ¬μ΄ μ€λ μ λλ©μ΄μ μΌλ‘ μ¬μ©μ κ²½ν ν₯μ
- UICollectionView λλΉ λ¨μν κ΅¬μ‘°λ‘ μ μ§λ³΄μ μ©μ΄
- ν°μΌ κ°μκ° μ μ΄ UIScrollViewλ‘ μΆ©λΆν μ±λ₯ ν보
νκ³ λ° κ°μ λ°©ν₯
λ§μ½ ν°μΌ κ°μκ° μμ κ° μ΄μμΌλ‘ μ¦κ°νλ€λ©΄, UICollectionViewλ‘ λ¦¬ν©ν λ§νμ¬ μ μ¬μ¬μ©μ ν΅ν λ©λͺ¨λ¦¬ μ΅μ νκ° νμν μ μμ΅λλ€.
- iOS Developer: 1λͺ (λ¨λ κ°λ°)
- Designer: 1λͺ
- BE: 1λͺ
- PM: 1λͺ
2025.03 - 2025.05 (μ½ 2κ°μ)








