# 박스오피스 step4 @July911 줄라이 안녕하세요! 코낄이, 리지 입니다. step4 PR 완성하여 올립니다! 2주동안 많은 도움 주셔서 감사합니다. 이번에도 잘 부탁드립니다 ☺️ # 고민했던 점 ## 1️⃣ JSON 타입과 DiffableDataSource가 받을 데이터 타입 분리 웹에서 받은 데이터를 JSON Decoder로 파싱하여 UICollectionViewCell에 전달하고자 UICollectionViewDiffableDataSource를 사용하였습니다. DiffableDataSource는 데이터를 제공하기 위해 snapshot을 사용하는데, 이 snapshot은 section과 item의 key, value로 구성되어 있고 이 둘은 Hashable 프로토콜을 준수해야하는 조건이 필요했습니다. ### 🔍 문제점 처음 구현한 방법은 value에 JSON`DailyBoxOffice` 타입에서 필요한 데이터인 Movie에 `Hashable`을 채택하였습니다. ```swift struct DailyBoxOffice: Decodable { ... struct BoxOfficeResult: Decodable { ... let boxOfficeList: [Movie] ... struct Movie: Decodable, Hashable { // Movie의 모든 프로퍼티 ``` ```swift final class DailyBoxOfficeViewController: UIViewController { private typealias DataSource = UICollectionViewDiffableDataSource<Section, DailyBoxOffice.BoxOfficeResult.Movie> ``` 이 코드에서`DiffableDataSource`의 관심사과 JSON 타입의 관심사를 분리하는게 좋을 것 같다는 줄라이의 의견을 받고 많은 고민을 하였습니다. ### ⚒️ 해결방안 둘의 관심사를 분리해야 하는 이유에 대해 줄라이가 말씀해주신 내용과 저희가 생각한 내용을 정리해보자면, `DiffableDataSource`의 역할은 UI 요소인 collectionViewCell에 데이터를 전달해서 띄워주는 것이기 때문에 네트워킹과의 의존성을 줄이고 필요한 데이터만 알도록 해주는 것이 맞다는 결론을 내렸습니다. 따라서 `DailyBoxOfficeItem` 타입을 만들고 그 타입이 Hashable을 채택하도록 하여 JSON decoder의 관심사와 `DiffableDataSource`의 관심사를 분리하였습니다. ```swift struct DailyBoxOfficeItem: Hashable { init(from movie: DailyBoxOffice.BoxOfficeResult.Movie) { self.rank = movie.rank self.rankVariance = movie.rankVariance self.rankOldAndNew = movie.rankOldAndNew self.code = movie.code self.name = movie.name self.audienceCount = movie.audienceCount self.audienceAccumulation = movie.audienceAccumulation } let identifier = UUID() // uniqueIdentifier를 주기 위해 구현 let rank: String let rankVariance: String let rankOldAndNew: String let code: String let name: String let audienceCount: String let audienceAccumulation: String } ``` ## 2️⃣ URL로 Image 받아오기, 이미지 캐싱하기 영화 포스터 이미지를 받아오기 위해 API를 설계하여 통신을 통해 Image의 URL을 받아왔습니다. ``` swift enum BoxOfficeEndPoint { ... // 영화 포스터 이미지를 받아오기 위한 API case MoviePosterImage(query: String, httpMethod: HttpMethod) } ``` 통신에 성공해서 받아온 URL로 실제 이미지 데이터를 받아오는 과정이 필요했는데, `Data(contentsOf: )`를 사용하여 이미지를 가져오도록 다음과 같이 구현하였습니다. ``` swift func load(url: URL, completion: @escaping () -> Void) { DispatchQueue.global().async { [weak self] in guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else { return } ... } ``` ### 🔍 문제점 하지만 줄라이의 코멘트를 받고 확인해보니, [공식 문서](https://developer.apple.com/documentation/foundation/nsdata/1413892-init)에서는 `Data(contentsOf: )`에 대하여 네트워크 통신에 사용하지 않을 것을 강조하고 있었습니다. `Data(contentsOf: )` 는 현재 스레드에서 작업을 수행하기 때문에 위험할 수 있다고 합니다. ### ⚒️ 해결방안 공식 문서의 가이드에 따라 `Data(contentsOf: )`를 사용하는 대신 `dataTask(with:completionHandler:)` 메서드를 사용하였습니다. ``` swift func load(url: URL, completion: @escaping ((Result<UIImage, NetworkError>) -> Void)) { let urlRequest = URLRequest(url: url) let task = URLSession.shared.dataTask { ... } ... } ``` 또한 추가적인 문제로 Image가 load 되는 속도가 느린 몇몇의 영화가 있어 속도 문제를 해결하고자 이번주 학습활동에 배운 캐싱을 적용해보았습니다. - NSCache의 싱글톤 구현 ```swift final class ImageCacheManager { static let shared = NSCache<NSString, UIImage>() private init() {} } ``` - 적용한 코드 ```swift private func fetchMoviePosterImage(from imageURL: URL) { // 캐시된 이미지가 있다면 여기서 바로 이미지를 띄워주게 됩니다. let cachedKey = NSString(string: imageURL.absoluteString) if let cachedImage = ImageCacheManager.shared.object(forKey: cachedKey) { DispatchQueue.main.async { self.movieInformationScrollView.moviePosterImageView.image = cachedImage self.loadingView.stopAnimating() return } } // 캐시된 이미지가 없다면 loadImage 메서드를 통해서 네트워킹으로 이미지를 받아와 캐싱을 합니다. loadImage(from: imageURL) { [weak self] result in switch result { case .failure(let error): print(error) case .success(let image): DispatchQueue.main.async { ImageCacheManager.shared.setObject(image, forKey: cachedKey) self?.movieInformationScrollView.moviePosterImageView.image = image self?.loadingView.stopAnimating() } } } } ``` 캐싱을 직접 사용해보니 확실히 이미지 로딩 속도가 빨라진 것을 확인할 수 있었습니다! ## 3️⃣ View와 Controller의 기능 분리 View는 판단하는 로직을 포함하거나 데이터를 정제하는 작업은 하지 않도록 하는것이 좋다는 의견을 주셔서 다음부터는 고려하도록 하겠습니다! 예시로 이번 프로젝트에서 반영한다면 다음과 같이 해볼 수 있을 것 같습니다. ``` swift // 현재 구조 : View가 데이터를 정제하는 작업을 수행 final class DailyBoxOfficeCollectionViewCell: UICollectionViewCell { ... private func setupMovieListLabels(with movie: DailyBoxOfficeItem) { ... audienceInformationLabel.text = "오늘 \(todayAudience) / 총 \(totalAudience)" } ``` ``` swift // 수정된 구조 : View는 정제된 데이터를 받아서 UI요소에 반영 final class DailyBoxOfficeCollectionViewCell: UICollectionViewCell { ... private func setupMovieListLabels(with audience: String) { ... audienceInformationLabel.text = audience } ``` # 조언을 구하고 싶은 점 ### 1️⃣ API 설계 저희는 `BoxOfficeEndPoint`로 네트워크 통신 요청을 보내도록 설계하였습니다. ``` swift enum BoxOfficeEndPoint { case DailyBoxOffice(tagetDate: String, httpMethod: HttpMethod) case MovieInformation(movieCode: String, httpMethod: HttpMethod) case MoviePosterImage(query: String, httpMethod: HttpMethod) } extension BoxOfficeEndPoint { // 프로퍼티 구현 ... ``` 이번 스텝에서 API가 추가되어 `case MoviePosterImage`를 추가 구현하였는데요, 이 API는 기존의 두 API에는 없던 헤더가 추가된 형태였습니다. 다음과 같이 `createURLRequest` 메서드 내에서 분기하여 처리했는데요, ``` swift func createURLRequest() -> URLRequest? { guard let url = createURL() else { return nil } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = httpMethod switch self { case .MoviePosterImage: urlRequest.setValue("KakaoAK d470dcea6bc2ede97003aac7b84e2533", forHTTPHeaderField: "Authorization") return urlRequest case .DailyBoxOffice, .MovieInformation: return urlRequest } } ``` `BoxOfficeEndPoint`를 따로 만들까 생각이 들기도 했는데요, 아직 모델을 구분하는 기준을 가지고 있지 않아서 모델 설계에 어려움이 있는 것 같습니다. 줄라이의 의견을 여쭙고 싶습니다 :smile: