Skip to content

iOS-Yetti/ios-box-office

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

62 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

λ°•μŠ€μ˜€ν”ΌμŠ€πŸŽ¬

μ†Œκ°œ

μ˜ν™”μ§„ν₯μœ„μ›νšŒμ™€ Daum 검색 APIλ₯Ό 톡해 λ‚ μ§œλ³„ λ°•μŠ€μ˜€ν”ΌμŠ€, μ˜ν™” 상세정보λ₯Ό λΆˆλŸ¬μ˜€λŠ” μ•±μž…λ‹ˆλ‹€.

ν”„λ‘œμ νŠΈ κΈ°κ°„ : 23/07/24~23/08/18

λͺ©μ°¨

  1. νŒ€μ› μ†Œκ°œ
  2. νƒ€μž„ 라인
  3. μ‹œκ°ν™” ꡬ쑰
  4. μ‹€ν–‰ ν™”λ©΄
  5. 핡심 κ²½ν—˜
  6. νŠΈλŸ¬λΈ” μŠˆνŒ…
  7. 참고 자료
  8. νŒ€ 회고


νŒ€μ› μ†Œκ°œ

Yetti maxhyunm


νƒ€μž„ 라인

λ‚ μ§œ λ‚΄μš©
2023.07.24. json 파일 νŒŒμ‹±μ„ μœ„ν•œ BoxOfficeEntity νƒ€μž… 생성, λ””μ½”λ”© μœ λ‹›ν…ŒμŠ€νŠΈ 진행
2023.07.25. DecodingManagerμ—μ„œ λ””μ½”λ”©λ§Œ 진행할 수 μžˆλ„λ‘ κΈ°λŠ₯ 뢄리
2023.07.26. NetworkingManager, BoxOfficeError, MoviewDetailEntity νƒ€μž… 생성
2023.07.27. extension으둜 μ€‘μ²©νƒ€μž… 뢄리, URLNamespace νƒ€μž… 생성
2023.07.31. ν”„λ‘œμ νŠΈ 진행을 μœ„ν•œ 개인 κ³΅λΆ€μ‹œκ°„
2023.08.01. ν”„λ‘œμ νŠΈ 진행을 μœ„ν•œ 개인 κ³΅λΆ€μ‹œκ°„
2023.08.02. CollectionViewμ„ΈνŒ…, BoxOfficeRankingCell 생성 및 μ…€ ꡬ성 μ„ΈνŒ…, DiffableDataSource μ„ΈνŒ… 및 μ—°κ²°, λž­ν‚Ή 증감뢄 AttributedString 처리, collectionView에 refreshControl μΆ”κ°€
2023.08.07. 리뷰에 λ”°λ₯Έ λ¦¬νŒ©ν† λ§ 진행
2023.08.08. DaumImageEntity 파일 μΆ”κ°€ 및 DAUM_API_KEY μΆ”κ°€, MovieDetailViewController 파일 μΆ”κ°€ 및 view μ „ν™˜ λ©”μ„œλ“œ μΆ”κ°€, MoviewDetailViewController에 MoviewDetail λ„€νŠΈμ›Œν¬ μ—°κ²°
2023.08.09. ν”„λ‘œμ νŠΈ 진행을 μœ„ν•œ 개인 κ³΅λΆ€μ‹œκ°„
2023.08.10. μŠ€νƒλ·°μ˜ textμ„€μ •λ©”μ„œλ“œ κΈ°λŠ₯ 뢄리 및 Namespace 생성, MovieDetailView λ‘œλ”©ν™”λ©΄ μˆ˜μ •
2023.08.11. README μž‘μ„±
2023.08.14. url 호좜 μ‹€νŒ¨μ‹œ completion으둜 μ—λŸ¬μ²˜λ¦¬ μΆ”κ°€, MovieDetailViewControllerμ—μ„œ isDataLoading, isImageLoading 호좜 μœ„μΉ˜ λ³€κ²½
2023.08.15. ν”„λ‘œμ νŠΈ 진행을 μœ„ν•œ 개인 κ³΅λΆ€μ‹œκ°„
2023.08.16. ν™”λ©΄ λͺ¨λ“œ λ³€κ²½ λ²„νŠΌ μΆ”κ°€ BoxOfficeRankingIconCell νƒ€μž… 생성, ν™”λ©΄ λͺ¨λ“œ λ³€κ²½μ‹œ CollectionView의 λ ˆμ΄μ•„μ›ƒ 섀정을 λ°”κΎΈλŠ” κΈ°λŠ₯ κ΅¬ν˜„, ν…μŠ€νŠΈμ— λ‹€μ΄λ‚˜λ―Ή νƒ€μž… 적용 및 auto layout μˆ˜μ •
2023.08.17. APIKey 검증과정 및 μ—λŸ¬μ²˜λ¦¬ μΆ”κ°€, λ‚ μ§œ λ³€κ²½μ‹œ μ»¬λ ‰μ…˜λ·° λ ˆμ΄μ•„μ›ƒ ν‹€μ–΄μ§€λŠ” 였λ₯˜ μˆ˜μ •
2023.08.18. README μž‘μ„±


