본문 바로가기

iOS

0812 Q&A 정리

728x90

1. Cell 안의 Cell 구조일 때, 로직 처리를 ViewController에서 하는 이유?

셀의 중첩 구조에서 UI 및 비즈니스 로직 처리를 할 수 있는 방법은 다양하다.

 

예를 들어서 테이블 뷰 안에 컬렉션 뷰가 들어가 있는 경우,

테이블 뷰의 셀 안에서 컬렉션 뷰에 대한 delegate와 datasource를 채택하고 컬렉션 뷰의 셀을 빼서 UI 및 로직 처리가 가능하다.

 

그러나, SeSAC에서 셀에 대한 관리를 모두 최상위 ViewController에서 담당하도록 코드를 구현했고, 그 이유는 아래와 같다.

UI와 데이터는 분리되어야 한다.

 

물론 위에서 말한 것과 같이 관리할 수도 있지만, 그렇게 할 경우 몇가지 번거로운/비효율적인 코드가 발생한다.

데이터를 다룰 때 (ViewController에서 다루게 되는데) UICollectionViewCell의 내용을 UITableViewCell 내에서 다루면 별도로 데이터를 계속 옮겨야하고, 이로 인해서 코드 가독성도 많이 떨어져서, UIViewController에서 다루는게 조금 더 알맞는 방법이다.

 

결국, 데이터들은 ViewController에서 제어가 되기 때문이다.

 

그래서 Cell 안에서는 데이터/비즈니스 로직에 관한 부분을 담당하는 것이 아니라 UI와 관련된 부분을 담당하는 것이고 이런 것들은 ViewController에서 관리하는 것이다.

 

이렇게 분리하는 것이 결과적으로 MVC 패턴에도 보다 적합한 방식이라고 할 수 있다.

 

2. APIManager에서 completionHandler에 같이 담아서 주는 데이터는 어떤 형태여야 할까?

ViewController와 APIManger 파일을 분리하면, 서버 통신을 APIManager 파일에서 관리를 하게 된다.

이때, ViewController에서는 서버 통신 후 데이터를 사용해야 하므로 이를 @escaping 을 통해 받게 된다.

 

여기서 생긴 의문점은 APIManager -> ViewController 로 데이터를 넘길 때 어디까지 가공을 하는 것이 좋은 것인가? 라는 것이다.

  • JSON 형태를 넘기는 것이 좋은 지,
  • 가공된 상태로 (응답 데이터에 대한 구조체가 있다면 해당 데이터로) 넘기는 것이 좋은지 

에 대한 의문이 생겼다.

 

이에 큰 이유가 없다면 (ex, ViewController에서 데이터를 각각 다르게 사용할 경우 등 ..) 최대한 APIManger 파일에서 가공한 상태로 넘겨주는 것이 좋다.

 

이것도 1번과 마찬가지로 이렇게 분리하는 것이 MVC 패턴에 보다 적합한 형태라고 할 수 있다.

 

3. APIManger 코드 정리 

TMDB 프로젝트를 진행하면서 아래와 같은 UI/서버통신을 구현했다.

TMDB GET API 사용

이 때, 각 섹션 별로 Movie > Popular / Top Rated / Now Playing .. 등의 API를 연결해서 데이터를 넣었다.

 

