@GREENOVER
안녕하세요! 그린🌳
BMO🤖 & Serena🐷팀입니다!
은행창구 매니저 프로젝트 Step4 PR 보냅니다.
이번 리뷰도 잘 부탁드립니다!😆
-----
## 구현한 내용
- Kakao API Key를 활용하여 영화 포스터 fetch하기
- fetch한 이미지 및 데이터를 StackView와 ScrollView에 넣기
- UIView를 ViewController에 갈아끼우기
- UIFont의 Weight 변경하기
> **주요 내용**
> API KEY 발급 및 노출 방지, Image Fetch, Cache
> Code-base View, UIFont Extension
-----
## 고민한 부분
### 🔥 loadView에서 View 갈아끼우기
- `Code-base`로 `View`를 짜다보니 `UIComponent`의 갯수에 따라 `ViewController`의 코드양과 역할이 비대해지는 상황에 직면하게 되었습니다. `CollectionView`로만 구성된 `BoxOfficeViewController`과는 달리 `MovieDetailViewController`는 `UIComponent`의 갯수가 많아 코드의 양이 비대해졌습니다.
- 비대해진 코드를 역할을 나누어 해결하고자 하였습니다. `View`를 구성하는 역할을 할 `MovieDetailView`에게, 이를 보여주는 역할을 `MovieDetailViewController`에게로 나누고자 하였습니다.
- 이때 `addSubview`를 하여 `ViewController`에 `MovieDetailView`를 추가해줄 수도 있지만, 추가적인 `view`가 존재하지 않기 때문에 `loadView`에서 애당초 `ViewController`의 `view`를 `MovieDetailView`로 갈아끼웠습니다.
> [TIL - loadView /View 갈아끼우기](https://saber-bobcat-047.notion.site/loadView-View-5622d0257b374cf39aad617cb8b3ad0e?pvs=4)
<br>
### 🔥 API Key를 git에 노출시키지 않는 방법
- `이미지 검색 API`를 사용하기 위해 `Kakao Developer`에서 앱을 생성하여 `REST API Key`를 발급받았습니다. 발급받은 `REST API Key`를 이용해 `이미지 검색 API`를 이용하는데 성공했고, 해당 내용을 커밋하려고 했습니다.
- 변경 내역을 확인하던 중 `API Key`가 포함된 코드가 커밋된다면 이후 별도 관리를 위해 해당 코드를 제거하더라도 깃 커밋 이력에 `API Key`가 그대로 노출 되는 상황이 발생하게 됩니다.
- 저희는 이러한 상황이 발생하지 않도록 하기 위해 `KakaoAPIKey.plist` 파일을 만들고 `gitignore`에 추가했습니다. 해당 파일은 깃을 통해 받을 수 없게 되었기 때문에 팀원에게 직접 파일 전달을 하는 방식으로 작업하게 됩니다.
- `plist`내부의 데이터는 `Bundle`을 확장하여 읽기전용 프로퍼티를 통해 가져오도록 했습니다.
```swift
extension Bundle {
var kakaoApiKey: String {
guard let file = self.path(forResource: "KakaoAPIKey", ofType: "plist") else { return "" }
guard let resource = NSDictionary (contentsOfFile: file) else { return "" }
guard let key = resource["Authorization"] as? String else {
fatalError("KakaoAPIKey.plist에 Authorization를 설정해주세요.")
}
return key
}
}
enum KakaoNameSpace {
...
static let authorization = "Authorization"
static let apiKey = Bundle.main.kakaoApiKey // Bundle에 등록된 Key를 NameSpace로 관리
}
// 이후 URLRequest에 header에 필요한 정보를 주입
let headers = [
KakaoNameSpace.authorization : KakaoNameSpace.apiKey
]
```
> `Bundle`은 실행 가능한 코드와 해당 코드의 자원을 포함하는 디렉토리입니다.
> `Bundle`은 여러가지가 있는데, 그 중 `main`은 앱이 실행되는 코드가 있는 `Bundle` 디렉토리에 접근할 수 있는 `bundle`입니다.
<br>
### 🔥 UIImageView의 Height를 동적으로 입력
- 이미지 검색 API를 통해 어떤 사이즈의 이미지를 가지 와도 이미지의 `width`는 `contentView`의 `width`와 맞추면 되었습니다. 하지만 `UIImage.contentMode`를 어떻게 조정해도 가로 혹은 세로 사이즈의 요구조건을 맞출 수 없었습니다.
- 때문에 `contentMode`와 상관없이, 비율을 계산하여 세로 사이즈를 조정해주기로 했습니다. 다행히도 이미지 검색 시 가로, 세로 사이즈 정보가 함께 제공되었기 때문에 어렵지 않게 높이를 동적으로 입력할 수 있었습니다.
```swift
private func setPosterImage(_ imageDocument: ImageDocument, _ image: UIImage) {
// 비율 = UIImage 프레임 가로 ÷ 로드된 이미지 실제 가로 사이즈
let ratio = self.movieDetailView.posterImage.frame.width / CGFloat(integerLiteral: imageDocument.width)
// 높이 = 비율 × 로드된 이미지 실제 세로 사이즈
let height = ratio * CGFloat(integerLiteral: imageDocument.height)
self.movieDetailView.posterImage.heightAnchor.constraint(equalToConstant: height).isActive = true
self.movieDetailView.posterImage.image = image
}
```
<br>
### 🔥 스크롤뷰 Constraints 오류 해결
- 이미지를 성공적으로 불러오고 비율에 맞게 노출시키는데 성공했으나, 스크롤뷰가 정상적으로 동작하지 않는것을 확인했습니다.
- 여러가지 제약사항을 확인한 결과 스크롤뷰의 `contentsView` 내부 최하단 요소에 `BottomAncor`를 주지 않았던 것을 확인했습니다.
```swift
private func setUpTotalStackViewConstraints() {
NSLayoutConstraint.activate([
totalStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
totalStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
totalStackView.topAnchor.constraint(equalTo: posterImage.bottomAnchor, constant: 8),
// 누락되었던 제약사항 추가
totalStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
```
- 누락된 제약사항을 추가하여 스크롤뷰가 정상 동작하는 것을 확인했습니다.
<br>
### 🔥 UIFont Extension을 활용하여 Dynamic Bold Font 구현
- 특정 문자의 두께를 변경하고자 할 때 어떤 방법을 사용할 지 고민하였습니다.
- swift 기본 제공 메서드를 활용하는 방법이 있지만, 이는 `font`의 `사이즈가 고정`이 된다는 단점이 존재했습니다.
```swift
.systemFont(ofSize: 17, weight: .bold)
```
- `Label`과 `Button`은 `Dynamic Type`에 대한 대응이 되어야한다고 생각했기 때문에, `Font`의 사이즈가 고정되지 않으면서 특정 `Font`의 두께를 조절할 수 있는 방법을 찾고자 하였습니다.
- `UIFont`를 `extension`하여 폰트를 `Custom`할 수 있다는 것을 알게되어 이를 활용하였습니다.
```swift
extension UIFont {
static func preferredFont(for style: TextStyle, weight: Weight) -> UIFont {
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style)
let font = UIFont.systemFont(ofSize: descriptor.pointSize, weight: weight)
let metrics = UIFontMetrics(forTextStyle: style)
return metrics.scaledFont(for: font)
}
}
```
<br>
## 조언을 얻고 싶은 부분
### ⭐️ Singleton vs. VC내부변수
- `imageUrl`을 통해 `image`를 `fetch`해 올 때, `NSCache`를 사용하여 보다 앱의 속도 및 효율성을 향상시키고자 하였습니다. 하지만 이때 `NSCache`를 `Singleton`으로 선언하여 사용할 때와 `전역변수`로 선언하여 사용할 때 동작에 차이가 보였습니다.
- `Singleton`으로 선언하여 사용 시, 저희가 원하는 대로 처음 데이터를 `fetch`한 이후로는 `cache`된 데이터를 사용하였지만, `전역변수`로 선언하여 사용할 땐 `cache`된 데이터가 nil로 나오는 것을 확인할 수 있었습니다.
- 현재 저희 PR 보내는 코드 기준으로 하기와 같이 3줄만 변경되어도 `cache`가 정상적으로 작동하지 않습니다. 저희가 아무리 고민을 해봐도 어디에서 발생되는 문제인지 감조차 잡히지 않습니다ㅜ 이 두가지의 어떤 차이 때문에 동작에 변화가 생기는 것일지 그린의 의견을 여쭤보고 싶습니다.🥹
```swift
final class MovieDetailViewController: UIViewController {
// 전역변수 사용 시 변경되는 코드
var cache = NSCache<NSString, UIImage>()
...
let cacheKey = NSString(string: imageDocument.imageUrl)
// 현재 코드
if let cachedImage = ImageCacheManager.shared.object(forKey: cacheKey) {
// 전역변수 사용 시 변경되는 코드
if let cachedImage = cache.object(forKey: cacheKey) {
DispatchQueue.main.async {
self.setPosterImage(imageDocument, cachedImage)
}
...
DispatchQueue.main.async {
self.setPosterImage(imageDocument, image)
// 현재 코드
ImageCacheManager.shared.setObject(image, forKey: cacheKey)
// 전역변수 사용 시 변경되는 코드
self.cache.setObject(image, forKey: cacheKey)
}
}
}
```
- 캐시 동작의 차이를 가시적적으로 전달드리기 위해 임의로 캐시 이미지 호출부와 새 이미지 호출부에 `print` 함수를 사용하였습니다.
| Singleton(현재 코드)사용 시 동작 | 내부변수 사용 시 동작 |
|:---:|:---:|
|<Img src = "https://cdn.discordapp.com/attachments/1101066086381785130/1138458043131109408/Aug-08-2023_22-04-39.gif" width="350"/>|<Img src = "https://cdn.discordapp.com/attachments/1101066086381785130/1138458043596668990/Aug-08-2023_22-03-42.gif" width="350"/>|
<br>
### ⭐️ URLCache inMemory
- 저희는 프로젝트에 `NSCache`를 적용했지만, `URLCache`도 공부해 보았습니다.
- `URLCache`는 기본적으로 캐시 저장이 `ondisk`인 것을 확인했고, 이것을 변경하기 위해 `StoragePolicy`를 `allowedInMemoryOnly`로 지정해 보았습니다. 하지만 저희의 예상과 달리 `StoragePolicy`를 변경하였음에도 캐시 데이터가 `Memory`에 저장 되지 않았습니다.
- 아래와 같이 여러 실험 끝에 `(30 * 1024 * 1024)` 부터는 `URLCache`가 `메모리`에 저장이 되는 것을 확인할 수 있었습니다.
- 때문에 저희는 저장되어야하는 데이터 보다 지정 `memoryCapacity`가 클 때만 `URLCache`의 `inmemory Policy`가 적용된다고 추측했습니다. 저희의 추측이 맞는지 그린의 의견이 궁금합니다.🤔
- 또한 `URLCache Policy` 관련하여 저희가 추가로 공부해보면 좋은 것이 있다면 조언 부탁드립니다.🙏
```
------------------------------------------------------------------------------
URLCache.shared의 memoryCapacity: 512,000 bytes
diskCapacity: 10,000,000 bytes
CachedURLResponse의 storagePolicy가 .allowedInMemoryOnly일 때,
memoryCapacity: 10, 20 (* 1024 * 1024)일 때는 실패함. 30부터 성공. 31,457,280 bytes
첫번째 data - 1,469,837 bytes
두번째 data - 1,078,478 bytes
------------------------------------------------------------------------------
```
-----
## 참고자료
- [🍎 Developer Apple: Bundle](https://developer.apple.com/documentation/foundation/bundle)
- [🍎 Developer Apple: NSCache](https://developer.apple.com/documentation/foundation/nscache)
- [🍎 Developer Apple: URLCache](https://developer.apple.com/documentation/foundation/urlcache)
- [🍎 Developer Apple: URLRequest.CachePolicy](https://developer.apple.com/documentation/foundation/urlrequest/cachepolicy)
- [🍎 Developer Apple: URLCache.StoragePolicy](https://developer.apple.com/documentation/foundation/urlcache/storagepolicy)
- [🌳 Blog: Cache](https://green1229.tistory.com/57)
- [🌳 Blog: NSCache vs URLCache](https://green1229.tistory.com/268)
- [📒 Blog: 이미지 캐시 처리와 NSCache](https://beenii.tistory.com/187)
- [📒 Blog: URLSession Cahce Policy](https://inuplace.tistory.com/1232)
- [📒 Blog: Custom Cell로 UICollectionView 구현하기](https://velog.io/@jyw3927/Swift-Custom-Cell로-UICollectionView-구현하기-i4xtxih4)
- [📒 Blog: github에 올리면 안되는 APIKEY 숨기기](https://nareunhagae.tistory.com/44)
- [📒 Blog: Dynamic Type을 지원하되, weight는 커스텀하기](https://dev-dain.tistory.com/244)
- [✨ Git: 토요스터디ClassC - DasanKim](https://github.com/WhalesJin/FireSaturdayStudyClassC/blob/dasan/2_week6_cache/2_week6_cache/ViewController.swift)
----추가수정-----
- UIComponents 중복코드 삭제
- Image URL moviecode로 수정
- init / loadView, Viewdidload 호출 순서
- image Animation활용하여 Loading 화면 구성