μ‹œκ°ν™” ꡬ쑰

File Tree

β”œβ”€β”€ BoxOffice
β”‚Β Β  β”œβ”€β”€ Extension
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ DateFormatter+.swift
β”‚Β Β  β”‚Β Β  └── String+.swift
β”‚Β Β  β”œβ”€β”€ Model
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ BoxOfficeEntity.swift
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ MovieDetailEntity.swift
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ DaumImageEntity.swift
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ DecodingManager.swift
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ Error.swift
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ NetworkingManager.swift
β”‚Β Β  β”‚Β Β  └── URLSessionProtocol.swift
β”‚Β Β  β”œβ”€β”€View
β”‚Β Β  β”‚   β”œβ”€β”€ BoxOfficeRankingListCell.swift
β”‚Β Β  β”‚   β”œβ”€β”€ BoxOfficeRankingIconCell.swift
β”‚Β Β  β”‚   └── MovieDetailStackView.swift
β”‚Β Β  β”œβ”€β”€ Controller
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ BoxOfficeViewController.swift
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ CalendarViewController.swift
β”‚Β Β  β”‚Β Β  └── MovieDetailViewController.swift
β”‚Β Β  β”œβ”€β”€ Resource
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ AppDelegate.swift
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ SceneDelegate.swift
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ NetworkConfiguration.swift
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ Assets.xcassets
β”‚Β Β  β”‚Β Β  └── box_office_sample.json
β”‚Β Β  └──Info.plist
β”œβ”€β”€ BoxOffice.xcodeproj
β”œβ”€β”€ BoxOfficeTests
β”‚Β Β  β”œβ”€β”€ BoxOffice.xctestplan
β”‚Β Β  β”œβ”€β”€ BoxOfficeDecodingTests.swift
β”‚Β Β  β”œβ”€β”€ BoxOfficeNetworkingTest.swift
β”‚Β Β  └── TestDouble.swift
└── README.md

UML

λ°•μŠ€μ˜€ν”ΌμŠ€ ν™”λ©΄


μ˜ν™” 상세정보 ν™”λ©΄



μ‹€ν–‰ ν™”λ©΄

μ˜ν™” 상세정보 λ‚ μ§œ λ³€κ²½ ν™”λ©΄ λͺ¨λ“œ λ³€κ²½


핡심 κ²½ν—˜

🌟 xcconfig, info.plistλ₯Ό ν™œμš©ν•œ api key μ„€μ •

ν™˜κ²½ νŒŒμΌμ„ ν™œμš©ν•΄ 원격 μ €μž₯μ†Œμ— κ³΅μœ λ˜μ§€ μ•Šμ•„μ•Ό ν•˜λŠ” key 정보λ₯Ό κ΄€λ¦¬ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

🌟 CodingKeys와 Nested Type Enum을 ν™œμš©ν•œ 쀑첩 json νŒŒμ‹±

