Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

박스오피스II [STEP 2] Diana, gama, Danny #134

Open
wants to merge 30 commits into
base: ic_11_diana
Choose a base branch
from

Conversation

forseaest
Copy link

안녕하세요🤩 @stevenkim18!
박스오피스 II [STEP 2] PR 드립니다~!🙏

🤔 고민했던 점

1. BoxOffice 앱을 종료하고 실행했을 때, 이전 화면모드(List, Icon) 유지하는 방법 고민

  • 사용자가 BoxOffice 앱을 종료하고 실행했일 때, 마지막으로 사용했던 화면모드를 유지하고 싶었습니다. 해당 기능을 구현하기 위해 고민한 결과, cellMode 값을 앱이 종료되더라도 저장이 되어야 한다고 생각했습니다. 그래서 UserDefaults 를 사용하기로 했습니다. UserDefaults 를 사용해도 된다고 판단한 이유는 화면모드 자체는 중요한 정보는 아니라고 생각했고, 앱이 종료되더라도 저장된 값이 그대로 갖고 있어야 했기 때문입니다.
  • 먼저, 앱을 실행했을 때 UserDefaults 를 통해 저장되어 있는 cellMode 값을 불러와 레이아웃을 구성하게 하였습니다.
class ViewController: UIViewController {
	...
	private var defaults = UserDefaults.standard
	private lazy var cellMode: CellMode = (defaults.value(forKey: "modeKey") as? String ?? "리스트" == "리스트") ? .list : .icon
	...
	override func viewDidLoad() {
		switch self.cellMode {
		case .list:
			self.collectionView.collectionViewLayout = self.createCollectionViewListLayout()
			self.configureDataSource(.list)
			self.configureUI()
		case .icon:
			self.collectionView.collectionViewLayout = self.createCollectionViewIconLayout()
			self.configureDataSource(.icon)
			self.configureUI()
		}	
		
	}
	
}
  • Key 값은 modeKey로 세팅하였으며, defaults.value(forKey: "modeKey") 메소드를 통해서 만약 nil 이라면 "리스트" 값을 무조건 불러오게 하였습니다. 삼항연산자를 통해서 값이 "리스트" 라면 cellMode 변수에 .list로 할당했고, "리스트"가 아니라면 "아이콘"이므로 cellMode 변수에 .icon을 할당했습니다.

  • 그렇다면, 사용자가 앱을 종료하기 직전에 보고있던 화면모드 값을 UserDefaults 에 저장해야한다고 생각했습니다. 화면모드를 변경하는 changeMode 메소드 내부에서 분기처리 할 때 defaults.setValue(, forKey:) 메소드를 활용해서 해당 cellMode 값을 저장하도록 하였습니다.

@objc private func changeMode(sender: AnyObject) {
	...
	let action = UIAlertAction(title: "\(cellMode.name)", style: .default) { _ in
	    self.defaults.setValue(self.cellMode.name, forKey: "modeKey")
            switch self.cellMode {
            case .list:
                self.collectionView.collectionViewLayout = self.createCollectionViewListLayout()
                self.configureDataSource(.list)
                self.configureUI()
            case .icon:
                self.collectionView.collectionViewLayout = self.createCollectionViewIconLayout()
                self.configureDataSource(.icon)
                self.configureUI()
        }
																		   }
}

2. 엔티티 타입과 DTO 타입의 분리

  • 신작 영화인지 판별하거나 순위 등락에 대해 다르게 표기해주는 함수 등을 뷰 컨트롤러보다 해당 모델 안에서 구현하는 것이 보다 적절하다고 판단하였고, 모든 데이터 타입에 대해 그 결과 API로부터 네트워크 통신을 통해 받아오는 엔티티와 실제 사용되는 값들만 모은 DTO를 만들었습니다. 엔티티 타입에는 DTO 타입으로 변환하는 함수를 추가했고, 뷰 컨트롤러에서 네트워크 통신 후 받아온 엔티티 형태의 데이터를 이 함수를 통해 DTO로 변환시켜 화면에 적용되도록 하였습니다. b4edcba 2ffe7bc 35a4668

3. Icon 화면모드에서 Dynamic Type 모드로 적용할 때 자연스럽게 보여주기 위한 고민

  • Icon 화면모드의 경우, 1개의 Group 안에 2개의 item으로 구성되어 레이아웃을 보여줍니다. 하지만, 구성 컴포넌트들에 Dynamic Type을 적용하고 폰트 크기를 최대로 설정할 경우, 폰트가 잘리는 것을 볼 수 있었습니다.
  • DDG 조에서는 2가지 방법을 고민해보고자 하였습니다.
    • 첫째, 폰트 크기가 커져서 Cell 범위를 벗어날 경우, 폰트의 크기에 맞기 Group과 item의 크기와 개수도 동적으로 변경하는 방법 → 해당 방법의 경우, 폰트 크기에 따른 Group과 item의 크기와 개수를 설정하는 것이 어려웠던 부분이었습니다. 그래서 해당 방법은 채택하지 않았습니다.
    • 둘째, Cell 범위를 벗어날 경우, ... 이 나오게 하여 사용자에게 내용이 더 있지만 폰트가 너무 커서 보여줄 수 있다는 것을 인지할 수 있도록 하는 방법 → 해당 방법의 경우, BoxOfficeCell 클래스 타입에서 해당 컴포넌트들의 레이아웃 수정을 통해 구현이 가능했기 때문에 해당 방법을 채택하였습니다.

4. 카카오 검색 API와 통신하는 함수

영화 상세 화면에서 영화 포스터의 이미지를 띄워주기 위해서 카카오 검색 API를 사용해야 했습니다. 카카오 검색 서비스에서 이미지를 검색하는 API에 영화 이름 + 포스터에 대한 검색의 첫번째 이미지를 출력해줘야 했습니다. 라이브러리를 사용하지 않고, URLSession만을 통한 메소드를 만들려고 시도했습니다.

먼저, 카카오 검색 API의 검색 파라미터들은 공통적으로 원하는 검색어와 이외의 검색 옵션들을 선택적으로 추가할 수 있게 되어 있었습니다.

이름 타입 설명 필수
query String 검색을 원하는 질의어 O
sort String 결과 문서 정렬 방식, accuracy(정확도순) 또는 recency(최신순), 기본 값 accuracy X
page Integer 결과 페이지 번호, 1~50 사이의 값, 기본 값 1 X
size Integer 한 페이지에 보여질 문서 수, 1~80 사이의 값, 기본 값 80 X

그렇다면 이 검색 파라미터들을 하나의 데이터로 표현하는 것이 보다 코드를 이해하고 읽기 쉬울 것으로 판단하여, 구조체로 구현했습니다. 그리고 함수의 파라미터로 받은 검색 옵션은 URLComponents의 queryItems을 통해 URL로 구성되게 만들어줬습니다.

struct KakaoSearchOption {
    var query: String
    var sort: searchResultSortingOption?
    var page: Int?
    var size: Int?
    
    enum searchResultSortingOption: String {
        case accuracy
        case recency
    }
}
urlComponents.queryItems = [
    URLQueryItem(name: "query", value: searchOption.query),
    URLQueryItem(name: "sort", value: searchOption.sort?.rawValue ?? KakaoSearchOption.searchResultSortingOption.accuracy.rawValue),
    URLQueryItem(name: "page", value: String(searchOption.page ?? 1)),
    URLQueryItem(name: "size", value: String(searchOption.page ?? 1)),
]

API 키를 헤더에 담아 GET으로 요청하고, task를 만든 후 resume()으로 네트워크 작업을 시작하게 했습니다.

var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "GET"
urlRequest.setValue("KakaoAK \(URLs.kakaoApiKey)", forHTTPHeaderField: "Authorization")

또한 이 함수를 카카오 검색 API에 대해서 보다 범용적으로 만들고 싶다고 생각했습니다. 카카오 검색 API에는 현재 프로젝트에서 필요한 이미지뿐만 아니라 웹문서, 동영상, 블로그 등을 검색할 수 있습니다. 다른 분야들을 검색하고 싶다면 해당 분야의 케이스와 URL 문자열을 추가하고, 네트워킹 타입을 구현하기만 하면 되도록 함수를 구현했습니다.

enum KakaoSearchType {
    case image
    
    var urlString: String {
        switch self {
        case .image:
            return "\(URLs.KAKAO_IMAGE_SEARCH)"
        }
    }
}

🙏 조언을 얻고 싶은 점

1. Constant 값의 위치

class MovieDetailViewController: UIViewController {
    static let topPaddingConstant: CGFloat = 20
    static let bottomPaddingConstant: CGFloat = -20
    static let leadingPaddingConstant: CGFloat = 20
    static let trailingPaddingConstant: CGFloat = -20
    static let gapPaddingConstant: CGFloat = 10
    static let titleMultiplierConstant: CGFloat = 0.2
    
    final let BODY_FONT: UIFont = FontConstants.BODY
    final let BODY_BOLD_FONT: UIFont = FontConstants.BODY_BOLD
}

현재 프로젝트에서는 뷰 컨트롤러 안에 UI에 필요한 Constant 값을 타입 프로퍼티로 구현하고, MovieDetailViewController.topPaddingConstant처럼 사용하고 있습니다.

enum Constant {
    struct MovieDetail {
        static let topPaddingConstant: CGFloat = 20
        static let bottomPaddingConstant: CGFloat = -20
        static let leadingPaddingConstant: CGFloat = 20
        static let trailingPaddingConstant: CGFloat = -20
        static let gapPaddingConstant: CGFloat = 10
        static let titleMultiplierConstant: CGFloat = 0.2
    }
}

그러나, 이렇게 별도의 파일을 생성하고 Constant 구조체나 열거형을 구현하고, 내부에서 뷰 컨트롤러 이름으로 중첩 타입을 구성하고 Constant 값을 타입 프로퍼티로 하여 Constant.MovieDetail.topPaddingConstant처럼 사용하는 방식이 보다 보편적으로 보았습니다.

저희가 프로젝트에서 사용한 방식은 사용하는 뷰 컨트롤러 안에 정의한만큼 추적이 쉽다는 장점이 있으나, Constant가 아닌 프로퍼티가 많을 경우 복잡할 수 있는 점이 단점이라고 생각합니다. 한편으로는 별도의 파일로 관리하는 것 또한 추적이 쉽다고 생각합니다. 어느 점에 주안점을 두고 방식을 채택해야할지 모르겠습니다.. 어떤 식으로 Constant 값을 관리해야 좋을지 스티븐의 조언을 얻고 싶습니다!

2. SendDataDelegate 네이밍과 위치

코드로 화면을 구현할 때에는 이전 화면으로 돌아가면서 데이터를 전달하기 위해서는 Delegate 패턴을 사용한다는 것을 알게 되었고, 다음과 같이 구현하여 캘린더 화면에서 선택한 날짜를 박스오피스 순위 리스트 화면으로 돌아갈 때 전달하도록 해주었습니다.

protocol SendDataDelegate: AnyObject {
    func updateDate(dateComponents: DateComponents)
}

class CalendarViewController: UIViewController {
    weak var delegate: SendDataDelegate?
}

extension CalendarViewController: UICalendarSelectionSingleDateDelegate {
    func dateSelection(_ selection: UICalendarSelectionSingleDate, didSelectDate dateComponents: DateComponents?) {
        delegate?.updateDate(dateComponents: safeDateComponents)
        dismiss(animated: true)
    }
}

이렇게 SendDataDelegate를 CalendarViewController가 있는 한 파일 안에서 CalendarViewController 앞에 선언해주었는데, 이런 데이터 전송 용으로 만든 프로토콜의 경우에는 위치를 어느 곳에 놓아야 할지가 고민입니다. 또한 네이밍 또한 적절한지 궁금합니다!

3. 네트워크 객체의 싱글톤 패턴 적용
본 프로젝트는 모든 화면에서 API와 네트워크하여 데이터를 가져와 화면에 보여주고 있기에, 뷰 컨트롤러마다 NetworkService를 사용합니다. 매번 인스턴스화 하여 사용하고 있기 때문에, 매번 메모리를 할당하고 초기화하는 과정과 비용이 든다고 생각합니다. 그래서 NetworkService를 싱글톤 객체로 만들어서, 단 하나만을 한번만 생성되도록 하여 메모리 초기화 과정과 비용을 줄이도록 했습니다. 싱글톤의 단점으로는 전역적으로 접근할 수 있기 때문에 관리가 어렵다는 단점이 있으나, NetworkService는 영화진흥위원회 API와 카카오 검색 API로부터 데이터를 일방적으로 받아오는 기능만 있기 때문에 이 단점은 작용되지 않는다고 생각했습니다.

아직 싱글톤 패턴을 언제 사용해야 할지 결정에 미숙하고, 판단 근거가 적절한지에 대한 의구심이 있습니다. 네트워크 객체를 싱글톤으로 적용하는 것에 대해 조언을 얻고 싶습니다..!

forseaest and others added 30 commits April 18, 2024 21:45
…이 실패하여 데이터 값을 못 받아 왔을 경우 Alert() 발생하도록 `getBoxofficeData()` 로직 추가
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants