# ๐Ÿ›์˜คํ”ˆ๋งˆ์ผ“ >ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„ 2022.05.09 ~ 2022.06.03 > >ํŒ€์› : [marisol](https://github.com/marisol-develop), [Eddy](https://github.com/kimkyunghun3) / ๋ฆฌ๋ทฐ์–ด : [๋ฆฐ์ƒ](https://github.com/jungseungyeo) ## ๋ชฉ์ฐจ - [ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ](#ํ”„๋กœ์ ํŠธ-์†Œ๊ฐœ) - [ํ‚ค์›Œ๋“œ](#ํ‚ค์›Œ๋“œ) - [๊ณ ๋ฏผํ•œ์ ](#๊ณ ๋ฏผํ•œ์ ) - [์˜คํ”ˆ๋งˆ์ผ“I](#์˜คํ”ˆ๋งˆ์ผ“I) - [์˜คํ”ˆ๋งˆ์ผ“II](#์˜คํ”ˆ๋งˆ์ผ“II) ## ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ ์˜คํ”ˆ๋งˆ์ผ“ ๋งŒ๋“ค๊ธฐ! ## ์‹คํ–‰ ํ™”๋ฉด ์•ฑ์„ ์ตœ์ดˆ์— ์‹คํ–‰ํ–ˆ์„ ๋•Œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ indicator๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. ![](https://i.imgur.com/L2pEY3Z.gif) ----- ๋™์ผํ•œ ๋ฐ์ดํ„ฐ๋ฅผ List, Grid ํ˜•์‹์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ๋ณด์—ฌ์ค€๋‹ค. ![](https://i.imgur.com/vEAQwdy.gif) ----- ์Šคํฌ๋กค์„ ๋‚ด๋ฆฌ๊ฒŒ ๋˜๋ฉด ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜จ๋‹ค. ![](https://i.imgur.com/44O40S7.gif) ----- ์Šคํฌ๋กค์„ ์œ„๋กœ ์˜ฌ๋ฆฌ๊ฒŒ ๋˜๋ฉด refresh๋กœ ํ†ต์‹ ์„ ํ•ด์„œ ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜จ๋‹ค. ![](https://i.imgur.com/RWtu0o6.gif) ----- ์ƒํ’ˆ์„ ๋“ฑ๋กํ•˜๋ฉด POST ํ˜ธ์ถœ์„ ํ†ตํ•ด ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ๋ฅผ ๋“ฑ๋กํ•œ๋‹ค. ![](https://i.imgur.com/LDbf0lB.gif) ----- ์ƒํ’ˆ ์ˆ˜์ •ํ•˜๋ฉด PATCH ํ˜ธ์ถœ์„ ํ†ตํ•ด ์„œ๋ฒ„์— ์ˆ˜์ •๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋“ฑ๋กํ•œ๋‹ค. ![](https://i.imgur.com/CcI7gzE.gif) ----- ์ƒํ’ˆ ์‚ญ์ œํ•˜๋ฉด DELETE ํ˜ธ์ถœ์„ ํ†ตํ•ด ์„œ๋ฒ„์—์„œ๋„ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•œ๋‹ค. ![](https://i.imgur.com/orKtQgR.gif) ## ๊ฐœ๋ฐœํ™˜๊ฒฝ ๋ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ![swift](https://img.shields.io/badge/swift-5.5-orange) ![xcode](https://img.shields.io/badge/Xcode-13.0-blue) ![iOS](https://img.shields.io/badge/iOS-13.0-yellow) ## ํ‚ค์›Œ๋“œ `git flow` `Test Double` `URLSession` `StubURLSession` `Protocol Oriented Programming` `์ถ”์ƒํ™”` `json` `HTTP method` `decode` `escaping closure` `Cache` `CompositionalLayout` `URLSession` `UIImagePickerController` ### ์ž์„ธํ•œ ๊ณ ๋ฏผ ๋ณด๊ธฐ #### [STEP1](https://github.com/yagom-academy/ios-open-market/pull/142) #### [STEP2](https://github.com/yagom-academy/ios-open-market/pull/153) #### [STEP3](https://github.com/yagom-academy/ios-open-market/pull/164) #### [STEP4](https://github.com/yagom-academy/ios-open-market/pull/170) # ๐Ÿ›์˜คํ”ˆ๋งˆ์ผ“I ## ๐Ÿง ๊ณ ๋ฏผํ•œ์  & ํ•ด๊ฒฐ๋ฐฉ๋ฒ• ### ๐Ÿ“Œ 1. URLSessionDataTask์˜ init์ด deprecated๋œ ๋ฌธ์ œ๋ฅผ ์˜์กด์„ฑ ์ฃผ์ž…์œผ๋กœ ํ•ด๊ฒฐ > Response๋ผ๋Š” ๊ตฌ์กฐ์ฒด๋ฅผ ํ™œ์šฉํ•ด์„œ ์˜์กด์„ฑ ์ฃผ์ž…ํ•˜์—ฌ URLSessionDataTask๋ฅผ ์ƒ์†๋ฐ›๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ๋งŒ๋“ค์–ด์ง„ ํ”„๋กœํ† ์ฝœ ์ฃผ์ž…์„ ํ†ตํ•ด ํ•ด๊ฒฐํ–ˆ๋‹ค. ```swift import Foundation typealias DataTaskCompletionHandler = (Response) -> Void struct Response { var data: Data? var statusCode: Int var error: Error? } protocol URLSessionProtocol { func dataTask(with request: URLRequest, completionHandler: @escaping DataTaskCompletionHandler) func dataTask(with url: URL, completionHandler: @escaping DataTaskCompletionHandler) } extension URLSession: URLSessionProtocol { func dataTask(with request: URLRequest, completionHandler: @escaping DataTaskCompletionHandler) { dataTask(with: request) { data, response, error in guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return } let response = Response(data: data, statusCode: statusCode, error: error) completionHandler(response) }.resume() } func dataTask(with url: URL, completionHandler: @escaping DataTaskCompletionHandler) { dataTask(with: url) { data, response, error in guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return } let response = Response(data: data, statusCode: statusCode, error: error) completionHandler(response) }.resume() } } ``` ### ๐Ÿ“Œ 2. MockURLSession Unit Test ๊ฐ€์งœ ๋ฐ์ดํ„ฐ๋ฅผ ํ™œ์šฉํ•ด์„œ ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์ด ์ž˜ ๋˜๋Š”์ง€ ํ™•์ธ ```swift func test_Product_dummy๋ฐ์ดํ„ฐ์˜_totalCount๊ฐ€_10๊ณผ_์ผ์น˜ํ•œ๋‹ค() { // given let promise = expectation(description: "success") var totalCount: Int = 0 let expectedResult: Int = 10 guard let product = NSDataAsset(name: "products") else { XCTFail("Data ํฌ๋งทํŒ… ์‹คํŒจ") return } let response = Response(data: product.data, statusCode: 200, error: nil) let dummyData = DummyData(response: response) let stubUrlSession = StubURLSession(dummy: dummyData) sutProduct.session = stubUrlSession // when sutProduct.execute(with: FakeAPI()) { result in switch result { case .success(let product): totalCount = product.totalCount case .failure(let error): XCTFail(error.localizedDescription) } promise.fulfill() } wait(for: [promise], timeout: 10) // then XCTAssertEqual(totalCount, expectedResult) } ``` ### ๐Ÿ“Œ 3. list cell๊ณผ grid cell์— dataSource ํ™œ์šฉ ์ฒ˜์Œ์— MainViewController์˜ makeDataSource ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์—์„œ ๋ฐ์ดํ„ฐ๋“ค์„ ๊ฐ€์ง€๊ณ  cell์„ ๊ตฌ์„ฑํ•ด์ฃผ์—ˆ๋Š”๋ฐ, ๊ทธ๋ ‡๊ฒŒ ํ•˜๋‹ค๋ณด๋‹ˆ makeDataSource ๋ฉ”์„œ๋“œ๊ฐ€ ๋„ˆ๋ฌด ๊ธธ์–ด์ง€๊ณ  ์—ญํ•  ๋ถ„๋ฆฌ๋„ ์• ๋งคํ•ด์กŒ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ configureCell์ด๋ผ๋Š” ๋ฉ”์„œ๋“œ์— ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ product๋ฅผ ๋ฐ›๋„๋ก ํ•˜๊ณ , cell ๋‚ด๋ถ€์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ์˜ product ๋ฐ์ดํ„ฐ๋กœ cell์„ ๊ตฌ์„ฑํ•ด์ฃผ๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค. > ๊ฐ๊ฐ grid, list cell์— ๋”ฐ๋ผ View์— ์ง์ ‘์ ์œผ๋กœ ๋ฟŒ๋ ค์ฃผ๊ฒŒ ๋˜๋ฉด View์— ๋กœ์ง์ด ๋” ํ•„์š”ํ•˜๊ธฐ ๋–„๋ฌธ์— Presenter๋ผ๋Š” ์ค‘๊ฐ„ ๋งค์ฒด๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. ์ด ๊ณณ์—์„œ๋Š” cell์—์„œ ํ†ต์‹ ์„ ํ†ตํ•ด ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ํ™œ์šฉํ•ด์„œ ๋ทฐ์— ํ•„์š”ํ•œ ๋กœ์ง๋“ค์„ ๊ตฌํ˜„ํ–ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ฐ์ดํ„ฐํฌ๋งทํ„ฐ๋ฅผ ํ†ตํ•ด ์‰ผํ‘œ๋ฅผ ๋„ฃ์–ด์ฃผ๊ฑฐ๋‚˜ ์žฌ๊ณ  ์ˆ˜๋Ÿ‰์œ ๋ฌด์— ๋”ฐ๋ผ cell์— ๋‹ค๋ฅด๊ฒŒ ๋ณด์—ฌ์ค˜์•ผํ•˜๋Š” ๋ถ€๋ถ„์„ ์ฒ˜๋ฆฌํ•˜์—ฌ์„œ View์—์„œ๋Š” ๋ณด์—ฌ์ฃผ๋Š” ์—ญํ• ๋งŒ์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋ถ„๋ฆฌํ–ˆ๋‹ค. ### ๐Ÿ“Œ 4. collectionView์˜ cell์— ๊ตฌ๋ถ„์„ ์„ ๊ทธ๋ฆฌ๊ธฐ cell๋งˆ๋‹ค ๊ตฌ๋ถ„์„ ์„ ์ฃผ๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์˜ค๋ž˜ ๊ณ ๋ฏผํ–ˆ๋‹ค ๋‹ค๋ฅธ ์บ ํผ๋“ค์—๊ฒŒ ์กฐ์–ธ์„ ๊ตฌํ•ด view๋ฅผ ์–‡๊ฒŒ ๋งŒ๋“ค์–ด์„œ ๋„ฃ๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค๋Š” ๊ฒƒ๋„ ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ , CALayer๋ฅผ extensionํ•ด์„œ bottom์— ๊ตฌ๋ถ„์„ ์„ ๋„ฃ์–ด์ฃผ๋Š” ๋ฐฉ๋ฒ•๋„ ์•Œ๊ฒŒ ๋˜์–ด, CALayer๋ฅผ extensionํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ฑ„ํƒํ–ˆ๋‹ค. > cell ์†์— view๋ฅผ ๋„ฃ์–ด์„œ ๊ตฌ๋ถ„์„ ์„ ๋„ฃ๋Š” ๋ฐฉ๋ฒ•์ด ์กด์žฌํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ์•„๋ž˜ ์ฝ”๋“œ๋กœ CALayer๋ฅผ ํ™•์žฅํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋ฉด ๋” ํŽธ๋ฆฌํ•˜๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ์•„๋ž˜์˜ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. ```swift= extension CALayer { func addSeparator() { let separator = CALayer() separator.frame = CGRect.init(x: 10, y: frame.height - 0.5, width: frame.width, height: 0.5) separator.backgroundColor = UIColor.systemGray2.cgColor self.addSublayer(separator) } } ``` ### ๐Ÿ“Œ 5. modern CollectionView์˜ ๋ทฐ์˜ ์ „ํ™˜์„ ๋น„๋™๊ธฐ๋กœ ๊ตฌํ˜„ segmentedControl์˜ index๋ฅผ ํ™œ์šฉํ•ด์„œ ๋ทฐ์˜ ์ „ํ™˜์„ ๊ตฌํ˜„ํ–ˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ์ฝ”๋“œ๋Š” modern ColletctionView์—์„œ ์ œ๊ณตํ•˜๋Š” init์˜ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋ทฐ์˜ ์ „ํ™˜์„ ํ™œ์šฉํ•˜์ง€ ์•Š๋Š” ์ฝ”๋“œ์˜€๋‹ค. ๊ทธ๋ฆฌํ•˜์—ฌ ๋‚ด๋ถ€์— ์กด์žฌํ•˜๋Š” init๋ฅผ ํ™œ์šฉํ•ด์„œ ์ฒ˜๋ฆฌํ•ด๋ณด์•˜๋‹ค. ```swift public init(sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider, configuration: UICollectionViewCompositionalLayoutConfiguration) ``` ์ด init๋Š” ๋ทฐ ์•ˆ์— ์—ฌ๋Ÿฌ Section๋“ค์— ๋Œ€ํ•œ ์ „ํ™˜์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ฃผ๋ฏ€๋กœ ์ด์ „ ์ฝ”๋“œ์™€ ๋‹ค๋ฅด๊ฒŒ ๋” ํšจ์œจ์ ์œผ๋กœ ๊ฐ€๋Šฅํ•ด์ง„๋‹ค. ์•„๋ž˜์ฒ˜๋Ÿผ ๊ตฌํ˜„ํ–ˆ์œผ๋ฉฐ ```swift private lazy var layouts: UICollectionViewCompositionalLayout = { return UICollectionViewCompositionalLayout(sectionProvider: { _, _ in return LayoutType.section(self.layoutType)() }, configuration: .init()) }() ``` ์ด๊ณณ์—์„œ layoutType๋Š” enum๋ฅผ ํ™œ์šฉํ•ด์„œ ๋‚ด๋ถ€๋ฅผ ์บก์Аํ™”ํ•ด์„œ ์‚ฌ์šฉํ–ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์™ธ๋ถ€์—์„œ์˜ ์ ‘๊ทผ๋„ ํ”ผํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ segmentedIndex๋ฅผ ํ™œ์šฉํ•˜์ง€ ์•Š๊ณ ๋„ ๋ทฐ์˜ ๋ณ€ํ™˜์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ### ๐Ÿ“Œ 6. ์ด๋ฏธ์ง€ ์บ์‹ฑ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ• NSCache๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ด๋ฏธ์ง€๋ฅผ ์บ์‹ฑํ•˜๋„๋ก ํ–ˆ๋‹ค. UIImageView๋ฅผ extensionํ•ด์„œ `loadImage(_ urlString: String)`๋ผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค๊ณ , ์ด๋ฏธ ์บ์‹œ์— ํ•ด๋‹น url์˜ ์ด๋ฏธ์ง€๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด ํ•ด๋‹น ์ด๋ฏธ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜๊ณ , ์บ์‹œ์— ์—†๋Š” ์ด๋ฏธ์ง€๋ผ๋ฉด ํ•ด๋‹น url๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋งŒ๋“ค์–ด ์บ์‹œ์— ์ €์žฅํ•˜๋„๋ก ํ–ˆ๋‹ค. ### ๐Ÿ“Œ 7. ๋ทฐ์˜ ancestor ๋ฌธ์ œ ๋ทฐ์˜ ๋ ˆ์ด์•„์›ƒ์„ ์žก๊ณ  ๋นŒ๋“œ๋ฅผ ํ–ˆ์„ ๋•Œ ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ์ž์ฃผ ๋ฐœ์ƒํ–ˆ๋‹ค. > Thread 1: "Unable to activate constraint with anchors <NSLayoutXAxisAnchor:0x6000034dcec0 \"UILabel:0x12260c090.centerX\"> and <NSLayoutXAxisAnchor:0x6000034dcf00 \"UIView:0x12260ab30.centerX\"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal." ์ด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ์‹œ 2๊ฐ€์ง€๋ฅผ ๊ณ ๋ คํ•˜๋ฉด ๋œ๋‹ค. 1. addSubview๊ฐ€ ์ž˜๋˜์–ด์žˆ๋Š”์ง€ 2. addSubview -> Layout Constraint ์ˆœ์œผ๋กœ ์ž˜ํ–ˆ๋Š”์ง€ ### ๐Ÿ“Œ 8. cell์˜ indexPath ๋น„๊ต vs cell prepareForReuse #### prepareForReuse - ์…€์„ ์žฌ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ๋‹ค - CollectionView์˜ ์žฅ์ ์€ ์…€์„ ์žฌ์‚ฌ์šฉํ•œ๋‹ค๋Š”๊ฒƒ. ๊ทธ๋ž˜์„œ ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ํšจ์œจ์ ์œผ๋กœ ๋ฐœํœ˜ํ•  ์ˆ˜ ์žˆ๋‹ค. - ๋˜ํ•œ Object ์ฑ…์—์„œ ๋งํ•˜๊ธธ, "๊ฐ์ฒด์ง€ํ–ฅ์—์„œ ๊ฐ์ฒด ์‚ฌ์ด์—์„œ ์„œ๋กœ์˜ ์ •๋ณด๋ฅผ ๋งŽ์ด ์•Œ์ˆ˜๋ก ์˜์กด ๊ด€๊ณ„๊ฐ€ ๋†’์•„์ง„๋‹ค."๊ณ  ํ•˜๋Š”๋ฐ, indexPath๊ฐ€ cell์˜ ์ •๋ณด๋ฅผ ์•„๋Š” ๊ฒƒ์ด ๊ฐ์ฒด ์‚ฌ์ด์˜ ์ •๋ณด๋ฅผ ๊ณต์œ ํ•˜๋Š” ๊ฒƒ์ด๊ธฐ ๋–„๋ฌธ์— ์ด๋Š” ์˜์กด๊ด€๊ณ„๋ฅผ ๋†’์ผ ์ˆ˜ ์žˆ๋Š” ๊ฒฐ๊ณผ๋ฅผ ์ดˆ๋ž˜ํ•œ๋‹ค. - ๊ฐ’์„ ๋ถ€๋ถ„์ ์œผ๋กœ updateํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์กด์žฌํ•œ๋‹ค. #### indexPath - indexPath๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ณ ์ •์ ์œผ๋กœ update๋ฅผ ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค - ์—ฌ๊ธฐ์—์„œ๋Š” ์žฌ์‚ฌ์šฉ ์…€์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๊ณ ์ •์ ์œผ๋กœ ์ ์ ˆํ•œ ๊ณณ์— ์—…๋ฐ์ดํŠธ ํ•ด์ฃผ๋ฏ€๋กœ ๊ณ ์ •์ ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์œ ๋ฆฌํ•  ๋“ฏ์‹ถ๋‹ค. - ํ•˜์ง€๋งŒ ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋–จ์–ด์ง€๋ฏ€๋กœ ์ด๋ฅผ ์ฃผ์˜ํ•ด์•ผํ•˜๋ฉฐ, ๊ด€๋ฆฌ๋ฅผ ํ•ด์ค„ ์ˆ˜ ์—†๋Š” ๋‹จ์ ์ด ์กด์žฌํ•œ๋‹ค. > ์œ„์™€ ๊ฐ™์€ ํŠน์„ฑ์ด ์žˆ๊ณ , collectionView์˜ ์žฅ์ ์ธ ์žฌ์‚ฌ์šฉ์„ฑ์„ ์ด์šฉํ•œ prepareForReuse๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์˜€๋‹ค --- # ๐Ÿ›์˜คํ”ˆ๋งˆ์ผ“II ## ๐Ÿง ๊ณ ๋ฏผํ•œ์  & ํ•ด๊ฒฐ๋ฐฉ๋ฒ• ### ๐Ÿ“Œ 1. ๊ณตํ†ต ์ฝ”๋“œ์— ํ”„๋กœํ† ์ฝœ ๊ธฐ๋ณธ ๊ตฌํ˜„ ํ™œ์šฉ ์ƒํ’ˆ ๋“ฑ๋กํ™”๋ฉด, ์ƒํ’ˆ ์ˆ˜์ •ํ™”๋ฉด View ํ”„๋กœํ† ์ฝœ ์ถ”์ƒํ™”๋กœ ์ธํ•œ ์€๋‹‰ํ™” ๋ถˆ๊ฐ€๋Šฅ ๋ฌธ์ œ > ํ”„๋กœํ† ์ฝœ ์ถ”์ƒํ™”์— ๋”ฐ๋ฅธ ์€๋‹‰ํ™” ๋ถˆ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๋…ธ์ถœ๋˜๋ฉด ์•ˆ๋˜๋Š” ๋ถ€๋ถ„์— ๋Œ€ํ•ด์„œ๋Š” ์ถ”์ƒํ™”๋ฅผ ํ•˜๋ฉด ์•ˆ๋˜๊ณ  ์žฌ์‚ฌ์šฉ์„ฑ ์žˆ์œผ๋ฉฐ ์€๋‹‰ํ™”๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š๋Š” ๋ถ€๋ถ„์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค. ```swift protocol Drawable: UIView { var entireStackView: UIStackView { get } var productInfoStackView: UIStackView { get } var priceStackView: UIStackView { get } var priceTextField: UITextField { get } var segmentedControl: UISegmentedControl { get } var productNameTextField: UITextField { get } var discountedPriceTextField: UITextField { get } var stockTextField: UITextField { get } var descriptionTextView: UITextView { get } func configureView() func configurePriceStackView() func configureProductInfoStackView() func configureEntireStackViewLayout() func makeStackView(axis: NSLayoutConstraint.Axis, alignment: UIStackView.Alignment, distribution: UIStackView.Distribution, spacing: CGFloat) -> UIStackView } ``` ### ๐Ÿ“Œ 2. ๋‹ค๋ฅธ ๋ทฐ์— ํ”„๋กœํผํ‹ฐ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ๋ฒ• ๋‹ค๋ฅธ ๋ทฐ์— ํ”„๋กœํผํ‹ฐ ์ „๋‹ฌ ์‹œ ๋™์ผํ•œ ์ƒํ’ˆ์„ ์•Œ๋ ค์ฃผ๊ธฐ ์œ„ํ•ด id๋ฅผ ํ™œ์šฉํ•˜๋Š” ์‹œ์ ์—์„œ์˜ id๊ฐ’์˜ ์€๋‹‰ํ™” ๋ฌธ์ œ > ProductDetailViewController์™€ ProductEditViewController๊ฐ€ ์ดˆ๊ธฐํ™” ๋  ๋•Œ id์™€ productDetail์„ ๋ฐ›๋„๋ก ๊ตฌํ˜„ํ•˜์—ฌ ์€๋‹‰ํ™” ๋ฌธ์ œ ํ•ด๊ฒฐ ```swift // ProductEditViewController.swift private let products: Products init(productDetail: ProductDetail) { self.productDetail = productDetail super.init(nibName: nil, bundle: nil) } // MainViewController.swift extension MainViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let productDetailViewController = ProductDetailViewController(products: item[indexPath.row]) self.navigationController?.pushViewController(productDetailViewController, animated: true) } } ``` ### ๐Ÿ“Œ 3. UIImagePicker์—์„œ ์„ ํƒํ•œ ์ด๋ฏธ์ง€๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ๋ฒ• image๊ฐ€ ์„ ํƒ๋˜์—ˆ์„๋•Œ, `didFinishPickingMediaWithInfo` ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ ์„ ํƒ๋œ ์ด๋ฏธ์ง€๋ฅผ `imageArray์—` appendํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  `cellForItemAt` ๋ฉ”์„œ๋“œ์—์„œ `imageArray[indexPath.row]`๋ฒˆ์งธ ์ด๋ฏธ์ง€๋ฅผ `button.setImage` ํ•ด์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ์ด๋ฏธ์ง€๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ๋ณด์—ฌ์ฃผ๋„๋ก ๊ตฌํ˜„ํ•œ๋‹ค. ### ๐Ÿ“Œ 4. ์—ฌ๋Ÿฌ ๊ฐœ์˜ ImageView ๋ณด์—ฌ์ง€๋„๋ก ํ•˜๋Š” ๋ฐฉ๋ฒ• ์ด๋ฏธ์ง€ ๋ทฐ๋ฅผ ์—ฌ๋Ÿฌ๊ฐœ ๋“ฑ๋ก ์‹œ, ๊ฐ๊ฐ ๋‹ค๋ฅธ ์ด๋ฏธ์ง€๋ทฐ๊ฐ€ ๋‚˜์˜ค์ง€ ์•Š๋Š” ์—๋Ÿฌ > cellForItemAt ๋ฉ”์„œ๋“œ๊ฐ€ ๋ถˆ๋ฆด ๋•Œ๋งˆ๋‹ค cell์— imageView๋ฅผ addSubviewํ•ด์ฃผ๊ณ , presenter์˜ images ๋ฐฐ์—ด ์ค‘ indexPath.row ๋ฒˆ์งธ์˜ ์ด๋ฏธ์ง€๋ฅผ ํ•ด๋‹น imageView์˜ image์— ๋„ฃ์–ด์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ–ˆ๋‹ค. ```swift func setImage(_ presenter: Presenter) { guard let productImages = presenter.images else { return } for image in productImages { guard let url = URL(string: image) else { return } guard let data = try? Data(contentsOf: url) else { return } imageView.image = UIImage(data: data) contentView.addSubview(imageView) } } ``` ```swift func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: EditViewCell.identifier, for: indexPath) as? EditViewCell else { return UICollectionViewCell() } cell.contentView.addSubview(cell.imageView) guard let imageArray = presenter.images else { return UICollectionViewCell() } guard let imageString = imageArray[indexPath.row].url, let imageURL = URL(string: imageString), let imageData = try? Data(contentsOf: imageURL) else { return UICollectionViewCell() } let image = UIImage(data: imageData) cell.imageView.image = image return cell } ``` ### ๐Ÿ“Œ 5. ImageView ์šฉ๋Ÿ‰ ์‚ฌ์ด์ฆˆ ์ฒดํฌ ์ด๋ฏธ์ง€ ์šฉ๋Ÿ‰์„ ์ฒดํฌํ•œ ํ›„ resizeํ•˜์—ฌ ์ €์žฅํ•˜๋„๋ก ํ–ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์‹ค์ œ ๋ฆฌ์‚ฌ์ด์ฆˆํ•œ ๊ฒƒ์„ ๋ณด๊ฒŒ ๋˜๋ฉด ์š”๊ตฌ์‚ฌํ•ญ ๋ณด๋‹ค ๋” ์ž‘์€ ์ด๋ฏธ์ง€ ํฌ๊ธฐ๊ฐ€ ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด๋ฅผ ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ํ‚ค์›Œ์„œ ๊ฝ‰์ฐจ๋„๋ก ํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ์ธ์ง€ ์•„๋‹ˆ๋ฉด ์ด๋Œ€๋กœ ์ž‘์•„์ง„ ํฌ๊ธฐ๊ฐ€ ์ ์ ˆํ•œ ๋ฐฉ๋ฒ•์ด์˜€๋Š”์ง€ ๊ณ ๋ฏผ๋˜์—ˆ๋‹ค. ```swift // ๊ตฌํ˜„๋ถ€ extension UIImage { func resize(newWidth: CGFloat) -> UIImage { let scale = newWidth / self.size.width let newHeight = self.size.height * scale let size = CGSize(width: newWidth, height: newHeight) let renderer = UIGraphicsImageRenderer(size: size) let renderedImage = renderer.image { context in self.draw(in: CGRect(origin: .zero, size: size)) } return renderedImage } func checkImageCapacity() -> Double { var capacity: Double = 0.0 guard let data = self.pngData() else { return 0.0 } capacity = Double(data.count) / 1024 return capacity } } ``` ```swift // ํ˜ธ์ถœ๋ถ€ guard let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage else { return } let imageCapacity = image.checkImageCapacity() if imageCapacity > 300 { let resizedImage = image.resize(newWidth: 80) self.imageArray.append(resizedImage) guard let imageData = resizedImage.jpegData(compressionQuality: 1) else { return } let imageInfo = ImageInfo(fileName: "marisol.jpeg", data: imageData, type: "jpeg") self.networkImageArray.append(imageInfo) } else { self.imageArray.append(image) guard let imageData = image.jpegData(compressionQuality: 1) else { return } let imageInfo = ImageInfo(fileName: "marisol.jpeg", data: imageData, type: "jpeg") self.networkImageArray.append(imageInfo) } ``` > RegistrationViewCell์—์„œ imageView์˜ frame์„ cell์˜ width์™€ height๋กœ ์„ค์ •ํ•ด์ฃผ์–ด ์ด๋ฏธ์ง€๊ฐ€ cell์— ๊ฝ‰ ์ฐจ๊ฒŒ ๋‚˜์˜ค๋„๋ก ํ–ˆ๋‹ค. ```swift imageView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.width) ``` ### ๐Ÿ“Œ 6. ์ƒํ’ˆ secret ์กฐํšŒ๋ฅผ ์œ„ํ•œ secretPost ๊ตฌํ˜„ DELETE ํ•˜๊ธฐ ์ „ secretPOST๋ฅผ ํ˜ธ์ถœ ์‹œ์  ๋ฌธ์ œ, ๋˜ ๋‹ค๋ฅธ httpMethod ์ผ€์ด์Šค๋กœ ๋ถ„๋ฆฌ ๊ฒฐ์ • ๋ฌธ์ œ > httpMethod๋Š” GET/POST/PATCH/DELETE ์ด๋ ‡๊ฒŒ 4๊ฐœ๋งŒ ์žˆ์–ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ณ , POST ๋‚ด๋ถ€์—์„œ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌํ•˜์—ฌ ๊ตฌํ˜„ํ–ˆ๋‹ค. ์‹ค์ œ ํ˜ธ์ถœ๋ถ€์—์„œ๋Š” CompletionHandler๋ฅผ ํ™œ์šฉํ•ด์„œ secretPOST๊ฐ€ ์‹คํ–‰๋˜๊ณ  ์„ฑ๊ณตํ•œ ๊ฐ’์„ ์ด์šฉํ•˜์—ฌ DELETE์— ํ™œ์šฉํ•ด์„œ ๊ตฌํ˜„ํ–ˆ๋‹ค. ```swift mutating private func makePOSTRequest(apiAble: APIable) -> URLRequest? { let boundary = generateBoundary() if let item = apiAble.item { let urlString = apiAble.url + apiAble.path guard let url = URL(string: urlString) else { return nil } var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("multipart/form-data; boundary=\"\(boundary)\"", forHTTPHeaderField: "Content-Type") request.addValue(UserInformation.identifier, forHTTPHeaderField: "identifier") request.addValue("eddy123", forHTTPHeaderField: "accessId") request.httpBody = createPOSTBody(requestInfo: item, boundary: boundary) return request } else { return makeSecretPOSTRequest(apiAble: apiAble) } ``` ### ๐Ÿ“Œ 7. View์˜ ๋กœ์ง์„ VC์—์„œ ์‚ฌ์šฉํ•จ์— ๋”ฐ๋ฅธ ์€๋‹‰ํ™” ๋ฌธ์ œ View์—์„œ ๋กœ์ง ๊ตฌํ˜„์„ ํ•˜๋Š” ๊ฒƒ์ด ์˜ณ์ง€ ์•Š๊ธฐ์— VC์—์„œ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ๋…ธ๋ ฅํ–ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ํ•˜๋‹ค๋ณด๋‹ˆ View์—์„œ ๋งŒ๋“  Label๋ฅผ ์ ‘๊ทผํ•ด์„œ ํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์€๋‹‰ํ™”๋ฅผ ํ•  ์ˆ˜ ์—†๋Š” ์ƒํ™ฉ์ด ์žˆ์—ˆ๋Š”๋ฐ ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ์€๋‹‰ํ™”๋ฅผ ์‹œ๋„ํ•ด์•ผํ• ์ง€ ๊ณ ๋ฏผํ–ˆ๋‹ค. ```swift final class ProductDetailView: UIView { ... private let entireScrollView = UIScrollView() private let productImage = UIImageView() let stockLabel = UILabel() let priceLabel = UILabel() let discountedLabel = UILabel() let productNameLabel = UILabel() private let descriptionTextView = UITextView() private let currencyLabel = UILabel() let pageControl = UIPageControl() } // VC ์‚ฌ์šฉ์ฒ˜ private func configureProductNameUI() { productDetailView.productNameLabel.font = UIFont.boldSystemFont(ofSize: 15) } private func configureStockUI() { productDetailView.stockLabel.textColor = .systemGray2 } ``ํฌ > ์ด๋Ÿฐ ๊ฒฝ์šฐ view์™€ controller๊ฐ€ ๋ชจ๋‘ ํ•ด๋‹น ๊ฐ’์— ์ ‘๊ทผํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, view๋‹จ์—์„œ ๊ทธ๋Œ€๋กœ ๊ฐ’์„ ๋…ธ์ถœํ•˜๋„๋ก ๋‘์—ˆ๋‹ค. ### ๐Ÿ“Œ 8. ๋„คํŠธ์›Œํฌ ์ถ”์ƒํ™” ์šฐ์„  ๊ฐ ๊ฐ์ฒด์˜ ์—ญํ• ์„ ์•„๋ž˜์™€ ๊ฐ™์ด ์ƒ๊ฐํ–ˆ๋‹ค. - `ViewController` - ์ •๋ณด (ex: pageNo, itemsPerPage, productId)๋ฅผ ๊ฐ€์ง€๊ณ  APIable์˜ request ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ - `APIable` - VC์—๊ฒŒ ๋ฐ›์€ ์ •๋ณด๋กœ request ๋ฉ”์„œ๋“œ์—์„œ get / post / patch / delete ๊ตฌ๋ถ„ํ•ด์„œ NetworkManager์˜ ๋ฉ”์„œ๋“œ (execute) ํ˜ธ์ถœ - `NetworkManager` - APIable์—๊ฒŒ ์ „๋‹ฌ๋ฐ›์€ host, path, params ๋“ฑ ์กฐํ•ฉํ•ด์„œ url ์ƒ์„ฑ - url๋กœ request ๋งŒ๋“ค์–ด์„œ ๋„คํŠธ์›Œํฌ ํ†ต์‹  - ํ†ต์‹ ํ•œ ๊ฒฐ๊ณผ๋ฅผ VC์— ์ „๋‹ฌ APIable์„ ๋จผ์ € ๊ตฌํ˜„ํ–ˆ๋Š”๋ฐ, VC๋Š” APIable์˜ request ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ GET์ธ์ง€, POST์ธ์ง€๋ฅผ ์•Œ๊ณ  ์žˆ์„ ํ•„์š”๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์—, request ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์€ ๊ฐ’์— ๋”ฐ๋ผ request ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์—์„œ httpMethod๋ฅผ ์ •ํ•ด์ค˜์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ํ˜•์‹์ด ๋‹ค์–‘ํ•˜๋‹ค๋ณด๋‹ˆ, APIable์˜ ๋ณ€์ˆ˜๋“ค๊ณผ request์˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์ข…๋ฅ˜๊ฐ€ ๋งŽ์•„์กŒ๋‹ค.. ```swift protocol APIable { var method: HTTPMethod? { get } var baseUrl: String? { get } var listParams: [String: String]? { get } var registrationParams: String? { get } var updateParams: Int? { get } var deleteParams: String? { get } var secretParams: String? { get } var images: [ImageInfo]? { get } func request(_ list: [String: String]?, _ registration: String?, _ update: Int?, _ delete: String?, _ secret: String?, completionHandler: @escaping (Result<Any, Error>) -> Void) } ``` > APIable์—๋Š” url์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ base url, path, params์™€ httpmethod๋งŒ ๊ฐ€์ง€๊ณ  ์žˆ๋„๋ก ๊ตฌํ˜„ํ–ˆ๊ณ , ๋“ฑ๋ก, ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ, ์ˆ˜์ •, ์‚ญ์ œ ๋“ฑ๊ณผ ๊ฐ™์€ ์ƒํ™ฉ๋งˆ๋‹ค APIable์„ ์ฑ„ํƒํ•˜๋Š” ๊ตฌ์กฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์คฌ๋‹ค. ```swift protocol APIable { var url: String { get } var path: String { get } var method: HTTPMethod { get } var item: Item? { get } var params: [String: String]? { get } var productId: Int? { get } var secret: String? { get } } struct List: APIable { let url: String = "https://market-training.yagom-academy.kr/" let path: String = "api/products" let method: HTTPMethod = .get let item: Item? = nil let pageNo: Int let itemsPerPage: Int var params: [String: String]? { return ["page_no": String(pageNo), "items_per_page": String(itemsPerPage)] } var productId: Int? = nil var secret: String? = nil ```