# ๐Ÿ—„๏ธ ๋ฐ•์Šค์˜คํ”ผ์Šค 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)