초기에 작성한 코드는 아래와 같다.

    func fetchPopularMovie(page: Int = 1, completionHandler: @escaping completionHandler) {
        let url = EndPoint.popular.requestURL + "?api_key=\(APIKey.APIKey)&language=ko-KR&page=\(page)"

        AF.request(url, method: .get).validate(statusCode: 200...500).responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)

                let statusCode = response.response?.statusCode ?? 500
                if statusCode == 200 {
                    let data = json["results"].arrayValue.map {
                        MovieResponse(title: $0["title"].stringValue,
                                      original_title: $0["original_title"].stringValue,
                                      posterPath: $0["poster_path"].stringValue,
                                      id: $0["id"].intValue)
                    }

                    completionHandler(data)
                }

            case .failure(let error):
                print(error)
            }
        }
    }

    func fetchNowPlayingMovie(page: Int = 1, completionHandler: @escaping completionHandler) {
        let url = EndPoint.nowPlaying.requestURL + "?api_key=\(APIKey.APIKey)&language=ko-KR&page=\(page)"

        AF.request(url, method: .get).validate(statusCode: 200...500).responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                
                let statusCode = response.response?.statusCode ?? 500
                if statusCode == 200 {
                    let data = json["results"].arrayValue.map {
                        MovieResponse(title: $0["title"].stringValue,
                                      original_title: $0["original_title"].stringValue,
                                      posterPath: $0["poster_path"].stringValue,
                                      id: $0["id"].intValue)
                    }
                    
                    completionHandler(data)
                }
                
            case .failure(let error):
                print(error)
            }
        }
    }
    
    func fetchSimilarMovie(movieId: Int = TMDBMovieAPIManager.shared.movieId, page: Int = 1, completionHandler: @escaping completionHandler) {
        let url = EndPoint.similar(id: movieId).requestURL + "?api_key=\(APIKey.APIKey)&language=ko-KR&page=\(page)"
        
        AF.request(url, method: .get).validate(statusCode: 200...500).responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                
                let statusCode = response.response?.statusCode ?? 500
                if statusCode == 200 {
                    let data = json["results"].arrayValue.map {
                        MovieResponse(title: $0["title"].stringValue,
                                      original_title: $0["original_title"].stringValue,
                                      posterPath: $0["poster_path"].stringValue,
                                      id: $0["id"].intValue)
                    }
                    
                    completionHandler(data)
                }
                
            case .failure(let error):
                print(error)
            }
        }
    }

    func fetchTopRatedMovie(page: Int = 1, completionHandler: @escaping completionHandler) {
        let url = EndPoint.topRated.requestURL + "?api_key=\(APIKey.APIKey)&language=ko-KR&page=\(page)"

        AF.request(url, method: .get).validate(statusCode: 200...500).responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)

                let statusCode = response.response?.statusCode ?? 500
                if statusCode == 200 {
                    let data = json["results"].arrayValue.map {
                        MovieResponse(title: $0["title"].stringValue,
                                      original_title: $0["original_title"].stringValue,
                                      posterPath: $0["poster_path"].stringValue,
                                      id: $0["id"].intValue)
                    }

                    completionHandler(data)
                }

            case .failure(let error):
                print(error)
            }
        }
    }

    func fetchUpComingMovie(page: Int = 1, completionHandler: @escaping completionHandler) {
        let url = EndPoint.upComing.requestURL + "?api_key=\(APIKey.APIKey)&language=ko-KR&page=\(page)"

        AF.request(url, method: .get).validate(statusCode: 200...500).responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)

                let statusCode = response.response?.statusCode ?? 500
                if statusCode == 200 {
                    let data = json["results"].arrayValue.map {
                        MovieResponse(title: $0["title"].stringValue,
                                      original_title: $0["original_title"].stringValue,
                                      posterPath: $0["poster_path"].stringValue,
                                      id: $0["id"].intValue)
                    }

                    completionHandler(data)
                }

            case .failure(let error):
                print(error)
            }
        }
    }

이런 식으로 URL 값만 다르고 그 외의 로직처리는 거의 비슷했다. (무엇보다 응답값이 똑같다는 것을 확인할 수 있다.)

 

그래서 위의 코드를 URL을 기준으로 enum 처리를 하여 case를 나누었다.

개선된 코드는 아래와 같다.

enum MovieServie {
    case popular
    case nowPlaying
    case similiar(id: Int)
    case topRated
    case upComing

    var path: String {
        switch self {
        case .popular:
            return EndPoint.popular.requestURL
        case .nowPlaying:
            return EndPoint.nowPlaying.requestURL
        case .similiar(let id):
            return EndPoint.nowPlaying.requestURL
        case .topRated:
            return EndPoint.topRated.requestURL
        case .upComing:
            return EndPoint.upComing.requestURL
        }
    }
}

이렇게 enum을 만들어주고,

 

    func fetchMovie(type: MovieServie, page: Int = 1, completionHandler: @escaping completionHandler) {
        let url = type.path + "?api_key=\(APIKey.APIKey)&language=ko-KR&page=\(page)"
        
        AF.request(url, method: .get).validate(statusCode: 200...500).responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                
                let statusCode = response.response?.statusCode ?? 500
                if statusCode == 200 {
                    let data = json["results"].arrayValue.map {
                        MovieResponse(title: $0["title"].stringValue,
                                      original_title: $0["original_title"].stringValue,
                                      posterPath: $0["poster_path"].stringValue,
                                      id: $0["id"].intValue)
                    }
                    
                    completionHandler(data)
                }
                
            case .failure(let error):
                print(error)
            }
        }
    }

이렇게 어떤 URL을 사용하고 싶은지 type으로 받은 다음 그에 맞는 URL을 반환해서 서버 통신을 구현하는 것이다.

