# My Market ๐Ÿช (MVC) > ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„: 2022-11-10 ~ 2022-12-02 ## ํŒ€์› [์†ก๊ธฐ์›](https://github.com/kiwi1023), [์œ ํ•œ์„](https://github.com/yusw10), [์ด์€์ฐฌ](https://github.com/apwierk2451) ## ๐Ÿ“ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ **์„œ๋ฒ„์™€์˜ ๋„คํŠธ์›Œํ‚น์„ ํ†ตํ•ด ์ƒํ’ˆ์„ ๋“ฑ๋ก, ์ˆ˜์ •, ์‚ญ์ œ๊ฐ€ ๊ฐ€๋Šฅํ•œ ๋‚˜๋งŒ์˜ ๋งˆ์ผ“** ## ๐Ÿ”‘ ํ‚ค์›Œ๋“œ - `UIKit` - `Network` - `URLSession Mock Test` - `Json Decoding Strategy` - `NSCache` - `XCTestExpection` - `completionHandler` - `Escaping Closure` - `URLSession` - `RefreshController` - `Test Double` - `UICollectionView` - `DiffableDataSource` - `CompositionalLayout` ## ๐Ÿ“ฑ ํ”„๋กœ์ ํŠธ ์‹คํ–‰ํ™”๋ฉด |๋ฉ”์ธํ™”๋ฉด (๋ฐฐ๋„ˆ๋ทฐ)|๋ฌดํ•œ์Šคํฌ๋กค|UISearch Bar ๊ตฌํ˜„ |-|-|-| ![](https://i.imgur.com/YNCvTrG.gif)|![](https://i.imgur.com/IetZB5q.gif)|![](https://i.imgur.com/XsmOkXR.gif) |์ƒํ’ˆ ๋“ฑ๋ก|์ƒํ’ˆ ์ˆ˜์ •|์ƒํ’ˆ ์‚ญ์ œ ![](https://i.imgur.com/lG54iB0.gif)|![](https://i.imgur.com/LYpv4HT.gif)|![](https://i.imgur.com/9CXdCm6.gif) ## ๐Ÿš€ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… ### <details> <summary>Launch Screen ์ด์Šˆ</summary> ์ดˆ๊ธฐ CollectionView๋ฅผ ์„ค์ •ํ•˜๋ฉด์„œ ํ™”๋ฉด์„ ํ™•์ธํ•ด๋ณด์•˜๋Š”๋ฐ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ƒํ•˜๋‹จ์˜ ์˜์—ญ์ด ์ž˜๋ ค์„œ ๋‚˜์˜ค๋Š”๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. <img src=https://i.imgur.com/xdtPbDP.png width=40%> CollectionView์˜ ๊ฐ anchor๋ฅผ ๋ฉ”์ธ View Controller์˜ View์˜ safeAreaLayoutGuide์— ๋งž์ถฐ์ฃผ์ง€ ์•Š์•˜๋‹ค๊ณ  ์ƒ๊ฐ๋˜์–ด ๋ทฐ ๊ณ„์ธต ์ฐฝ์„ ๋ณด์•˜๋Š”๋ฐ ์˜คํžˆ๋ ค ๋ทฐ ๊ณ„์ธต์ƒ์—์„œ๋Š” ์ „ํ˜€ ๋ฌธ์ œ๊ฐ€ ์—†์—ˆ๋‹ค. <img src=https://i.imgur.com/vhbb1HH.png width=40%> ์ž˜๋ ธ๋‹ค๊ธฐ ๋ณด๋‹ค๋Š” ์•„์˜ˆ window์ž์ฒด๊ฐ€ ์ž‘๊ฒŒ ์žกํ˜€์žˆ๋‹ค๋Š” ๊ฒƒ์— ๊ฐ€๊นŒ์šด ํ˜•ํƒœ์˜€๋‹ค. <img src=https://i.imgur.com/9z4bjck.png width=50%> ๋ฌธ์ œ๋Š” ์˜ˆ์ƒ ํ•˜์ง€ ๋ชปํ•œ๊ณณ์—์„œ ๋ฐœ์ƒํ•˜๊ณ  ์žˆ์—ˆ๋‹ค. ํ”„๋กœ์ ํŠธ์˜ UI๋ฅผ ์ฝ”๋“œ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด์„œ ๊ธฐ๋ณธ์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” ์Šคํ† ๋ฆฌ๋ณด๋“œ ํŒŒ์ผ๋“ค์„ ๋ชจ๋‘ ์ œ๊ฑฐํ•˜๋Š” ๊ณผ์ •์„ ๊ฑฐ์ณค๋‹ค. ๊ทธ ๊ณผ์ • ์ค‘์— LaunchScreen์„ ์ƒ์„ฑํ•˜๋Š” ์˜ต์…˜์„ ๊ป๋Š”๋ฐ ์ด ์˜ต์…˜์„ ๊บผ๋ฒ„๋ฆฌ๋‹ˆ ์œ„์™€ ๊ฐ™์ด window ์ž์ฒด๊ฐ€ ์ž‘๊ฒŒ ์žกํ˜”๋‹ค. ์ด ์˜ต์…˜์„ ๋‹ค์‹œ ํ‚ด์œผ๋กœ์จ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ์ง€๋งŒ, ์ •ํ™•ํžˆ ์–ด๋–ค ์›๋ฆฌ๋กœ ์ด์™€๊ฐ™์ด ๋™์ž‘๋˜์—ˆ๋Š”์ง€ ๊ด€๋ จ ๊ธ€์ด ๋ถ€์กฑํ•ด์„œ ์•Œ ์ˆ˜ ์—†์—ˆ๋‹ค...(๋Ÿฐ์น˜ ์Šคํฌ๋ฆฐ์„ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์˜ ๊ธ€์ด ์ฃผ๋ฅ˜์˜€๋‹ค) ๋‹ค๋งŒ ์˜ˆ์ƒํ•ด๋ณด์ž๋ฉด, SceneDelegate์—์„œ ์œˆ๋„์šฐ๋ฅผ ์ธ์Šคํ„ด์Šคํ™” ํ•˜๋Š” ๊ณผ์ •์—์„œ ๊ธฐ์กด์—๋Š” ๋Ÿฐ์น˜์Šคํฌ๋ฆฐ์ด ํ™”๋ฉด ์ „์ฒด ํฌ๊ธฐ์— ๋งž๊ฒŒ ์ตœ์ƒ์œ„ Frame์„ ์žก๊ณ  ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ windowํฌ๊ธฐ๊ฐ€ ์žกํ˜€์™”์ง€๋งŒ, ์šฐ๋ฆฌ์˜ ์ฝ”๋“œ์—์„œ๋Š” ์ด ๊ณผ์ •์ด ์ƒ๋žต๋˜์–ด์„œ ์ปจํ…์ธ  ์ตœ์†Œ ํฌ๊ธฐ๋Œ€๋กœ ์œˆ๋„์šฐ๊ฐ€ ์„ค์ •๋œ๊ฒƒ์œผ๋กœ ์ถ”์ธกํ–ˆ๋‹ค. ์•„๋งˆ๋„ SceneDelegate์—์„œ Scene์„ ์ƒ์„ฑ์‹œ์— ์œˆ๋„์šฐ ํฌ๊ธฐ๋ฅผ ์Šคํฌ๋ฆฐ ํฌ๊ธฐ๋กœ ์ง€์ •ํ•ด์ค€๋‹ค๋ฉด ์œˆ๋„์šฐ ํฌ๊ธฐ๊ฐ€ ์˜๋„ํ•œ ๋Œ€๋กœ ๋‚˜์˜ค์ง€ ์•Š์„๊นŒ ์‹ถ๋‹ค. </details> ### <details> <summary> ์ƒํ’ˆ ๋ชฉ๋ก ํ™”๋ฉด์ด๋™์‹œ ์„œ์น˜๋ฐ”๊ฐ€ ๋ณด์—ฌ์ง€๋„๋ก ์ˆ˜์ • </summary> ์ƒํ’ˆ ๋ชฉ๋ก ํ™”๋ฉด์œผ๋กœ ์ง„์ž…์‹œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„œ์น˜๋ฐ”๊ฐ€ ๋ณด์—ฌ์ง€๋„๋ก ์„ค์ •ํ•˜๊ณ ์ž ํ–ˆ๋‹ค. <img src=https://i.imgur.com/S5HlxtC.png width=50%> ์ด๋ฅผ NavigationItem์˜ `hidesSearchBarWhenScrolling` ์†์„ฑ์„ ํ†ตํ•ด ์ง€์ •ํ•˜๊ณ ์ž ํ–ˆ๋Š”๋ฐ ๋ทฐ ์ง„์ž…์‹œ ์„œ์น˜๋ฐ”๊ฐ€ ๋ณด์ด๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ์ด ์†์„ฑ์„ false๋กœ ํ•˜๋ฉด ์Šคํฌ๋กค์‹œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์‚ฌ๋ผ์ง€์ง€ ์•Š์•˜๋‹ค. ๋”ฐ๋ผ์„œ ๋ทฐ ์ตœ์ดˆ ์ง„์ž…ํ•˜์—ฌ ViewWillAppear์‹œ์— ์ด๋ฅผ ํ•ด์ œํ•˜์—ฌ ์„œ์น˜๋ฐ”๊ฐ€ ๋‚˜์˜ค๊ฒŒ ํ•˜๊ณ  ์Šคํฌ๋กค๋ง์ด ์‹œ์ž‘๋  ๋•Œ true๋กœ ๋ฐ”๊ฟ” ์„œ์น˜๋ฐ”๊ฐ€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋„ค๋น„๊ฒŒ์ด์…˜ ์•„์ดํ…œ์— ์ ์šฉ๋˜์–ด ์Šคํฌ๋กคํ•˜๋ฉด ์‚ฌ๋ผ์ง€๋„๋ก ํ•˜์˜€๋‹ค. </details> ### <details> <summary>์ด๋ฏธ์ง€ ์บ์‹œ ์‹ฑ๊ธ€ํ†ค ๊ฐ์ฒด </summary> ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ ๋ทฐ์—์„œ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•˜๊ธฐ ์œ„ํ•ด DataTask ์ž‘์—…์„ UIImageView์˜ extension์œผ๋กœ ํ™•์žฅํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๋‹ค. ```swift extension UIImageView { func setImageUrl(_ url: String) { DispatchQueue.global(qos: .background).async { guard let url = URL(string: url) else { return } URLSession.shared.dataTask(with: url) { (data, result, error) in guard error == nil else { DispatchQueue.main.async { [weak self] in self?.image = UIImage() } return } DispatchQueue.main.async { [weak self] in if let data = data, let image = UIImage(data: data) { self?.image = image } } }.resume() } } ``` ๋‹ค๋งŒ ์ž‘์—…์ค‘ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ์ƒ๊ฐํ–ˆ๋‹ค. 1. ๋ฐ์ดํ„ฐ๋ฅผ loadํ•˜๊ธฐ ์œ„ํ•ด dataTask์ฝ”๋“œ๋ฅผ ํ˜„์žฌ ํ™•์žฅํ•˜๊ณ ์žˆ๋Š”๋ฐ ๋ชจ๋“  UIImageView๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๊ธฐ์กด์˜ ๋ชจ๋“  UIImageView๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ™•์žฅํ•˜๋Š” ๋ฐฉ์‹์—์„œ UIImageView๋ฅผ ์ƒ์†๋ฐ›๋Š” ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ๋งŒ๋“ค์—ˆ๋‹ค. ```swift final class DownloadableUIImageView: UIImageView { var dataTask: URLSessionDataTask? func setImageUrl(_ url: String) { guard let url = URL(string: url) else { return } self.image = UIImage() self.dataTask = URLSession.shared.dataTask(with: url) { (data, result, error) in guard error == nil else { DispatchQueue.main.async { [weak self] in self?.image = UIImage() } return } DispatchQueue.main.async { [weak self] in if let data = data, let image = UIImage(data: data) { self?.image = image } } } self.dataTask?.resume() } func cancelImageDownload() { dataTask?.cancel() dataTask = nil } } ``` ๊ทธ๋Ÿฌ๋‚˜ ์ด ๋ถ€๋ถ„์—์„œ๋„ ์ข€ ๋” ๊ทผ๋ณธ์ ์ธ ๊ณ ๋ฏผ์„ ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. "๊ณผ์—ฐ UIImageView๊ฐ€ ๋„คํŠธ์›Œํฌ ํ†ต์‹  ์ฝ”๋“œ๋ฅผ ์†Œ์œ ํ•˜๋Š”๊ฒŒ ๋งž์„๊นŒ? UIImageView๋Š” ๋ง ๊ทธ๋Œ€๋กœ UI์— ์“ฐ์ด๋Š” ์ด๋ฏธ์ง€ ๋ทฐ ๊ด€๋ จ ์ฝ”๋“œ๋งŒ ์†Œ์ง€ํ•ด์•ผํ•˜์ง€ ์•Š์„๊นŒ?" ๊ฒฐ๊ตญ ์บ์‹ฑ ์ž‘์—…์„ ์ถ”๊ฐ€ํ•˜๋ฉด์„œ UIImageView์—์„œ ๋„คํŠธ์›Œํฌ ํ†ต์‹  ์ฝ”๋“œ๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ์ž‘์—…์„ ํ•œ๋ฒˆ ๋” ์ˆ˜ํ–‰ํ–ˆ๋‹ค. ```swift final class ImageCache { static let shared = ImageCache() private init() {} private let cachedImages = NSCache<NSURL, UIImage>() private var waitingRespoinseClosure = [NSURL: [(UIImage) -> Void]]() private var dataTasks = [NSURL: URLSessionDataTask]() private func image(url: NSURL) -> UIImage? { return cachedImages.object(forKey: url) } func load(url: NSURL, completion: @escaping (UIImage?) -> Void) { if let cachedImage = image(url: url) { DispatchQueue.main.async { completion(cachedImage) } return } if waitingRespoinseClosure[url] != nil { return } else { waitingRespoinseClosure[url] = [completion] } let urlSession = URLSession(configuration: .ephemeral) let task = urlSession.dataTask(with: url as URL) { data, response, error in guard let responseData = data, let image = UIImage(data: responseData), let blocks = self.waitingRespoinseClosure[url], error == nil else { DispatchQueue.main.async { completion(nil) } return } self.cachedImages.setObject(image, forKey: url, cost: responseData.count) for block in blocks { DispatchQueue.main.async { block(image) } } return } dataTasks[url] = task dataTasks[url]?.resume() } func cancel(url: NSURL) { dataTasks[url]?.cancel() dataTasks[url] = nil dataTasks.removeValue(forKey: url) waitingRespoinseClosure[url] = [] waitingRespoinseClosure.removeValue(forKey: url) } } ``` ์บ์‹œ์— ์กด์žฌํ•˜๋Š” ์ด๋ฏธ์ง€๋ผ๋ฉด ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์ทจ์†Œํ•˜๋„๋ก ํ•˜๊ณ , ๋™์ผํ•œ URL์˜ ์ด๋ฏธ์ง€๋ผ๋„ ํ˜„์žฌ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ค‘์ธ์ง€, ์™„๋ฃŒํ•˜์—ฌ ์บ์‹œ์— ์กด์žฌํ•˜๋Š”์ง€ ๋“ฑ ๊ฐ๊ฐ์˜ ๊ฒฝ์šฐ๋งˆ๋‹ค ์ค‘๋ณต ์ž‘์—…์„ ํ”ผํ•˜๋„๋ก ์„ค๊ณ„ํ•ด๋ณด์•˜๋‹ค. ๊ฐ URL ์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ํƒœ์Šคํฌ, ์™„๋ฃŒ์‹œ ํด๋กœ์ €, ์บ์‹œ๋ฅผ ๋‘์—ˆ๋‹ค. </details> ### <details> <summary>UITextView์˜ ํฌ๊ธฐ๊ฐ€ ๋Š˜์–ด๋‚˜์ง€ ์•Š๋Š” ๋ฌธ์ œ</summary> <img src="https://i.imgur.com/KXhDRWW.png" width="250" height="500"/> UITextView๊ฐ€ ์†ํ•œ StackView์˜ bottomAnchor๋ฅผ ScrollView์˜ bottomAnchor์™€ constraint๋ฅผ ๊ฐ™๊ฒŒ ๋งž์ถ”์–ด ์ฃผ์—ˆ์Œ์—๋„ ๋Š˜์–ด๋‚˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค. <img src="https://i.imgur.com/m9ZM32G.png" width="250" height="500"/> ์œ„ view Hierarchy์—์„œ ๋ณด๋“ฏ StackView์˜ ํฌ๊ธฐ ์ž์ฒด๊ฐ€ ๋Š˜์–ด๋‚˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ๋‹ค. ์„ธ๋กœ๋กœ ์Šคํฌ๋กค์ด ๋˜์–ด์•ผํ•˜๋Š” ํŠน์„ฑ์„ ์ฃผ์–ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— StackView์˜ topAnchor, bottonAnchor๋ฅผ contentLayoutGuide์— constraintํ•œ ๊ฒƒ์ด ๋ฌธ์ œ๊ฐ€ ๋˜์—ˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๋‹ค. ๋”ฐ๋ผ์„œ, StackView์˜ heightAnchor๋ฅผ ์ง€์ •ํ•ด์ฃผ์–ด ํ•ด๊ฒฐํ–ˆ๋‹ค. </details> ### <details> <summary> ๋ฐฐ๋„ˆ ๋ทฐ์˜ ์ด๋ฏธ์ง€๊ฐ€ ๋ฌดํ•œ ๋ฐ˜๋ณตํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ• </summary> ์ด๋ฏธ์ง€์˜ ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค์—์„œ ๋‹ค์‹œ ์ฒ˜์Œ ์ธ๋ฑ์Šค๋กœ ๋„˜์–ด๊ฐ€๋Š” ๋กœ์ง์— ๋Œ€ํ•ด์„œ ๊ณ ๋ฏผํ•˜์˜€๋‹ค. ![](https://i.imgur.com/4DsUbs4.png) ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” ์ฒซ๋ฒˆ์งธ ์ด๋ฏธ์ง€ ๋ฐ ๋งˆ์ง€๋ง‰ ์ด๋ฏธ์ง€์— ์ด๋ฏธ์ง€ ๋ทฐ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค์Œ ํ•ด๋‹น ์ด๋ฏธ์ง€ ๋ทฐ์— ๋‹ค์Œ์— ์˜ฌ ์ด๋ฏธ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๊ณ  ๊ทธ ์ด๋ฏธ์ง€๊ฐ€ ํ™”๋ฉด์— ๋‚˜์˜ฌ๋•Œ scrollView์˜ contentOffset์„ ํ•ด๋‹น ์ด๋ฏธ์ง€์˜ ์›๋ž˜ ์œ„์น˜๋กœ ์ด๋™์‹œํ‚จ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ๋˜๋ฉด ์‚ฌ์šฉ์ž์ž…์žฅ์—์„œ๋Š” ์ด์งˆ๊ฐ์„ ๋А๋ผ์ง€ ์•Š๊ณ  ๋ฌดํ•œ ์Šคํฌ๋กค์ด ๋œ๋‹ค๋Š” ์ฐฉ๊ฐ์„ ํ•˜๊ฒŒ ๋œ๋‹ค. </details> ### <details> <summary> ํ™”๋ฉด ์ด๋™๊ฐ„์˜ ๋ฆฌ์ŠคํŠธ ์…€ ์œ„์น˜ ์ด๋™ ๋ฌธ์ œ </summary> ๋ฆฌ์ŠคํŠธ ๋ทฐ์˜ ํŠน์ •์œ„์น˜์—์„œ ํŠน์ • ์…€์— ๋Œ€ํ•œ ์ˆ˜์ •์ด๋‚˜, ์‚ญ์ œ๊ฐ€ ์ด๋ฃจ์–ด์งˆ๋•Œ ํ•ด๋‹น ์ž‘์—…์ดํ›„ ๋‹ค์‹œ ์…€๋กœ ๋Œ์•„ ์˜ฌ๋•Œ ์œ„์น˜๊ฐ€ ๋ณ€๊ฒฝ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค. ![](https://i.imgur.com/JsVv7AR.png) ![](https://i.imgur.com/XTxLFME.png) ํ™”๋ฉด์ด๋™๊ฐ„์— ํ•ด๋‹น ์…€์˜ indexPath ๊ฐ’์„ ํ• ๋‹น ๋ฐ›์€๋‹ค์Œ ํ• ๋‹น ๋ฐ›์€ indexPath ์œ„์น˜๋กœ ์Šคํฌ๋กคํ•ด์ฃผ์—ˆ๋‹ค. </details> ### <details> <summary> RegistView Image ์‚ญ์ œํ•˜๋Š” ๋ฐฉ๋ฒ• </summary> CollectionView๋กœ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€๋งŒ ๊ตฌํ˜„ํ•œ ์ƒํƒœ์—์„œ "X"๋ฒ„ํŠผ์„ ๋งŒ๋“ค์–ด ์‚ญ์ œ๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ๋๋‹ค. ๊ฐ Cell์— ๊ตฌํ˜„๋œ "X"๋ฒ„ํŠผ์— ์•ก์…˜์„ ๋„ฃ๋Š” ๋ฐฉ๋ฒ•์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค. ์‚ญ์ œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ index๋ฅผ ๊ตฌํ•  ์ˆ˜ ์—†์—ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด DiffableDataSource๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. DiffableDataSource๋Š” indexPath๊ฐ€ ์•„๋‹Œ ์ง€์ •๋œ ํƒ€์ž…์œผ๋กœ ์•Œ๊ธฐ ๋•Œ๋ฌธ์— index๋ฅผ ์ด์šฉํ•˜๋Š” ๊ฒƒ์€ DiffableDataSource์˜ ํŠน์ง•์„ ์ด์šฉํ•˜์ง€ ๋ชปํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๊ฐ Cell์„ ์ง€์ •ํ•  ๋•Œ ํด๋กœ์ €๋ฅผ ์ด์šฉํ•˜์—ฌ Action์„ ๋„ฃ์–ด์ฃผ๊ธฐ๋กœ ํ–ˆ๋‹ค. ```swift let cell = UICollectionView.CellRegistration<ProductRegistCollectionViewCell, UIImage> { cell, indexPath, item in cell.removeImage = { self.deleteDataSource(image: item) } cell.configureImage(data: item) } ``` ์œ„ ์ฝ”๋“œ์™€ ๊ฐ™์ด ๊ฐ cell์— item์„ ์ง€์ •ํ•  ๋•Œ ๊ทธ item์„ dataSource์—์„œ ์ง€์šฐ๋Š” action์„ ํด๋กœ์ €๋กœ ์ด์šฉํ•˜์—ฌ ๋„˜๊ฒจ์ฃผ๊ฒŒ ๋œ๋‹ค. ```swift @objc private func didTapRemoveButton() { removeImage?() } ``` ์œ„ ์ฝ”๋“œ์™€ ๊ฐ™์ด ๊ฐ Cell์— ์ง€์ •๋œ "X"๋ฒ„ํŠผ action์— ํด๋กœ์ €๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์–ด Delete๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค. </details>