Nested Type을 ν™œμš©ν•˜μ—¬ μ—¬λŸ¬ λ‹¨κ³„λ‘œ μ€‘μ²©λœ ν˜•νƒœμ˜ json을 νŒŒμ‹±ν•  수 μžˆλ„λ‘ ν•˜μ˜€κ³ , CodingKeysλ₯Ό ν™œμš©ν•΄ μ΄ν•΄ν•˜κΈ° μ–΄λ €μš΄ νŒŒλΌλ―Έν„°λͺ…을 λ³€κ²½ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μƒμ„Έμ½”λ“œ
extension BoxOfficeEntity {
    struct BoxOfficeResult: Decodable {
        let boxOfficeType, showRange: String
        let dailyBoxOfficeList: [DailyBoxOffice]

        enum CodingKeys: String, CodingKey {
            case boxOfficeType = "boxofficeType"
            case showRange, dailyBoxOfficeList
        }
    }
}

🌟 Generic을 ν™œμš©ν•œ λ²”μš© λ©”μ„œλ“œ κ΅¬ν˜„

λ‹€μ–‘ν•œ νƒ€μž…μ˜ Entityλ₯Ό λ°˜ν™˜ν•΄μ•Ό ν•˜λŠ” DecodingManager의 λ©”μ„œλ“œλ₯Ό Generic으둜 κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μƒμ„Έμ½”λ“œ
func decode<T: Decodable>(_ data: Data?) throws -> T {
    guard let data = data,
          let decodedData = try? decoder.decode(T.self, from: data) else {
        throw DecodingError.decodingFailure
    }
    return decodedData
}

🌟 Modern Collection View κ΅¬ν˜„

Modern Collection Viewλ₯Ό 톡해 λ°•μŠ€μ˜€ν”ΌμŠ€ λž­ν‚Ή 리슀트λ₯Ό κ΅¬ν˜„ν•˜κΈ° μœ„ν•˜μ—¬ Diffable Data Source와 Collection View List Cellλ₯Ό ν™œμš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μƒμ„Έμ½”λ“œ
private let collectionView: UICollectionView = {
    let configuration = UICollectionLayoutListConfiguration(appearance: .plain)
    let layout = UICollectionViewCompositionalLayout.list(using: configuration)
...
}
    
private var dataSource: UICollectionViewDiffableDataSource<NetworkNamespace, BoxOfficeEntity.BoxOfficeResult.DailyBoxOffice>?
...
    

🌟 UICalendarView ν™œμš©

UICalendarViewλ₯Ό ν™œμš©ν•΄ Calendarλ₯Ό κ΅¬ν˜„ν•˜κ³  κ³Όκ±° λ‚ μ§œμ˜ 데이터λ₯Ό 뢈러올 수 μžˆλ„λ‘ ν™œμš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μƒμ„Έμ½”λ“œ
private let calendarView: UICalendarView = {
        var calendarView = UICalendarView()
        calendarView.translatesAutoresizingMaskIntoConstraints = false
        calendarView.backgroundColor = .systemBackground
        
        let endDate = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()
        let calendarViewDateRange = DateInterval(start: Date(timeIntervalSince1970: 0), end: endDate)
        calendarView.availableDateRange = calendarViewDateRange
        
        return calendarView
    }()

🌟 UIActivityIndicatorViewλ₯Ό ν™œμš©ν•œ λ‘œλ”© κ΅¬ν˜„

데이터 fetch μƒνƒœμ— 따라 UIActivityIndicatorView의 μƒνƒœκ°’μ„ λ³€κ²½ν•˜μ—¬ λ‘œλ”© λ§ˆν¬κ°€ ν™œμ„±ν™”/λΉ„ν™œμ„±ν™” λ˜λ„λ‘ κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μƒμ„Έμ½”λ“œ
private let indicatorView: UIActivityIndicatorView = {
    let indicatorView = UIActivityIndicatorView()
    indicatorView.style = .large
    indicatorView.translatesAutoresizingMaskIntoConstraints = false

    return indicatorView
}()

private var isLoading: Bool = true {
    willSet(newValue) {
        if newValue == true {
            indicatorView.isHidden = false
            indicatorView.startAnimating()
        } else {
            indicatorView.isHidden = true
            indicatorView.stopAnimating()
        }
    }
}

🌟 UIRefreshControlλ₯Ό ν™œμš©ν•œ μƒˆλ‘œκ³ μΉ¨ κ΅¬ν˜„

