---
# System prepended metadata

title: 박스오피스 step4

---


# 박스오피스 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: