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/서버통신을 구현했다.
이 때, 각 섹션 별로 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()
})
}
}
위 - 적용하지 않은 것
아래 - 적용한 것
으로 나눠서 구현할 수 있다.
'iOS' 카테고리의 다른 글
Access Control - Basic to Advanced (0) | 2022.08.16 |
---|---|
YPImagePicker (0) | 2022.08.13 |
required init VS override init (0) | 2022.08.10 |
Kakao 다음(Daum) 검색 API 구현 (feat. Expandable Cell) (0) | 2022.08.08 |
Cell안의 UIButton 이벤트 처리 (+WebKit View) (0) | 2022.08.08 |