# ๐๏ธ ๋ฐ์ค์คํผ์ค PR
## ๋ฐ์ค์คํผ์ค [STEP 1] - Erick, kyungmin
์๋
ํ์ธ์ July!!
๋ฆฌ๋ทฐ์ด Erick๐ถ, Kyungmin๐ผ ์
๋๋ค!
๋ฐ์ค์คํผ์ค ์ฒซ๋ฒ์งธ PR์
๋๋ค!
2์ฃผ๊ฐ ์ ๋ถํ๋๋ฆฝ๋๋ค ๐โโ๏ธ
## ๐ค ๊ณ ๋ฏผํ๋ ์
### Unit Test ๐
`Json` ๋ฐ์ดํฐ๊ฐ `BoxOffice`๊ฐ์ฒด๋ก ์ ์์ ์ผ๋ก ๋ณํ๋๋์ง ํ์ธํ๊ธฐ ์ํด `Unit Test`๋ฅผ ์งํํ์์ต๋๋ค.
```swift
let result = try? JSONDecoder().decode(BoxOffice.self, from: dataAsset.data)
XCTAssertNotNil(result)
```
- `decode` ์์
์ `try?`๋ก ์คํํ์ฌ ๋์ฝ๋ฉ์ด ์ฑ๊ณตํ์๋ `nil`์ด ์๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ์ง ๊ฒ์ฌํ์์ต๋๋ค.
```swift
let expectation = "์ผ๋ณ ๋ฐ์ค์คํผ์ค"
let boxOffice = try? JSONDecoder().decode(BoxOffice.self, from: dataAsset.data)
let result = boxOffice?.boxOfficeResult.boxOfficeType
XCTAssertEqual(result, expectation)
```
- `Json` ํ์ผ ๋ด์ `boxOfficeType`์ ๋ฐ์ดํฐ "์ผ๋ณ ๋ฐ์ค์คํผ์ค"์ `decode`๋ฅผ ํตํด ๊ฐ์ ธ์จ ๋ฐ์ดํฐ์ ๊ฐ์ด ๊ฐ์์ง ๊ฒ์ฌํ์์ต๋๋ค.
```swift
let expectation = "๊ฒฝ๊ด์ ํผ"
let boxOffice = try? JSONDecoder().decode(BoxOffice.self, from: dataAsset.data)
let result = boxOffice?.boxOfficeResult.dailyBoxOfficeList.first?.movieName
XCTAssertEqual(result, expectation)
```
- `Json` ํ์ผ ๋ด์ ์ฒซ๋ฒ์งธ ์ํ์ `movieName`์ ๋ฐ์ดํฐ "๊ฒฝ๊ด์ ํผ"์ `decode`๋ฅผ ํตํด ๊ฐ์ ธ์จ ๋ฐ์ดํฐ์ ๊ฐ์ด ๊ฐ์์ง ๊ฒ์ฌํ์์ต๋๋ค.
</br>
## ๐โโ๏ธ ์กฐ์ธ์ ์ป๊ณ ์ถ์ ์
### Multiple Variable Declaration โ๏ธ
`DailyBoxOfficeList`๊ฐ์ฒด์ ํ๋กํผํฐ๋ฅผ ๊ด๋ จ๋ ํ๋กํผํฐ๋ผ๋ฆฌ `Multiple Variable Declaration` ํด๋ ๋๋์ง์ ๋ํ ๊ณ ๋ฏผ์ ํ์ต๋๋ค. `DTO`๊ฐ์ฒด์ ๋ํ์ฌ `Multiple Variable Declaration`์ ํ๋ ๊ฒ์ด ๋ง๋์ง์ ๋ํ ์กฐ์ธ ๋ถํ๋๋ฆฝ๋๋ค.
๐ **Before**
```swift
struct DailyBoxOfficeList: Codable {
let number: String
let rank: String
let rankIntensity: String
let rankOldAndNew: String
let movieCode: String
let movieName: String
let openDate: String
let salesAmount: String
let salesShare: String
let salseIntensity: String
let salesChange: String
let salesAccumulation: String
let audienceCount: String
let audienceIntensity: String
let audienceChange: String
let audienceAccumulation: String
let screenCount: String
let showCount: String
```
๐ **After**
```swift
struct DailyBoxOfficeList: Codable {
let number: String
let rank, rankIntensity: String
let rankOldAndNew: String
let movieCode, movieName: String
let openDate: String
let salesAmount, salesShare, salseIntensity, salesChange, salesAccumulation: String
let audienceCount, audienceIntensity, audienceChange, audienceAccumulation: String
let screenCount: String
let showCount: String
}
```
---
## ๐ ์ฐธ๊ณ ๋งํฌ
- [๐Apple: CodingKey](https://developer.apple.com/documentation/swift/codingkey)
## ๋ฐ์ค์คํผ์ค [STEP 2] - Erick, kyungmin
์๋
ํ์ธ์ July!!
๋ฆฌ๋ทฐ์ด Erick๐ถ, Kyungmin๐ผ ์
๋๋ค!
๋ฐ์ค์คํผ์ค ๋๋ฒ์งธ PR์
๋๋ค!
์ด๋ฒ์๋ ๋ง์ ์กฐ์ธ ํด์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค๐โโ๏ธ
## ๐ ํ์ต๋ด์ฉ
### 1๏ธโฃ URL ์ ๊ตฌ์กฐ๋ ์ด๋ป๊ฒ ๋๋๊ฐ?
**URL ๊ตฌ์กฐ**
- **Scheme**: ํ๋กํ ์ฝ
- **Host**: ์๋ฒ์ ํธ์คํธ ์ด๋ฆ
- **Path**: ์๋ฒ ๊ฒฝ๋ก
- **Query**: ์ถ๊ฐ์ ์ธ ํ๋ผ๋ฏธํฐ
```swift
var url = URL(string: "http://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?&key=mykey&targetDt=20230725")
url?.scheme // http
url?.host // kobis.or.kr
url?.path // /kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json
url?.query // &key=mykey&targetDt=20230725
```
### 2๏ธโฃ HTTP method์ ์ข
๋ฅ์ ์ฐ์
- **GET**: ์๋ฒ๋ก ๋ถํฐ ๋ฆฌ์์ค ๋ฐ์
- **POST**: ์๋ฒ์ ์๋ก์ด ๋ฆฌ์์ค๋ฅผ ๊ฒ์
- **PUT**: ์๋ฒ ๋ด ๋ฆฌ์์ค์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์
๋ฐ์ดํธ
- **PATCH**: ์๋ฒ ๋ด ๋ฆฌ์์ค์ ์ผ๋ถ๋ฅผ ์
๋ฐ์ดํธ
- **DELETE**: ์๋ฒ ๋ด ๋ฆฌ์์ค ์ญ์
```swift
var request = URLRequest(url: url)
request.httpMethod = // HTTP method
```
- `url`์ ์ด์ฉํด `URLRequest`๋ฅผ ๋ง๋ค์ด `request`์ `httpMethod`๋ฅผ ์ถ๊ฐํ ์ ์์ต๋๋ค.
### 3๏ธโฃ Test Double์ ์ข
๋ฅ์ธ mock spy stub dummy๋ ๋ฌด์์ธ๊ฐ ?
- **mock**: ์ค์ ๊ฐ์ฒด์ ๋น์ทํ๊ฒ ๊ตฌํ๋์ด ์์ผ๋ฉฐ `ํ์ ๊ธฐ๋ฐ ํ
์คํธ(Behavior Base Test)`๋ฅผ ์ํด ์ฌ์ฉ๋๋ ๊ฐ์ฒด, ์์๋๋ ํ์์ ๋ํ ์๋๋ฆฌ์ค๋ฅผ ๋ง๋ค์ด ๊ทธ์ ๋ฐ๋ผ ํ
์คํธ๋ฅผ ์งํํฉ๋๋ค.
- **spy**: `Stub`์ ์ญํ ์ ๊ฐ์ง๋ฉด์ ํธ์ถ๋ ๋ด์ฉ์ ๊ธฐ๋กํ๋ ๊ฐ์ฒด, ํธ์ถ ์ฌ๋ถ๋ ํ์ ๋ฑ์ ๊ธฐ๋กํ ์ ์์ต๋๋ค.
- **stub**: `Dummy`๊ฐ ์ค์ ๋ก ๋์ํ๋ ๊ฒ์ฒ๋ผ ๋ง๋ค์ด ์ค์ ์ฝ๋๋ฅผ ๋์ ํด์ ๋์ํด ์ฃผ๋ `์ํ ๊ธฐ๋ฐ ํ
์คํธ(State Base Test)`๋ฅผ ์ํด ์ฌ์ฉ๋๋ ๊ฐ์ฒด, ํ
์คํธ๊ฐ ์ด๋ ค์ด ๋ถ๋ถ์ ๊ฐ์ฒด๋ฅผ ๋์ฒดํ์ฌ ์ญํ ์ ์ต์ํ์ผ๋ก ๊ตฌํํ ๊ฐ์ฒด์
๋๋ค.
- **dummy**: ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ํ
์คํธ ๋๋ธ๋ก์ ์ค์ ๋ก ๋์ํ๋ ๊ธฐ๋ฅ์ด ๊ตฌํ๋์ด ์์ง ์์ ๊ฐ์ฒด, ๊ฐ์ฒด๋ฅผ ์ ๋ฌํ๋ ์ฉ๋๋ก ์ฌ์ฉ๋ฉ๋๋ค.
> `์ํ ๊ธฐ๋ฐ ํ
์คํธ`๋ ๋ฉ์๋๋ฅผ ํธ์ถํ๊ณ ๊ทธ ๊ฒฐ๊ด๊ฐ๊ณผ ์์ ๊ฐ์ ๋น๊ตํ๋ ์์ผ๋ก ๋์ํ๋ ํ
์คํธ์ด๊ณ , `ํ์ ๊ธฐ๋ฐ ํ
์คํธ`๋ ์์๋๋ ํ์๋ค์ ๋ํ ์๋๋ฆฌ์ค๋ฅผ ๋ง๋ค์ด ๋๊ณ , ์๋๋ฆฌ์ค๋๋ก ๋์ํ๋์ง์ ๋ํ ์ฌ๋ถ๋ฅผ ํ์ธํ๋ ํ
์คํธ์
๋๋ค.
## ๐ค ๊ณ ๋ฏผํ๋ ์
### 1๏ธโฃ NetworkManager ํ์
์ค๊ณ
์๊ตฌ์ฌํญ์ ํ์ธํ์๋,
>- ์ด๋ฒ ์คํ
์์ ์งํํ ๋คํธ์ํน ์์
> - ์ค๋์ ์ผ์ผ ๋ฐ์ค์คํผ์ค ์กฐํ
> - ์ํ ๊ฐ๋ณ ์์ธ ์กฐํ
์์ ๊ฐ์ด ๋๊ฐ์ง์ ๋ค๋ฅธ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์์ผ ํ์ต๋๋ค. ํ์ง๋ง ๋คํธ์ํน์ ํ๋ ๊ฐ์ฒด์์ `loadBoxOffice()`, `loadMovie()`์ ๊ฐ์ด ๋ฐ์ดํฐ๋ฅผ ๋๋ ๊ฐ์ ธ์ค๋ ๊ฒ์ ์ข์ง ์๋ค๊ณ ์๊ฐํ์์ต๋๋ค.
`url`์ด ๋ค๋ฅผ๋๋ ๋คํธ์ํน์ ํ ์ ์๋ ํ๋์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์๊ณ , ๊ทธ ๊ฐ์ฒด์ `url`์ ๋๋ ๋ฃ์ด์ฃผ๊ฑฐ๋ `decoding`ํ ํ์
์ ๋๋ ๋ฃ์ด์ฃผ๋ ๊ฒ์ ๋ค๋ฅธ ๊ฐ์ฒด์์ ํด์ผํ๋ ์์
์ด๋ผ๊ณ ์๊ฐํ์ต๋๋ค.
๋ฐ๋ผ์ `NetworkManager`๋ ์๋์ ๊ฐ์ด ์ธ๊ฐ์ง ๊ธฐ๋ฅ์ ๊ฐ์ง๊ณ ์๊ฒ ์ค๊ณํ๊ณ , ์ถํ์ NetworkManager๊ฐ ์ด๋ป๊ฒ ์ฐ์ผ์ง๋ฅผ ๊ณ ๋ฏผํ์ฌ `URLQueryItems`๋ฅผ ๋ง๋ค์ด ์ฃผ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐ ํ ์์ ์
๋๋ค.
โจ`func configuredURL(scheme: String, host: String, path: String, queryItems: [URLQueryItem]) -> URL?`
: โ๏ธ`scheme`, `host`, `path`, `queryItems`๋ฅผ ํตํด `URL`์ ๋ง๋ค์ด ์ฃผ๋ ๊ธฐ๋ฅ
โจ`func startLoad(_ url: URL, completion: @escaping (Data?) -> Void` :
: โ๏ธ`URL`๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํด์ฃผ๋ ๊ธฐ๋ฅ
โจ`func decodeJSON<T: Decodable>(data: Data) -> T?` :
: โ๏ธ`Data`๋ฅผ ๋ฐ์ `JSON`์ผ๋ก ๋์ฝ๋ฉ ํด์ฃผ๋ ๊ธฐ๋ฅ
### 2๏ธโฃ TestDouble
๋คํธ์ํฌ๊ฐ ์ฐ๊ฒฐ๋์ง ์์ ์ํฉ์์ `NetworkManager`์ ํ
์คํธ๋ฅผ ํ๊ธฐ ์ํด `TestDouble` ๊ฐ์ฒด `StubUserSession`์ ๋ง๋ค์ด์ฃผ์ด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ `startLoad`๊ฐ ์ ์์ ์ผ๋ก ์๋ํ๋์ง ํ
์คํธ ํ์ต๋๋ค.
**์์กด์ฑ ์ฃผ์
**
```swift
// ์ค์ NetworkManager ์์ฑ
NetworkManager(urlSession: URLSession.shared)
// ํ
์คํธ NetworkManager ์์ฑ
NetworkManager(urlSession: StubURLSession())
```
- `URLSessionProtocol`์ ๋ง๋ค์ด `NetworkManager`๊ฐ ํ๋กํผํฐ๋ก ํด๋น ํ์
์ ๊ฐ์ง๊ณ ์๋๋ก ํ์ฌ `URLSession`๋ฟ๋ง ์๋๋ผ `StubURLSession`์ ์ฃผ์
ํ ์ ์๋๋ก ํ์ฌ ํ
์คํธํ์ต๋๋ค.
**ํ
์คํธ**
```swift
// given
// ๋ฐ์์ฌ ๋ฐ์ดํฐ๋ฅผ ์์๋ก ์์ฑ
let dummy = DummyData(data: data, response: response, error: nil)
let stubUrlSession = StubURLSession(dummy: dummy)
sut?.urlSession = stubUrlSession
// when
sut?.startLoad(url!, completion: { data in
// then
// startLoad๋ก ๋ฐ์์จ ๋ฐ์ดํฐ๋ DummyData์ ๋ฐ์ดํฐ
XCTAssertEqual(data, dataAsset.data)
expectation.fulfill()
})
```
- `StubURLSession`๋ `DummyData`๋ฅผ ๊ฐ์ง๊ณ `dataTask` ๋ฉ์๋๋ฅผ ์คํ์ `StubURLSessionDataTask`์ ์ด์ฉํด์ `DummyData`๋ง ๋์ ธ์ฃผ๋๋ก ๊ตฌํํ์ต๋๋ค.
## ๐โโ๏ธ ์กฐ์ธ์ ์ป๊ณ ์ถ์ ์
### ๐ API KEY ๊ด๋ฆฌ
`APIKey`๋ ๊ณต์ฉ`repository`์ ๊ฐ์ง๊ณ ์์ผ๋ฉด ์๋๊ณ , `private`ํ๊ฒ ๊ด๋ฆฌ ๋ผ์ผ ํ๋ค๊ณ ์๊ฐํ์ต๋๋ค. ๊ทธ๋์ `.gitignore`์ `.xcconfig`๋ฅผ ์ถ๊ฐ ์์ผ์คฌ์ต๋๋ค.
ํ์ง๋ง ๋ก์ปฌ์์ API config ํ์ผ์ ์ถ๊ฐํ์๋ `project.pbxproj`์ ์๊ธฐ๋ ํ์ผ๊ฒฝ๋ก๋ฅผ `commit`ํด์ค ๋๋ง๋ค `unstage`ํด์ฃผ์ด์ผ ํ์ต๋๋ค.
์ด๋ฐ์์ผ๋ก `APIKey`๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ์ด ๊ด์ฐฎ์์ง, ์ฃผ๋ก ์ด๋ป๊ฒ `APIKey`๋ฅผ ๊ด๋ฆฌํ๋์ง ์กฐ์ธ์ ์ป๊ณ ์ถ์ต๋๋ค๐
---
### ๐ ์ฐธ๊ณ ๋งํฌ
- [๐Apple: URLSession](https://developer.apple.com/documentation/foundation/urlsession)
- [๐Apple: URL](https://developer.apple.com/documentation/foundation/url)
- [๐Apple: Fetching Website Data into Memory](https://developer.apple.com/documentation/foundation/url_loading_system/fetching_website_data_into_memory)
- [๐ป์ผ๊ณฐ๋ท๋ท: Unit Test](https://yagom.net/courses/unit-test-์์ฑํ๊ธฐ/)
## ๋ฐ์ค์คํผ์ค [STEP 2] - Erick, kyungmin
์๋
ํ์ธ์ July!!
๋ฆฌ๋ทฐ์ด Erick๐ถ, Kyungmin๐ผ ์
๋๋ค!
๋ฐ์ค์คํผ์ค ๋๋ฒ์งธ PR์
๋๋ค!
์ด๋ฒ์๋ ๋ง์ ์กฐ์ธ ํด์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค๐โโ๏ธ
<br>
## ๐ ํ์ต๋ด์ฉ
### Closure Capture Strong Reference Cycle
`Closure`๋ `Reference Capture`๋ฅผ ํ๊ธฐ ๋๋ฌธ์ ์ฐธ์กฐ ํ์
์ `Capture`ํ๋ค๋ฉด `RC`๊ฐ ์ฆ๊ฐํ๋ฉฐ ์ด๋ `Strong Reference Cycle`์ ๋ฐ์ ์ํฌ ์ ์์ต๋๋ค.
๋ฐ๋ผ์ ์ํ ์ฐธ์กฐ๊ฐ ๋ฐ์ํ ์ ์๋ ์ฐธ์กฐํ์
์ ์บก์ณํ ๊ฒฝ์ฐ `[weak self]`๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฅผ ๋ฐฉ์งํ ์ ์์ต๋๋ค.
```swift
boxOfficeManager.fetchBoxOffice { [weak self] error in
// ...
DispatchQueue.main.async {
self?.collectionView.reloadData()
self?.collectionView.refreshControl?.endRefreshing()
self?.activityIndicator.stopAnimating()
}
}
```
<br>
## ๐ค ๊ณ ๋ฏผํ๋ ์
### 1๏ธโฃ `UICollectionViewDiffableDataSource`, `UICollectionViewCompositionalLayout`
`UICollectionViewDiffableDataSource`์ `UICollectionViewCompositionalLayout`๋ฅผ ํ์ตํ๊ณ ์ ์ฉํด ๋ณด์์ง๋ง ํ์์ฑ์ ๋๋ผ์ง ๋ชปํด ๊ธฐ์กด์ `UICollectionViewDataSource`, `UICollectionViewDelegateFlowLayout`๋ง์ ์ฌ์ฉํ์ฌ ๊ตฌํํ์์ต๋๋ค.
๐ [**์ ์ฉ์ฝ๋**](https://github.com/h-suo/ios-box-office/blob/step3_2nd/BoxOffice/Controller/BoxOfficeViewController.swift)
**์ด์ **
- ํ์ฌ ํ๋ก์ ํธ์์๋ ํ
์ด๋ธ๋ทฐ์ ๊ฐ์ด ํ ์ค์ ์์ดํ
์ด๋ง ๊ฐ์ง๊ณ ์์ด `layout`์ ๊ทธ๋ฃน๋ณ์ด๋ ์น์
๋ณ๋ก ๋ณต์กํ๊ฒ ๋๋ ์ค ํ์๊ฐ ์์ด `UICollectionViewCompositionalLayout`์ ํ์์ฑ์ ๋๋ผ์ง ๋ชปํ์ต๋๋ค.
- UI์
๋ฐ์ดํธ๋ฅผ ํ๋ ์ํฉ๋ ์ฒ์ ํ๋ฉด์ ์ด๊ฑฐ๋ `refreshControl`๋ก ๋ฐ์ดํฐ๋ฅผ ๋ฆฌ๋ก๋ ํ ๊ฒฝ์ฐ์๋ง ํ๋ฒ์ `CollectionView`๋ฅผ ์
๋ฐ์ดํธ ์์ผ์ฃผ๊ธฐ ๋๋ฌธ์ `UICollectionViewDiffableDataSource`๋ฅผ ์ฌ์ฉํ์ ๋์ ์์ดํ
์ ์ถ๊ฐ ๋ฐ ์ญ์ ํ์ ๋ ์ฌ์ฉ์์๊ฒ ์ ๋๋ฉ์ด์
์ ๋ณด์ฌ์ฃผ๋ ๋ฐ์ํ API๋ก์์ ์ด์ ์ ๊ฐ์ ธ๊ฐ์ง ๋ชปํ๊ธฐ ๋๋ฌธ์ ํ์์ฑ์ ๋๋ผ์ง ๋ชปํ์ต๋๋ค.
### 2๏ธโฃ ๊ฐ์ฒด ๋ถ๋ฆฌ
- `Entity` : ์๋ฒ๋ก๋ถํฐ ๋ฐ์ `Data`๋ฅผ `Decode`ํ ๋ ์ฐ๋ ํ์
- `DTO` : ์ค์ `App`์์ ์ฌ์ฉํ `Data` ํ์
- `NetworkManager` : ์๋ฒ๋ก๋ถํฐ `Data`๋ฅผ ๋ฐ์์ค๋ ์ญํ
- `BoxOfficeManager` : `VC`์ ์ฐ๊ฒฐ๋ ํ์
,
- `DataManager` : `Entity`๋ฅผ `DTO`๋ก ๋ณํํด์ฃผ๋ ์ญํ
- `Extension`
- `URL` : `URL`์ ์์ฑํ๋ `makeKobisURL`๋ฉ์๋ ์ถ๊ฐ
- `UILabel` : `attributedText`์ ๋ถ๋ถ ์์ ๋ณ๊ฒฝํด์ฃผ๋ `asColor`๋ฉ์๋ ์ถ๊ฐ
- `UIAlertController` : ํ๋์ ์ก์
์ ๊ฐ์ง ๊ธฐ๋ณธ์ ์ธ `alert`์ ๋ฐํํ๋ `makedBasicAlert` ๋ฉ์๋ ์ถ๊ฐ
- `DateFormatter` : ์ค๋๋ก๋ถํฐ ์ํ๋ ๋งํผ ๋จ์ด์ง ๋ ์ง๋ฅผ ์ํ๋ ํฌ๋ฉง์ผ๋ก ๋ฐํํ๋ `bringDateString` ๋ฉ์๋ ์ถ๊ฐ
- `NumberFormatter` : ์ซ์๋ก ์ด๋ฃจ์ด์ง `String`์ ๋ฐ์ `Decimal`์คํ์ผ๋ก ํฌ๋ฉงํด์ฃผ๋ `bringDecimalString` ๋ฉ์๋ ์ถ๊ฐ
<br>
## ๐โโ๏ธ ์กฐ์ธ์ ์ป๊ณ ์ถ์ ์
### โญ๏ธ private extension
`extension`์ ํด์ค ๊ณณ์์ ๋ฉ์๋๋ค์ `private`์ ๋ชจ๋ ๋ถ์ฌ ์ค ๊ฒฝ์ฐ `extension`์ `private`์ ํด์ผํ๋์ง ๋ฉ์๋๋ณ๋ก ๋ชจ๋ `private`์ ๋ถ์ฌ์ฃผ๋๊ฒ์ด ๋์์ง์ ๋ํ ๊ณ ๋ฏผ์ ํ์ต๋๋ค. ๋ณดํต ์ด๋ค์์ ๋ฐฉ๋ฒ์ ๋ง์ด ์ฐ๋์ง ์กฐ์ธ ๋ถํ๋๋ฆฝ๋๋ค๐๐ป
**Before**
```swift
private extension BoxOfficeViewController {
func configureUI() {
...
}
func setupConstraint() {
...
}
}
```
**After**
```swift
extension BoxOfficeViewController {
private func configureUI() {
...
}
private func setupConstraint() {
...
}
}
```
## ๐
ํด๊ฒฐํ์ง ๋ชปํ ์
### โ๏ธ stopAnimating
์ด๊ธฐํ๋ฉด์์ ๋ฐ์ดํฐ ๋ก๋์ ์คํจํ๊ณ `refreshControl`๋ฅผ ์ด์ฉํด์ ๋ค์ `loadBoxOfficeData`๋ฅผ ํธ์ถํ์๋ `refreshControl?.endRefreshing()`์ด ์ ์์ ์ผ๋ก ์๋ํ์ง ์๋ ๊ฒ ๊ฐ์ต๋๋ค.
ํ์ง๋ง `present(alert, animated: true)`๋ฅผ ํ์ง ์์ผ๋ฉด `refreshControl?.endRefreshing()` ์ ์์ ์ผ๋ก ์๋ํฉ๋๋ค.
---
### ๐ ์ฐธ๊ณ ๋งํฌ
- [๐Apple: Capturing Values](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Capturing-Values)
- [๐Apple: UICollectionViewDiffableDataSource](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource)
- [๐Apple: UICollectionViewCompositionalLayout](https://developer.apple.com/documentation/uikit/uicollectionviewcompositionallayout)
- [๐Apple: attributedText](https://developer.apple.com/documentation/uikit/uilabel/1620542-attributedtext)
๋ต๋ณ ๊ฐ์ฌํฉ๋๋ค July!!
### 1.
`jsonํ์ฑ ์๋ฌ`๊ฐ ๋ฐ์ํ๋ ๊ฑด ์๋ง `Resource`/`API_KEY` ํ์ผ์ `KOBIS_API_KEY` ํค๋ฅผ ์ถ๊ฐํด์ฃผ์
์ผ ํ ๊ฒ ๊ฐ์ต๋๋ค.
`API` ํค๋ ๊นํ๋ธ์ ์ฌ๋ฆฌ์ง ์๊ณ ๊ฐ์ ๋ก์ปฌ์์ ๊ด๋ฆฌํ๋ ๋ฐฉ์์ผ๋ก ์ฌ์ฉ ์ค์
๋๋ค! (์์ key: `7a526456eb8e084eb294715e006df16f`)
### 2.
์ ๊ฐ ์ดํดํ `Snapshot`์ ์ฅ์ ์ UI ๋ณ๊ฒฝ์ด ์์ ๋ `apply`๋ฅผ ํธ์ถํ๋ฉด `Hashable`ํ ๋ฐ์ดํฐ๋ฅผ ์ด์ฉํด ์ด์ `Snapshot`๊ณผ์ ์ํ๋ฅผ ๋น๊ตํ์ฌ UI๋ฅผ ์
๋ฐ์ดํธํ๊ธฐ ๋๋ฌธ์ ํ์ํ ๋ถ๋ถ๋ง ์
๋ฐ์ดํธํ๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ์์ ์ด์ ๋ ์๋ค๊ณ ์๊ฐํ์ต๋๋ค.
### 3.
`nil`์ธ `object`์ ์ ๊ทผ์ ์๋ฌด ์ผ๋ ์ผ์ด ๋์ง ์์ต๋๋ค. ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์์ ๋ฃ์ด์ฃผ๋ ํด๋ก์ ๊ฐ ๋๋๊ธฐ ์ ์ `BoxOfficeManager`๊ฐ `deinit`๋ ๋ ๋ฐ๋ก `deinit`๋๊ฒ ํ๊ธฐ ์ํด `[weak self]`๋ฅผ ํตํด `RC`๋ฅผ ์ฌ๋ ค์ฃผ์ง ์์๋๋ฐ, ํด๋ก์ ๊ฐ ๋๋๊ธฐ ์ ๊น์ง `deinit`์ ๊ธฐ๋ค๋ ค์ค๋ ์๊ด ์์ ๊ฒ ๊ฐ์ต๋๋ค.
### 4.
์ ํฌ๋ `Entity`๋ JSON ๋ฐ์ดํฐ๋ฅผ 1:1 ๋งคํ๋๋ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๋ ๊ฐ์ฒด์ด๊ณ , `DTO`๋ ์ค์ ์ฌ์ฉํ ๋ ํ์ํ ๋ฐ์ดํฐ๋ค๋ง ๋ชจ์๋ ๊ฐ์ฒด๋ผ๊ณ ์๊ฐํ์ต๋๋ค.
ํ์ง๋ง ํต์์ ์ผ๋ก `DTO`๋ฅผ ์๋ฒ๋ก๋ถํฐ 1:1 ๋งคํ๋๋ ๊ฐ์ฒด๋ก ์ฌ์ฉํ๊ณ , `Entitiy`๋ฅผ ์ค์ ๋ก์ง์์ ์ฌ์ฉํ๋ ๊ฐ์ฒด์ธ ๊ฒ ๊ฐ์ต๋๋ค.
๋ ๋ถ๋ถ ์์ ํ๊ฒ ์ต๋๋ค!
## ๋ฐ์ค์คํผ์คII [step1] Erick, kyungmin
์๋
ํ์ธ์ July!!
๋ฆฌ๋ทฐ์ด Erick๐ถ, Kyungmin๐ผ ์
๋๋ค!
๋ฐ์ค์คํผ์คII์ ์ฒซ ์คํญ์
๋๋ค!
๋ฐ์ค์คํผ์ค 1-4 ๊ฐ์ด ํฌํจ๋ ์ฝ๋์
๋๋ค
์ด๋ฒ์๋ ๋ง์ ์กฐ์ธ ํด์ฃผ์๋ฉด ์๊ฒจ๋ฃ๊ฒ ์ต๋๋ค๐
## ๐ค ๊ณ ๋ฏผํ๋ ์
### 1๏ธโฃ VC์ ๊ธฐ๋ฅ ๋ถ๋ฆฌ
#### ๐บextension ํ์ง ์์ ๋ถ๋ถ
- stored property
- initializer
- viewDidLoad
#### ๐บextension-setupComponents
- component๊ด๋ จ ์ธํ
#### ๐บextension-configureUI
- ๊ฐ๊ฐ์ UI๊ฐ์ฒด์ addSubview
#### ๐บextension-setupConstraint
- ๊ฐ๊ฐ์ UI๊ฐ์ฒด์ constraint
#### ๐บextension-buttonAction
- ๋ฒํผ ๋ฉ์๋
#### ๐บ๊ทธ ์ธ ๊ธฐ๋ฅ
- ๋ธ๋ฆฌ๊ฒ์ดํธ, ๋ฐ์ดํฐ์์ค ๋ฑ..
### 2๏ธโฃ Hashable
July์ ์กฐ์ธ์ผ๋ก `CompositionalLayout`์ `DiffableDataSource`๋ฅผ ์ ์ฉํด๋ณด์์ต๋๋ค.
`DiffableDataSource`๋ `DiffableDataSourceSnapshot`์ ์ด์ฉํด UI๋ฅผ ์
๋ฐ์ดํธ ํ๋๋ฐ ๊ทธ๋ ์ ํฌ๊ฐ `Item`์ ๊ตฌ์ฑํ๋๋ฐ ์ฌ์ฉํ๋ `DailyBoxOffice` ๊ฐ์ฒด๊ฐ `Hashable`์ ์ฑํํด์ผ ํ์ต๋๋ค.
`Hashalbe`์ ์ฑํํ ๋ `struct`๋ ๋ชจ๋ ํ๋กํผํฐ๋ `Hashable`์ ์ค์ํด์ผ ํ๋ค๊ณ ๋์์์ต๋๋ค.
> For a struct, all its stored properties must conform to Hashable.
๊ทธ๋ฌ๋ `rankStateColor`๋ `rankState property`๋ฅผ ํตํด์ ๋ง๋ค์ด์ง๋ `property`์ด๊ธฐ ๋๋ฌธ์
`hash value`๋ฅผ ๋น๊ตํ ๋ ํ์ํ์ง ์๋ ๊ฐ์ด๋ผ๊ณ ์๊ฐํด์ `hash`ํจ์์์ ๋นผ์คฌ์ต๋๋ค.
```swift
typealias RankStateColor = (targetString: String, color: UIColor)
struct DailyBoxOffice: Hashable {
let movieCode: String
let rank: String
let rankState: String
let movieTitle: String
let dailyAndTotalAudience: String
let rankStateColor: RankStateColor
static func == (lhs: DailyBoxOffice, rhs: DailyBoxOffice) -> Bool {
return lhs.movieCode == rhs.movieCode
&& lhs.rank == rhs.rank
&& lhs.rankState == rhs.rankState
&& lhs.movieTitle == rhs.movieTitle
&& lhs.dailyAndTotalAudience == rhs.dailyAndTotalAudience
}
func hash(into hasher: inout Hasher) {
hasher.combine(movieCode)
hasher.combine(rank)
hasher.combine(rankState)
hasher.combine(movieTitle)
hasher.combine(dailyAndTotalAudience)
}
}
```
### 3๏ธโฃ URLRequest
์ํ ํฌ์คํฐ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์ฌ ๋ [๋ค์ ์ด๋ฏธ์ง ๊ฒ์ API](https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide#search-image) ์ด์ฉํด์ผ ํ์ต๋๋ค.
๊ธฐ์กด `API`๋ `URLRequest`๋ฅผ ์ค์ ํ ํ์ ์์ด `URL`์ `QureyItem`๋ง์ ์ค์ ํ์ฌ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ ์ ์์์ง๋ง `๋ค์ ์ด๋ฏธ์ง ๊ฒ์ API`๋ `Request Header`์ `Key`๋ฅผ ๋ด์์ ๋ณด๋ด์ผ ํ์ต๋๋ค.
๋ฐ๋ผ์ `NetworkManager`์ `URLRequest`๋ฅผ ๋ฐ์ ์ ์๋ `requestData`๋ฅผ ์ถ๊ฐํ์์ต๋๋ค.
```swift
func requestData(from urlRequest: URLRequest, completion: @escaping (Result<Data, NetworkError>) -> Void) {
let task = urlSession.dataTask(with: urlRequest) { data, response, error in
// ...
}
// ...
}
```
### 4๏ธโฃ NSCache
๋ฐ์ดํฐ๊ฐ ํฐ ์ด๋ฏธ์ง๋ ํ๋ฒ ๋ฐ์์จ ํ ์ฌ์ฌ์ฉํ ์ ์๋๋ก `CacheManager`๋ฅผ ๋ง๋ค์ด ๊ด๋ฆฌํด์ฃผ์์ต๋๋ค.
```swift
final class CacheManager {
static let shared = NSCache<NSString, UIImage>()
private init() {}
}
```
- `NSString`์ ํค๊ฐ์ผ๋ก `UIImage`๋ฅผ ์ ์ฅํ๋ `NSCache`๋ฅผ ์ฑ๊ธํค ํจํด์ ์ด์ฉํ์ฌ ๊ตฌํํ์ต๋๋ค.
```swift
func fetchPosterImage(_ movieName: String, completion: @escaping (Bool) -> Void) {
if let cacheImage = CacheManager.shared.object(forKey: movieName as NSString) {
posterImage = cacheImage
completion(true)
return
}
// ...
networkManager.requestData(from: urlRequest) { [weak self] result in
// ...
switch result {
case .success(let data):
do {
// ...
CacheManager.shared.setObject(image, forKey: movieName as NSString)
self.posterImage = image
completion(true)
} catch {
// ...
}
case .failure(let error):
// ...
}
}
}
```
- ์ค์ ์บ์ฑ์ ํ๋ ์์น๋ `BoxOfficeManager`์ `fetchPosterImage`์์ ์ด๋ฏธ์ง๋ฅผ ๋ฐ์์ค๊ธฐ ์ ์ `movieName`์ `cacheKey`๋ก ์บ์ฑ๋ ์ด๋ฏธ์ง๊ฐ ์๋ค๋ฉด `requestData`๋ฅผ ํธ์ถํ์ง ์๊ณ ์บ์ฑ๋ ์ด๋ฏธ์ง๋ฅผ ์ฌ์ฉํฉ๋๋ค.
- ์บ์ฑ๋ ์ด๋ฏธ์ง๊ฐ ์๋ค๋ฉด `requestData`๋ก ์ด๋ฏธ์ง๋ฅผ ๋ฐ์์ ์บ์ฑํ๋๋ก ๊ตฌํํ์ต๋๋ค.
### 5๏ธโฃ ๋ ์ง์ ํ
`UICalendarView`๋ฅผ ์ฌ์ฉํ์ฌ ๋ ์ง์ ํ ๋ทฐ๋ฅผ ๊ตฌํํ์์ต๋๋ค.
```swift
private func setupCalendarView() {
let fromDateComponents = DateComponents(calendar: Calendar(identifier: .gregorian), year: 2004, month: 1, day: 1)
guard let fromDate = fromDateComponents.date else {
return
}
let calendarViewDateRange = DateInterval(start: fromDate, end: Date.yesterday)
calendarView.availableDateRange = calendarViewDateRange
let dateSelection = UICalendarSelectionSingleDate(delegate: self)
dateSelection.selectedDate = selectedDate
calendarView.selectionBehavior = dateSelection
}
```
- `๋ฐ์ค์คํผ์ค API`์ ๋ฐ์ดํฐ๊ฐ `2003๋
1์ 1์ผ`์๋ ๊ฐ์ ธ์ค์ง ๋ชปํ๊ณ `2004๋
1์ 1์ผ`์๋ถํฐ ๊ฐ์ ธ์ค๋ ๊ฒ์ ํ์ธํ์์ต๋๋ค. ๋ฐ๋ผ์ ๋ฌ๋ ฅ์ ๋ฒ์๋ฅผ `2004๋
1์ 1์ผ`๋ถํฐ ์ด์ ๋ก ์ค์ ํ์ต๋๋ค.
- `BoxOfficeViewController`์์ ๋ฐ์์จ `selectedDate`๋ฅผ ์ ํ๋ ๋ ์ง๋ก ์ ํํ๋๋ก ํ์ต๋๋ค.
- `UICalendarSelectionSingleDate`๋ก ๋จ์ผ ์ ํ๋ง ๊ฐ๋ฅํ๋๋ก ์ค์ ํ์ต๋๋ค.
```swift
extension CalendarViewController: UICalendarSelectionSingleDateDelegate {
func dateSelection(_ selection: UICalendarSelectionSingleDate, didSelectDate dateComponents: DateComponents?) {
delegate?.selectedDate(date: dateComponents)
dismiss(animated: true)
}
}
```
- `UICalendarSelectionSingleDateDelegate`๋ฅผ ์ฑํํ์ฌ ๋ ์ง๊ฐ ์ ํ๋์์ ๋ `Delegate` ํจํด์ ์ด์ฉํ์ฌ `BoxOfficeViewController` ์ ํ๋ `DateComponents`๋ฅผ ๋๊ฒจ์ฃผ์์ต๋๋ค.
## ๐โโ๏ธ ์กฐ์ธ์ ์ป๊ณ ์ถ์ ์
### 1๏ธโฃ AutoLayout
AutoLayout์ ๋ํ Constant๋ฅผ ์ค๋ ์์๋ก ์ฃผ๋ ๊ฒ์ด ๋ง์ ์ง ๋น์จ๋ก ์ฃผ๋ ๊ฒ์ด ๋ง๋ ์ง, ํ์
์์๋ ์ด๋ค์์ผ๋ก layout์ ์ก์ ๋๊ฐ๋์ง ๊ถ๊ธํฉ๋๋ค๐๐ป
๐ป ์ฒซ๋ฒ์งธ ๊ฒฝ์ฐ
```swift
NSLayoutConstraint.activate([
rankStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 32),
rankStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
rankStackView.widthAnchor.constraint(equalToConstant: 40)
])
```
๐ป ๋๋ฒ์งธ ๊ฒฝ์ฐ
```swift
NSLayoutConstraint.activate([
rankStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, self.frame.width * 0.03),
rankStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
rankStackView.widthAnchor.constraint(equalToConstant: self.frame.width * 0.1)
])
```
---
### ๐ ์ฐธ๊ณ ๋งํฌ
- [๐Apple: UICollectionViewDiffableDataSource](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource)
- [๐Apple: NSDiffableDataSourceSnapshot](https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot)
- [๐Apple: Hashable](https://developer.apple.com/documentation/swift/hashable)
- [๐Apple: UICalendarView](https://developer.apple.com/documentation/uikit/uicalendarview#3992448)
---
## ๋ฐ์ค์คํผ์คII [STEP 2] Erick, kyungmin
์๋
ํ์ธ์ July!!
๋ฆฌ๋ทฐ์ด Erick๐ถ, Kyungmin๐ผ ์
๋๋ค!
๋ฐ์ค์คํผ์คII์ ๋๋ฒ์งธ ์คํญ์
๋๋ค!
์ด๋ฒ์๋ ๋ง์ ์กฐ์ธ ๋ถํ๋๋ฆฝ๋๋ค๐
## ๐ค ๊ณ ๋ฏผํ๋ ์
### 1๏ธโฃ ํ๋ฉด ๋ชจ๋ ๋ณ๊ฒฝ
`alert`์ ๋ฒํผ์ ๋๋ ์ ๋ `CollectionView`์ `layout`๊ณผ `cell`์ ์ด๋ป๊ฒ ๋ฐ๊ฟ์ง ๊ณ ๋ฏผํ์ต๋๋ค.
`CollectionViewMode`๋ผ๋ ์ด๊ฑฐํ์ ๋ง๋ค์ด `Controller`๊ฐ ํ๋กํผํฐ๋ก ์ด๊ฑฐํ ์ธ์คํด์ค๋ฅผ ๋ค๊ณ ์๊ณ `collectionViewMode`๋ฅผ ๋ถ๊ธฐ์ฒ๋ฆฌํ์ฌ `layout`๊ณผ `cell`์ ๊ฒฐ์ ํ๋๋ก ์ค์ ํ์ต๋๋ค.
**CollectionViewMode ์ฝ๋**
```swift
private enum CollectionViewMode {
case list
case grid
}
```
**dataSource ์ฝ๋**
```swift
UICollectionViewDiffableDataSource<Section, DailyBoxOffice>(collectionView: collectionView) { collectionView, indexPath, dailyBoxOffice in
switch self.collectionViewMode {
case .list:
// ...
return listCell
case .grid:
// ...
return gridCell
}
}
```
### 2๏ธโฃ ๋ชจ๋ ๋ณ๊ฒฝ ์ ๋๋ฉ์ด์
์ฒ์์๋ ๋ชจ๋ ๋ณ๊ฒฝ์ ํ ๋ `collectionView.reloadData()`๋ก ๋ชจ๋ ์
์ ๋ค์ ๋ก๋ํ์์ต๋๋ค. ํ์ง๋ง ์ด๋ด ๊ฒฝ์ฐ ์ ๋๋ฉ์ด์
ํจ๊ณผ ์์ด ํ๋ฉด์ด ํ๋ฒ์ ๋ฐ๋์ด ์ฌ์ฉ์ ๊ฒฝํ์ ์ ํ์ํฌ ์ ์๊ธฐ์ `setCollectionViewLayout`๊ณผ `reloadSections`์ ์ฌ์ฉํ์ฌ `layout`์ด ๋ฐ๋๋ ์๊ฐ๊ณผ `section`์ด ๋ฆฌ๋ก๋ ๋๋ ์๊ฐ์ ์ ๋๋ฉ์ด์
ํจ๊ณผ๋ฅผ ์ฃผ์์ต๋๋ค.
**layout์ด ์
๋ฐ์ดํธ ๋ ๋ ์ ๋๋ฉ์ด์
**
```swift
switch collectionViewMode {
case .list:
collectionView.setCollectionViewLayout(listLayout(), animated: true)
case .grid:
collectionView.setCollectionViewLayout(gridLayout(), animated: true)
}
```
**reloadSections์ apply๋ฅผ ์ฌ์ฉํ์ฌ section์ด ์
๋ฐ์ดํธ ๋ ๋ ์ ๋๋ฉ์ด์
**
```swift
var snapshot = NSDiffableDataSourceSnapshot<Section, DailyBoxOffice>()
snapshot.appendSections([.main])
snapshot.appendItems(boxOfficeManager.dailyBoxOffices, toSection: .main)
snapshot.reloadSections([.main])
dailyBoxOfficeDataSource.apply(snapshot, animatingDifferences: true)
```
### 3๏ธโฃ dynamicํ๊ฒ cell์ ๋์ด๋ฅผ ๋ณ๊ฒฝํ๊ธฐ
์ฒ์ `label`๋ค์ `text`์๋ง `adjustsFontForContentSizeCategory`์์ฑ์ `true`๋ก ๋ณ๊ฒฝํด์คฌ์๋๋ `collectionView`์ `cell`๋์ด๊ฐ `dynamic`ํ๊ฒ ๋ณ๊ฒฝ๋์ง ์์์ต๋๋ค.
`centerY`๋ก ์ค์ ๋ผ์๋ `label`๋ค์ ์ ์ฝ์ `topAnchor`์ `bottomAnchor`๋ฅผ `contentView`์ `topAnchor`์ `bottomAnchor`๋ฅผ ์๊ณ , `.estimated`๋ฅผ ํ์ฉํด `dynamic`ํ๊ฒ `cell`์ ๋์ด๊ฐ ๋ณ๊ฒฝ ๋๊ฒ ํด์คฌ์ต๋๋ค.
```swift
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(80))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(80))
```
## ๐โโ๏ธ ์กฐ์ธ์ ์ป๊ณ ์ถ์ ์
### 1๏ธโฃ ์ํ ์์ธํ๋ฉด dynamic type ์ ์ฉ
์๊ตฌ์ฌํญ์์๋ `dynamic type`์ ์ ์ฉํ์๋ ๊ฐ๋
, ์ ์๋
๋, ๊ฐ๋ด์ผ ๋ฑ์ `titleLabel`์ด ํฌ๊ธฐ๊ฐ ์ปค์ ธ์ ๋ชจ๋ ๊ฐ์ ํฌ๊ธฐ๋ฅผ ๊ฐ๋ ๊ฒ์ฒ๋ผ ๋ณด์์ต๋๋ค. `titleLabel`์ `Compression priority`๋ฅผ `.required`๋ก ์ค์ ํ๋ ๋ฐฉ๋ฒ์ด ์ ์ผ ๋ฐ๋์งํ๋ค๊ณ ํ๋จํ์ฌ ์ ์ฉํ์ง๋ง, ์ธ๋ก ์ค์ด ์๊ตฌ์ฌํญ๊ณผ๋ ๋ค๋ฅด๊ฒ ๋ง์ง ์์์ต๋๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด `titleLabe`l๊ณผ ์ค๋ฅธ์ชฝ ์ ๋ณด๋ฅผ ๋ฎ๋ `label`์ ๊ฐ๊ฐ ๋ค๋ฅธ ์ธ๋ก ์คํ๋ทฐ์ ๋ฐ๋ก ๋ด์์ ํด๊ฒฐํ๋ ค ํ์ผ๋ ์ค๋ฅธ์ชฝ ์ ๋ณด๋ฅผ ๋ฎ๋ `label`ํฌ๊ธฐ๊ฐ ์ ๋์ ์ด๊ธฐ๋๋ฌธ์ ์ด ๋ฐฉ๋ฒ์ผ๋ก๋ ํด๊ฒฐํ์ง ๋ชปํ์ต๋๋ค.
๋ฏธ๋ฆฌ `titleLabel`์ ํฌ๊ธฐ๋ฅผ ๊ณ ์ ์์ผ๋๋ ๋ฐฉ๋ฒ ์ด์ธ์ ์ด ๋ฌธ์ ๋ฅผ ์ด๋ป๊ฒ ํด๊ฒฐํ ์ ์์๊น์?
#### ๐ ์๊ตฌ์ฌํญ ํ๋ฉด
<Img src = "https://hackmd.io/_uploads/r1rWYounh.png" width="350"/>
#### ๐จ ํ์ฌ ๊ตฌํ๋ ํ๋ฉด
<Img src = "https://hackmd.io/_uploads/SJppvj_2h.png" width="350"/>
---
### ๐ ์ฐธ๊ณ ๋งํฌ
- [๐Apple: estimated(_:)](https://developer.apple.com/documentation/uikit/nscollectionlayoutdimension/3199057-estimated)
- [๐Apple: setCollectionViewLayout(_:animated:)](https://developer.apple.com/documentation/uikit/uicollectionview/1618086-setcollectionviewlayout)
- [๐Apple: reloadSections(_:)](https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot/3375784-reloadsections)