그러면 각 섹션에 대해서 서버 통신을 하는 메서드를 따로 만들지 않아도 하나의 메서드에서 관리할 수 있다.

 


추가로 async await를 적용해보았는데 .. 이게 맞나 .. ? 싶지만 .. 코드를 비교하자면 아래와 같다. (보다 자세한 내용은 .. 언젠가 글로 다시 정리할 것 .. )

    // MARK: - CompletionHandler
    
    func fetchMovie(type: MovieServie, page: Int = 1, completionHandler: @escaping completionHandler) {
        let url = type.path + "?api_key=\(APIKey.APIKey)&language=ko-KR&page=\(page)"
        
        AF.request(url, method: .get).validate(statusCode: 200...500).responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                
                let statusCode = response.response?.statusCode ?? 500
                if statusCode == 200 {
                    let data = json["results"].arrayValue.map {
                        MovieResponse(title: $0["title"].stringValue,
                                      original_title: $0["original_title"].stringValue,
                                      posterPath: $0["poster_path"].stringValue,
                                      id: $0["id"].intValue)
                    }
                    
                    completionHandler(data)
                }
                
            case .failure(let error):
                print(error)
            }
        }
    }
    
    func requestMovie(completionHandler: @escaping ([[MovieResponse]]) -> ()) {
        var movieList: [[MovieResponse]] = []
        
        TMDBMovieAPIManager.shared.fetchMovie(type: .popular) { value in
            movieList.append(value)
            
            TMDBMovieAPIManager.shared.fetchMovie(type: .similiar(id: self.movieId)) { value in
                movieList.append(value)
                
                TMDBMovieAPIManager.shared.fetchMovie(type: .nowPlaying) { value in
                    movieList.append(value)
                }
                
                TMDBMovieAPIManager.shared.fetchMovie(type: .topRated){ value in
                    movieList.append(value)
                    
                    TMDBMovieAPIManager.shared.fetchMovie(type: .upComing) { value in
                        movieList.append(value)
                        
                        completionHandler(movieList)
                    }
                }
            }
        }
    }

이것이 적용하지 않은 것이고 ..

 

    // MARK: - Async/Await
    
    func fetchMovieWithAsyncAwait(type: MovieServie, page: Int = 1) async -> [MovieResponse] {
        let url = type.path + "?api_key=\(APIKey.APIKey)&language=ko-KR&page=\(page)"
        var movieList: [MovieResponse] = []
        
        AF.request(url, method: .get).validate(statusCode: 200...500).responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                
                let statusCode = response.response?.statusCode ?? 500
                if statusCode == 200 {
                    let data = json["results"].arrayValue.map {
                        MovieResponse(title: $0["title"].stringValue,
                                      original_title: $0["original_title"].stringValue,
                                      posterPath: $0["poster_path"].stringValue,
                                      id: $0["id"].intValue)
                    }
                    movieList = data
                }
                
            case .failure(let error):
                print(error)
            }
        }
        
        return movieList
    }
    
    func requestMovieWithAsyncAwait(completionHandler: @escaping ([[MovieResponse]]) -> ()) async {
        var movieList: [[MovieResponse]] = []
        
        let popularMovie = await self.fetchMovieWithAsyncAwait(type: .popular)
        let similiarMovie = await self.fetchMovieWithAsyncAwait(type: .similiar(id: self.movieId))
        let nowPlayingMovie = await self.fetchMovieWithAsyncAwait(type: .nowPlaying)
        let topRatedMovie = await self.fetchMovieWithAsyncAwait(type: .topRated)
        let upComingMovie = await self.fetchMovieWithAsyncAwait(type: .upComing)
        
        movieList.append(popularMovie)
        movieList.append(similiarMovie)
        movieList.append(nowPlayingMovie)
        movieList.append(topRatedMovie)
        movieList.append(upComingMovie)
        
        completionHandler(movieList)
    }

이것이 적용한 것 ..

 

그리고 이제 ViewController에서는 

    private func callRequest() {
        TMDBMovieAPIManager.shared.requestMovie { value in
            self.movieList = value
            self.movieTableView.reloadData()
        }
        
        Task {
            await TMDBMovieAPIManager.shared.requestMovie(completionHandler: { value in
                self.movieList = value
                self.movieTableView.reloadData()
            })
        }
    }

위 - 적용하지 않은 것

아래 - 적용한 것

으로 나눠서 구현할 수 있다.