Collection View에 UIRefreshColtrol 객체λ₯Ό μΆ”κ°€ν•˜μ—¬, μ•„λž˜λ‘œ 당겼을 λ•Œ μƒˆλ‘œκ³ μΉ¨μ„ 진행할 수 μžˆλ„λ‘ ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μƒμ„Έμ½”λ“œ
collectionView.refreshControl = refreshControl
    refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)

🌟 UIToolBar ν™œμš©

UIToolBar와 flexibleSpaceλ₯Ό ν™œμš©ν•˜μ—¬ ν™”λ©΄ ν•˜λ‹¨ λ²„νŠΌμ„ κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μƒμ„Έμ½”λ“œ
let modeChangeButton = UIBarButtonItem(title: "ν™”λ©΄ λͺ¨λ“œ λ³€κ²½", style: .plain, target: self, action: #selector(hitChangeModeButton))
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
...
self.toolbarItems = [flexibleSpace, modeChangeButton, flexibleSpace]

🌟 UIAlertController ν™œμš©

ν™”λ©΄ λͺ¨λ“œ λ³€κ²½μ‹œ UIAlertController의 actionSheetμŠ€νƒ€μΌλ‘œ μ•„μ΄μ½˜ λͺ¨λ“œμ™€ 리슀트 λͺ¨λ“œ 화면을 μ„ νƒμ μœΌλ‘œ μ μš©ν•  수 μžˆλ„λ‘ κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μƒμ„Έμ½”λ“œ
@objc func hitChangeModeButton() {
    let mode: String = isListMode == true ? "μ•„μ΄μ½˜" : "리슀트"
    let alert = UIAlertController(title: "ν™”λ©΄ λͺ¨λ“œ λ³€κ²½", message: nil, preferredStyle: .actionSheet)
    let modeChangeAction = UIAlertAction(title: mode, style: .default) { _ in
        self.isListMode.toggle()
    }
    let cancelAction = UIAlertAction(title: "μ·¨μ†Œ", style: .cancel)
        
    alert.addAction(modeChangeAction)
    alert.addAction(cancelAction)
    present(alert, animated: true)
}

🌟 Attributed String ν™œμš©

ν•˜λ‚˜μ˜ λ ˆμ΄λΈ” μ•ˆμ—μ„œ μ—¬λŸ¬ 가지 색상을 ν‘œμ‹œν•˜κΈ° μœ„ν•˜μ—¬ Attributed String을 ν™œμš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μƒμ„Έμ½”λ“œ
attributedString.addAttribute(.foregroundColor, value: UIColor.systemRed, range: (fixedIntensity as NSString).range(of: "β–²"))

🌟 DynamicType 적용

preferredFontλ₯Ό ν™œμš©ν•˜μ—¬ ν°νŠΈμ— DynamicType을 μ μš©ν•˜μ˜€κ³ , adjustsFontSizeToFitWidth 섀정을 톡해 κ°€λ‘œ λ„ˆλΉ„μ— 맞좰 ν…μŠ€νŠΈ 크기λ₯Ό μžλ™ μ‘°μ ˆν•  수 μžˆλ„λ‘ κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μƒμ„Έμ½”λ“œ
private let audienceLabel: UILabel = {
    let label = UILabel()
    label.font = .preferredFont(forTextStyle: .body)
    label.translatesAutoresizingMaskIntoConstraints = false
    label.adjustsFontForContentSizeCategory = true
    label.adjustsFontSizeToFitWidth = true

    return label
}()


νŠΈλŸ¬λΈ” μŠˆνŒ…

1️⃣ λ„€νŠΈμ›Œν¬ μ—°κ²°μ‹œ URL 전달 방식

🚨 문제점
κΈ°μ‘΄μ—λŠ” dataTask λ©”μ„œλ“œμ— URL νƒ€μž…μ„ μ „λ‹¬ν•˜μ—¬ λ„€νŠΈμ›Œν¬ 연결을 μ§„ν–‰ν•˜λ„λ‘ κ΅¬ν˜„ν–ˆμ—ˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ μ΄λ²ˆμ— Daum APIκ°€ μΆ”κ°€λ˜λ©΄μ„œ Authorization 헀더λ₯Ό μΆ”κ°€ν•΄μ•Ό ν–ˆκΈ° λ•Œλ¬Έμ— ν•΄λ‹Ή 정보λ₯Ό μ–΄λ–»κ²Œ 전달할지 κ³ λ―Όν•˜μ˜€μŠ΅λ‹ˆλ‹€.

πŸ’‘ ν•΄κ²° 방법
URLRequestλ₯Ό ν™œμš©ν•˜λ©΄ setValueλ₯Ό 톡해 헀더 정보λ₯Ό μΆ”κ°€ν•  수 μžˆμŒμ„ ν™•μΈν•˜μ˜€μŠ΅λ‹ˆλ‹€. 이에 따라 κΈ°μ‘΄ ν…ŒμŠ€νŠΈλ”λΈ” 및 ν…ŒμŠ€νŠΈμ½”λ“œ λͺ¨λ‘ URLRequest에 맞게 μˆ˜μ •μ„ μ§„ν–‰ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

var request = URLRequest(url: url)
request.setValue("KakaoAK \(NetworkNamespace.daumApiKey)", forHTTPHeaderField: "Authorization")

λ˜ν•œ Daum API에 전달할 쿼리 λ‚΄μš©μ—λŠ” 띄어쓰기가 μΆ”κ°€λ˜μ–΄ μžˆμ–΄, URL에 λΆ™μ—¬μ„œ λ³΄λ‚΄κΈ°λ³΄λ‹€λŠ” URLComponentsλ₯Ό μ‚¬μš©ν•΄ ν•„μš”ν•œ 쿼리 μ•„μ΄ν…œμ„ μΆ”κ°€ν•˜λŠ” 게 μ’‹κ² λ‹€λŠ” 생각이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€. ν•΄λ‹Ή λ‚΄μš©μ€ μ•„λž˜μ™€ 같이 κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

var urlComponents = URLComponents(string: NetworkNamespace.daumImage.url)
urlComponents?.queryItems = [URLQueryItem(name: "query", value: "\(movieName) μ˜ν™” ν¬μŠ€ν„°")]

2️⃣ dataTask λ©”μ„œλ“œλ‘œ λ°›μ•„μ˜¨ 데이터 처리

🚨 문제점
NetworkingManager의 load() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•œ μœ„μΉ˜μ—μ„œ dataTaskλ₯Ό 톡해 λ°›μ•„ 온 데이터λ₯Ό ν™œμš©ν•  수 μžˆλŠ” 방법에 λŒ€ν•΄ 고민이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. μ²˜μŒμ—λŠ” 두 νƒ€μž…μ„ 델리게이트둜 μ—°κ²°ν•˜μ—¬ μ „λ‹¬ν•˜λŠ” λ“±μ˜ 방법을 μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ 데이터 처리λ₯Ό μœ„ν•΄ λ„€νŠΈμ›Œν‚Ήμ„ μ‚¬μš©ν•˜λŠ” λͺ¨λ“  νƒ€μž…μ„ 델리게이트둜 μ—°κ²°ν•˜λŠ” 것은 ꢌμž₯λ˜λŠ” 방식도 μ•„λ‹ˆκ³ , νš¨μœ¨μ μ΄μ§€ λͺ»ν•œ 것 κ°™μ•˜μŠ΅λ‹ˆλ‹€.

πŸ’‘ ν•΄κ²° 방법
@escaping ν΄λ‘œμ €μ™€ Resultνƒ€μž…μ„ ν™œμš©ν•˜μ—¬ ν•΄κ²°ν•˜μ˜€μŠ΅λ‹ˆλ‹€. ResultλŠ” 성곡/μ‹€νŒ¨ 두 가지 κ°€λŠ₯성에 λŒ€ν•œ λ°μ΄ν„°νƒ€μž…μ„ λ”°λ‘œ 지정해쀄 수 μžˆμ–΄ CompletionHandler에 ν™œμš©ν•˜κΈ°μ— μ μ ˆν•˜λ‹€κ³  νŒλ‹¨ν–ˆμŠ΅λ‹ˆλ‹€.

func load(_ urlString: String, completion: @escaping (Result<Data, BoxOfficeError>) -> Void) {
    guard let url = URL(string: urlString) else {
        return
    }

    let task = session.dataTask(with: url) { data, response, error in
        if error != nil {
            completion(.failure(BoxOfficeError.connectionFailure))
            return
        }
        ...

3️⃣ ATSλ₯Ό ν†΅ν•œ λ„€νŠΈμ›Œν¬ μ„€μ •

🚨 문제점
APIλ₯Ό 받아와야 ν•˜λŠ” 도메인이 httpsκ°€ μ•„λ‹Œ httpλ₯Ό ν™œμš©ν•˜κ³  μžˆμ–΄ λ„€νŠΈμ›Œν¬ μ—°κ²°μ‹œμ— 였λ₯˜κ°€ λ°œμƒν•˜μ˜€μŠ΅λ‹ˆλ‹€.

πŸ’‘ ν•΄κ²° 방법
ν•΄λ‹Ή 도메인 및 ν•˜μœ„ 도메인 정보λ₯Ό ATS에 Exception Domains둜 μΆ”κ°€ν•˜μ—¬ μ •μƒμ μœΌλ‘œ λ„€νŠΈμ›Œν‚Ήμ΄ κ°€λŠ₯ν•˜λ„λ‘ κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

4️⃣ λ‘œλ”©κ³Ό μƒˆλ‘œκ³ μΉ¨ μ’…λ£Œ μœ„μΉ˜ μ„€μ •

🚨 문제점
λ„€νŠΈμ›Œν‚Ήμ„ 톡해 λ°›μ•„ 온 데이터λ₯Ό μ²˜λ¦¬ν•˜λŠ” κ³Όμ •μ—μ„œ μ„±κ³΅ν•œ κ²½μš°μ—λ§Œ λ‘œλ”©μ„ 끝내고 λΉ μ Έλ‚˜κ°ˆ 수 μžˆλ„λ‘ μ²˜λ¦¬ν•˜μ˜€λ”λ‹ˆ, μ—λŸ¬κ°€ 났을 λ•ŒλŠ” μ•„λž˜μ™€ 같이 계속 λ‘œλ”©μ΄ λŒμ•„κ°€κ³  μžˆλŠ” κ²ƒμ²˜λŸΌ λ³΄μ΄λŠ” 것을 ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.

λ˜ν•œ RefreshControl의 μ’…λ£Œ 처리λ₯Ό μ•„λž˜μ™€ 같이 μ§„ν–‰ν•˜λ‹ˆ, 데이터 λ‘œλ”©μ΄ μ™„λ£Œλ˜κΈ° 전에 λΉ„λ™κΈ°λ‘œ λ‹€μŒ 호좜이 μ§„ν–‰λ˜μ–΄ μƒˆλ‘œκ³ μΉ¨μ΄ λ°”λ‘œ λλ‚˜λ²„λ¦¬λŠ” 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

@objc private func refresh() {
    passFetchedData()
    refreshControl.endRefreshing()
}

πŸ’‘ ν•΄κ²° 방법
λ„€νŠΈμ›Œν‚Ήκ³Ό λ””μ½”λ”© 여뢀에 상관없이 λ‘œλ”©κ³Ό μƒˆλ‘œκ³ μΉ¨μ„ μ™„λ£Œν•  수 μžˆλ„λ‘ ν•΄λ‹Ή ν˜ΈμΆœλΆ€λ₯Ό switchλ¬Έ λ°–μœΌλ‘œ μ΄λ™ν•˜μ˜€μœΌλ©°, isLoading λ³€μˆ˜κ°€ false일 λ•Œ endRefreshing도 ν˜ΈμΆœν•  수 μžˆλ„λ‘ λ³€κ²½ν•΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

networkingManager?.load(url) { [weak self] (result: Result<Data, NetworkingError>) in
    switch result {
    case .success(let data):
        ...
    case .failure(let error):
        ...
}

DispatchQueue.main.async {
    self?.isLoading = false
    self?.refreshControl.endRefreshing()
}

5️⃣ μ…€ μž¬ν™œμš©μœΌλ‘œ μΈν•œ 문제 ν•΄κ²°

🚨 문제점
Collection Viewμ—μ„œ 셀을 μž¬μ‚¬μš©ν•˜λ©΄μ„œ, 검은 μƒ‰μœΌλ‘œ λ“€μ–΄κ°€μ•Ό ν•˜λŠ” μˆœμœ„ ν…μŠ€νŠΈκ°€ λΉ¨κ°„μƒ‰μœΌλ‘œ 잘λͺ» λ“€μ–΄κ°€λŠ” λ¬Έμ œκ°€ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
πŸ’‘ ν•΄κ²° 방법
PrepareForReuse()λ₯Ό 톡해 μ•„λž˜μ™€ 같이 폰트 색상을 μ΄ˆκΈ°ν™”ν•΄ μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

override func prepareForReuse() {
    rankIntensityLabel.textColor = .black
}

6️⃣ Test Double 생성

🚨 문제점
인터넷 연결이 μ—†λŠ” μƒνƒœμ—μ„œ λ„€νŠΈμ›Œν¬ 톡신을 ν…ŒμŠ€νŠΈν•˜κΈ° μœ„ν•΄ Test Double을 μƒμ„±ν•˜μ˜€μŠ΅λ‹ˆλ‹€. 이 κ³Όμ •μ—μ„œ ν…ŒμŠ€νŠΈμš© Stub Sessionκ³Ό μ‹€μ œ Session 사이에 ν˜Έν™˜μ΄ κ°€λŠ₯ν•˜λ„λ‘ ν•˜κΈ° μœ„ν•΄ URLSessionProtocol을 κ΅¬ν˜„ν•˜μ˜€λŠ”λ°, URLSessionμ—μ„œ 이λ₯Ό μƒμ†ν•˜λ € ν•˜λ‹ˆ μ•„λž˜μ™€ 같은 κ²½κ³ κ°€ λ°œμƒν•˜μ˜€μŠ΅λ‹ˆλ‹€.

πŸ’‘ ν•΄κ²° 방법
CompletionHandler typealias에 @Sendable을 μ±„νƒν•˜μ—¬ ν•΄κ²°ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

typealias CompletionHandler = @Sendable (Data?, URLResponse?, Error?) -> Void

7️⃣ λ‹€μ–‘ν•œ λ„€νŠΈμ›Œν¬μ— λŒ€ν•œ Test ν™˜κ²½ ꡬ성

🚨 문제점
μ„Έ 가지 λ‹€λ₯Έ APIλ₯Ό ν˜ΈμΆœν•˜λŠ” ν”„λ‘œκ·Έλž¨μ΄λ‹€ λ³΄λ‹ˆ, 각 API λ‚΄μš©μ— 따라 맀번 μƒˆλ‘œμš΄ ν…ŒμŠ€νŠΈ ν™˜κ²½μ„ λ§Œλ“€μ–΄μ£Όμ–΄μ•Ό ν•΄μ„œ λ²ˆκ±°λ‘œμ› μŠ΅λ‹ˆλ‹€.

πŸ’‘ ν•΄κ²° 방법
μ•„λž˜μ™€ 같이 ν…ŒμŠ€νŠΈ νƒ€μž…μ„ νŒŒλΌλ―Έν„°λ‘œ 전달받아 각 νƒ€μž…μ— λ§žλŠ” ν…ŒμŠ€νŠΈν™˜κ²½μ„ ꡬ성할 수 μžˆλ„λ‘ κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

func setUpSUT(isSuccess: Bool, apiType: NetworkNamespace) {
    ...
        switch apiType {
    case .boxOffice:
        urlString = String(format: NetworkNamespace.boxOffice.url, NetworkNamespace.apiKey, "20230801")
        asset = "box_office_sample"
    case .movieDetail:
        urlString = String(format: NetworkNamespace.movieDetail.url, NetworkNamespace.apiKey, "20230801")
        asset = "movie_detail_sample"
    case .daumImage:
        urlString = String(format: NetworkNamespace.daumImage.url)
        asset = "daum_image_sample"
    }
    ...

    if isSuccess {
        ...
        let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
        ...
    } else {
        let response = HTTPURLResponse(url: url, statusCode: 400, httpVersion: nil, headerFields: nil)
        ...
    }
}


참고 자료



νŒ€ 회고

일일 슀크럼


About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Swift 100.0%