### STEP1
## 고민했던 점
### 커스텀 JsonDecoder객체 생성
현재는 `parse`하는 부분이 한군데만 존재하지만 프로젝트 추후에 여러군데에서 사용할 가능성과, 객체를 테스트할 것을 고려해 커스텀 클래스로 정의했습니다.
```swift
final class BoxOfficeJsonDecoder: JSONDecoder {
func loadJsonData<T: Decodable>(name: String, type: T.Type) throws -> T {
guard let dataAsset = NSDataAsset(name: name) else {
throw DataAssetError.invalidFileName
}
return try self.decode(type, from: dataAsset.data)
}
}
```
### parse한 데이터 unit테스트
정확한 비교를 하기 위해서는 데이터 타입의 인스턴스를 만들어 예상한 데이터들을 넣고 그 인스턴스와 parse한 모든 데이터들이 맞는지 확인하는 방법이 정확하다고 생각합니다만 저희는 일부만 확인해주어도 parse가 됐는지 확인이 된다고 생각합니다. 혹시 태태는 어떻게 생각하시나요?
## STEP2
안녕하세요 태태 @uuu1101
스텝 2-1까지 완료하여서 PR보냅니다.
## 조언받고 싶은 점
### URL 관리방법
네트워크 통신을 위해 사용하는 URL을 상황에 따라 다르게 생성시키기 위해서 다음과 같이 정의했습니다.
```swift
enum BoxOfficeAPI {
case dailyBoxOffice(date: String)
case detailMovieInformation(movieCode: String)
static let key: String = "67e99e70400656a77208ca1775261071"
}
extension BoxOfficeAPI {
var url: URL? {
switch self {
case .dailyBoxOffice(let date):
let path = "boxoffice/searchDailyBoxOfficeList.json?"
return .makeForEndpoint("\(path)key=\(BoxOfficeAPI.key)&targetDt=\(date)")
case .detailMovieInformation(let movieCode):
let path = "movie/searchMovieInfo.json?"
return .makeForEndpoint("\(path)key=\(BoxOfficeAPI.key)&movieCd=\(movieCode)")
}
}
}
private extension URL {
static let baseURL = "http://kobis.or.kr/kobisopenapi/webservice/rest/"
static func makeForEndpoint(_ endpoint: String) -> URL? {
guard let url = URL(string: baseURL + endpoint) else {
return nil
}
return url
}
}
```
하지만 이 방법은 요청하는 url의 열거형타입이 `dailyBoxOffice`인 상태에서 여러가지 쿼리 파라미터를 받고자 한다면 매개변수의 개수만 다른 비슷한 유형의 case를 정의해야 합니다.
```swift
var url: URL? {
switch self {
case .dailyBoxOffice(let date):
let path = "boxoffice/searchDailyBoxOfficeList.json?"
return .makeForEndpoint("\(path)key=\(BoxOfficeAPI.key)&targetDt=\(date)")
case .dailyBoxOffice(let date, let itemPerPage):
let path = "boxoffice/searchDailyBoxOfficeList.json?"
return .makeForEndpoint("\(path)key=\(BoxOfficeAPI.key)&targetDt=\(date)&itemPerpage=\(itemPerPage)")
}
}
```
이 때문에 쿼리파라미터의 종류가 다양한 서비스의 경우에는 보일러 플레이트 코드가 많이 생겨날 것으로 생각했습니다.
혹시 태태는 어떤 방식으로 URL Endpoint를 관리하시는지 궁금합니다.
### mimeType
처음에는 httpResponse의 mimeType을 검사하여 json 형식의 데이터만 받아오도록 하려고 하였습니다. 하지만 데스트를 하기 위해 MockURLSession을 만들었고 HTTPURLResponse를 생성할 때 mimeType이 생성되는 생성자가 아닌 statusCode가 생성되는 생성자로 구현하였고 그로인해 api를 통해 받아오고 있던 mimeType이 존재하지 않아(nil값이 나옴) if let 구문에서 실행이 안되어 테스트가 안되었습니다.
따라서 현재는 mimeType을 검사하는 코드를 삭제하였습니다. 혹시 직접 만든 HTTPURLResponse객체에 mimeType을 추가하는 방법이 있을까요?
혹은 다른 자료들을 찾아봤을 때 mimetype까지 검사하지 않고 에러나 상태코드에 의해서만 검사를 진행했는데 이대로 해도 괜찮을까요?
수정 전
```swift
if let mimeType = httpResponse.mimeType, mimeType == "application/json",
if let data = data {
do {
let jsonData = try JSONDecoder().decode(type, from: data)
completion(.success(jsonData))
} catch {
completion(.failure(NetworkError.failToParse))
}
return
}
```
수정 후
```swift
if let data = data {
do {
let jsonData = try JSONDecoder().decode(type, from: data)
completion(.success(jsonData))
} catch {
completion(.failure(NetworkError.failToParse))
}
return
}
```
## STEP3
안녕하세요 태태!
박스오피스 step3 PR입니다.
여러가지 새로운 API를 적용해보느라 조금 늦었습니다..ㅎㅎ
리뷰 잘 부탁드리겠습니다!
## 고민한 점
### UICollectionViewListCell, ContentConfiguration의 사용
요구사항의 뷰를 보고 테이블 뷰로 구현할 지, 컬렉션 뷰로 구현할 지, CollectionListCell을 이용해서 구현할 지 고민했습니다.
테이블 뷰와 컬렉션 뷰로 충분히 구현이 가능해보였지만 `ContentConfiguration`의 이점으로 상태에 따른 셀의 외형, 외형, 데이터 주입을 분리할 수 있다는 점에서 `CollectionViewListCell`을 이용했습니다.
하지만 평범한 `UICollectionViewListCell`은 `text`, `secondaryText` 등의 기본적인 프로퍼티만 존재했습니다.
이번 프로젝트에서는 랭킹이 어제에 비해 얼만큼 증가 혹은 감소하였는가와 같은 레이블이 필요했고 추후 셀의 레이아웃에 무언가 추가가 되었을 때 기존에 존재하는 `text`같은 요소들은 사용하지 않을 수 있기 때문에(다른 무언가 요소로 대체가 되는 경우가 있을 것 같습니다) `UICollectionViewListCell`를 상속받은 커스텀 ListCell을 만들게 되었습니다.
ListCell을 커스텀하게 만들게 되면 셀에 보여지는 데이터(ContentView)또한 만들어야 합니다. 만약 `ContentView`를 직접 만들지 않는다면 `UICollectionViewListCell`을 상속받은 기본 레이아웃에서 원하는 View를 추가하는 경우만 가능할 것이라고 생각했습니다.
이 때문에 `BoxOfficeListCell`, `BoxOfficeContentView`, `BoxOfficeContentConfiguration` 세 개의 커스텀 타입을 정의했습니다.
`BoxOfficeListCell`에서는 데이터를 주입하고, `BoxOfficeContentConfiguration`에서는 현재 상태에 맞는 구성을 제공해줍니다. 그리고 `BoxOfficeContentView`에서는 화면에 보여지는 셀의 요소들을 보여주는 역할을 합니다.
이와 같이 역할을 분리함으로써 상태에 따른 모든 코드를 같은 객체에 정의하는 것을 피했습니다
### DTO 분리
화면의 보여지는 데이터로는 순위, 순위 변동, 영화 제목, 당일/누적 관객수 정도만 보여지는데 실제 저희가 받아오는 데이터는 이보다 더 많았습니다. 따라서 데이터를 가져올 때 사용하지 않은 데이터들을 없앤 데이터 모델을 따로 만들었습니다.
- 기존 데이터 모델
```swift
struct DailyBoxOffice: Decodable {
let rowNumber: String
let rank: String
let rankIncrement: String
let rankOldAndNew: String
let movieCode: String
let movieName: String
let openDate: String
let salesAmount: String
let salesShare: String
let salesIncrement: String
let salesChange: String
let salesAccumulation: String
let audienceCount: String
let audienceIncrement: String
let audienceChange: String
let audienceAccumulation: String
let screenCount: String
let showCount: String
}
```
- 새로운 데이터 모델
```swift
struct BoxOfficeItem: Identifiable {
let id = UUID()
let rank: String
let rankIncrement: String
let rankOldAndNew: String
let title: String
let audienceCount: String
let audienceAccumulationCount: String
init(){}
}
```
### Identifiable 프로토콜을 통한 DiffableDataSource 성능향상
기존에는 DiffableDataSource의 Item부분 Identifier로 `struct`타입의 구조체를 넣었습니다.
```swift
struct BoxOfficeItem: Hashable {
let rank: String
let rankIncrement: String
// ...
}
```
하지만 구조체가 `Hashable`프로토콜을 따른다고해서 과연 유일한 값이 되는건지에 대해서 궁금해져서 다음의 자료를 찾아보았습니다. [Apple - Updating Collection Views Using Diffable Data Sources](https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/updating_collection_views_using_diffable_data_sources)
위 공식문서에서는 `DiffableDataSource`의 최적화를 위해 `snapshot`을 통해 데이터 전체를 받는 것이 아닌 식별할 수 있는`id`값만 가질 수 있도록 권장하고 있습니다.
`Identifiable` 프로토콜은 `ID`라는 associatedtype을 가지고 있고,`id`프로퍼티를 가져야 합니다. 그리고 이 `id`프로퍼티의 타입은 `Self.ID`이기 때문에 프로토콜을 채택하고 난 다음에 결정되는 타입입니다.
그래서 다음과 같이 수정해, Diffable DataSource의 성능을 높였습니다.
```swift
struct BoxOfficeItem: Identifiable {
let id = UUID()
let rank: String
let rankIncrement: String
// ...
}
```
### dequeueConfiguredReusableCell의 문제점
`tableView`와 마찬가지로 `collectionView`에서도 cell을 재사용하여 UI를 그려주게 됩니다. 따라서 스크롤을 움직여서 몇몇의 `cell`이 안보이게 한 후 다시 그려주는 작업을 수행하게 하면 `cell`의 속성 값이 그대로 남아 적용되는 것을 확인하였습니다.
처음에는 `prepareForReuse`메서드를 사용하려 했지만 저희의 코드 상 cell이 UI적 요소들을 가지고 있는게 아닌 item이라는 프로퍼티를 가지고 있었기에 `prepareForReuse`를 사용하기가 어려웠습니다.
```swift
final class BoxOfficeListCell: UICollectionViewListCell {
var item: BoxOfficeItem?
override func updateConfiguration(using state: UICellConfigurationState) {}
}
```
따라서 저희가 해결한 방식은 UI를 그리기 전 재사용되던 속성 값들을 초기화 한 후 다시 UI를 그리는 방법으로 해결하였습니다.
```swift
rankIncrementLabel.text = nil
rankIncrementLabel.textColor = .black
```
## 조언 받고 싶은 점
### dequeueConfiguredReusableCell의 문제점
위에서 말했지만 재사용되는 셀의 속성값이 그대로 유지되는 점을 해결하기 위해 cell의 속성 값을 초기화해주고 다시 그리는 과정을 하여 해결하였는데 혹시 다른 방법이 있는지 궁금합니다.