/ios-wanted-BoxOffice

๐ŸŽฌ ๋ฐ•์Šค์˜คํ”ผ์Šค ์ˆœ์œ„์™€ ์˜ํ™” ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๊ณ , ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ์•ฑ

Primary LanguageSwift

Swift 5.7 Xcode 14.1

BoxOffice

ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„: 2023.01.02 - 2023.01.06
์˜ํ™”์ง„ํฅ์œ„์›ํšŒ Open API, OMDb API, Firebase-FireStore ํ™œ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•˜๋Š” ์•ฑ

 

API Key

์•„๋ž˜์™€ ๊ฐ™์ด .plist ํŒŒ์ผ์„ ํ”„๋กœ์ ํŠธ ๊ฒฝ๋กœ์— ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.

 

๐Ÿ’ป ๊ฐœ๋ฐœ์ž ์†Œ๊ฐœ

์˜ˆํ†ค ์•„๋ฆฌ
- FirebaseManager ๊ตฌํ˜„
- ์˜ํ™” ์ƒ์„ธ์ •๋ณด ๊ตฌํ˜„
- ๋„คํŠธ์›Œํฌ ๋™์ž‘ ๊ตฌํ˜„
- ๋ฐ•์Šค์˜คํ”ผ์Šค ๋ชฉ๋ก ํ™”๋ฉด ๊ตฌํ˜„
- ๋ฆฌ๋ทฐ์ž‘์„ฑ ํ™”๋ฉด ๊ตฌํ˜„

 

๋ชฉ์ฐจ

๐Ÿ—‚ ํŒŒ์ผ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ

 BoxOffice
 โ”œโ”€โ”€ Resources
 โ”‚   โ”œโ”€โ”€ Assets.xcassets
 โ”‚   โ””โ”€โ”€ Base.lproj
 โ””โ”€โ”€ Sources
     โ”œโ”€โ”€ App
     โ”œโ”€โ”€ Extensions
     โ”‚   โ”œโ”€โ”€ Combine
     โ”‚   โ””โ”€โ”€ UI
     โ”œโ”€โ”€ Presentation
     โ”‚   โ”œโ”€โ”€ Coordinator
     โ”‚   โ”œโ”€โ”€ CreateReview
     โ”‚   โ”‚   โ”œโ”€โ”€ Coordinator
     โ”‚   โ”‚   โ”œโ”€โ”€ ViewController
     โ”‚   โ”‚   โ”œโ”€โ”€ ViewModel
     โ”‚   โ”‚   โ””โ”€โ”€ Views
     โ”‚   โ”œโ”€โ”€ Detail
     โ”‚   โ”‚   โ”œโ”€โ”€ ViewController
     โ”‚   โ”‚   โ”œโ”€โ”€ ViewModel
     โ”‚   โ”‚   โ””โ”€โ”€ Views
     โ”‚   โ””โ”€โ”€ List
     โ”‚       โ”œโ”€โ”€ Coordinator
     โ”‚       โ”œโ”€โ”€ ViewController
     โ”‚       โ”œโ”€โ”€ ViewModel
     โ”‚       โ””โ”€โ”€ Views
     โ””โ”€โ”€ Repositories
         โ”œโ”€โ”€ Model
         โ””โ”€โ”€ Network
             โ”œโ”€โ”€ Protocol
             โ”œโ”€โ”€ Request
             โ””โ”€โ”€ Response

 

๐Ÿ›  ๊ธฐ์ˆ  ์Šคํƒ

  • Swift/UIKit
  • Combine

์•„ํ‚คํ…์ฒ˜

  • MVVM
    • Input/Output Modeling
  • Coordinator

๐Ÿ“ฑ ๊ธฐ๋Šฅ ๋ฐ UI

๊ธฐ๋Šฅ/UI ์„ค๋ช…
๋ฐ•์Šค์˜คํ”ผ์Šค ๋ชฉ๋ก ์ผ๊ฐ„, ์ฃผ๊ฐ„, ์ฃผ๋ง๋ณ„๋กœ ๋ฐ•์Šค์˜คํ”ผ์Šค ์ˆœ์œ„๋ฅผ ๋ณด์—ฌ์ค€๋‹ค.
์˜ํ™” ์ƒ์„ธํ™”๋ฉด ๋ชฉ๋กํ™”๋ฉด์—์„œ ์„ ํƒํ•œ ์˜ํ™”์˜ ์ƒ์„ธํ•œ ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. ๊ณต์œ ๋ฒ„ํŠผ์„ ํ†ตํ•ด ์˜ํ™”์˜ ์ •๋ณด๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  '๋ฆฌ๋ทฐ์“ฐ๊ธฐ' ๋ฒ„ํŠผ์„ ํ†ตํ•ด ์˜ํ™”์— ๋Œ€ํ•œ ์ƒˆ๋กœ์šด ํ‰๊ฐ€๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ์ž‘์„ฑ๋˜์—ˆ๋˜ ๋ฆฌ๋ทฐ์˜ ๋ชฉ๋ก๋„ ๋ณด์—ฌ์ค€๋‹ค.
๋ฆฌ๋ทฐ ์“ฐ๊ธฐ ์ƒˆ๋กœ์šด ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ณ„๋ช…, ์•”ํ˜ธ, ๋ฆฌ๋ทฐ๋‚ด์šฉ, ๋ณ„์ , ํ”„๋กœํ•„ ์‚ฌ์ง„ ๋“ฑ์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด์žˆ๊ณ , ํ•„์ˆ˜ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๋ฆฌ๋ทฐ๋ฅผ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๐Ÿ’ป ์„ค๊ณ„ ๋ฐ ๊ตฌํ˜„

MVVM + Coordinator ๊ตฌ์กฐ

 

์—ญํ•  ๋ถ„๋ฐฐ

class/struct ์—ญํ• 
AppCoordinator ์•ฑ์˜ ๋ฃจํŠธ. ์ฒซ ํ™”๋ฉด์„ ์ค€๋น„ํ•˜๊ธฐ ์œ„ํ•œ ํƒ€์ž…
BoxOfficeListCoordinator ๋ชฉ๋ก ํ™”๋ฉด์˜ ํ™”๋ฉด ์ „ํ™˜์„ ๋‹ด๋‹นํ•˜๋Š” ํƒ€์ž…
BoxOfficeListViewController ๋ฐ•์Šค์˜คํ”ผ์Šค ์ˆœ์œ„๋ฅผ ์ผ๊ฐ„/์ฃผ๊ฐ„/์ฃผ๋ง ๋ณ„๋กœ ๋ณด์—ฌ์ค€๋‹ค.
MovieDetailViewController ์˜ํ™”์˜ ์ƒ์„ธํ•œ ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ค€๋‹ค.
CreateReviewCoordinator ๋ฆฌ๋ทฐ ์ž‘์„ฑ ํ™”๋ฉด์˜ ํ™”๋ฉด ์ „ํ™˜์„ ๋‹ด๋‹นํ•˜๋Š” ํƒ€์ž…
CreateReviewViewController ์˜ํ™”์— ๋Œ€ํ•œ ์ƒˆ๋กœ์šด ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ํ™”๋ฉด์ด๋‹ค.

 

Utilities

class/struct ์—ญํ• 
DefaultAPIProvider ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ํ•˜๋Š” ๊ธฐ๋ณธ ํƒ€์ž…์ด๋‹ค.
BoxOfficeRepository ๋„คํŠธ์›Œํฌ๋ฅผ ํ†ตํ•ด ๋ฐ›์•„์˜จ Response๋ฅผ ๋ทฐ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์ˆ˜์›”ํ•˜๋„๋ก ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ํƒ€์ž…์ด๋‹ค.
FirebaseManager ํŒŒ์ด์–ด๋ฒ ์ด์Šค๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ฆฌ๋ทฐ๋ฅผ ๋“ฑ๋ก, ์‚ญ์ œ, ๋ถˆ๋Ÿฌ์˜ค๊ธฐ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋Š” ํƒ€์ž…์ด๋‹ค.
ImageCacheManager ์ด๋ฏธ์ง€ ์บ์‹ฑ์„ ๋‹ด๋‹นํ•˜๋Š” ํƒ€์ž…์ด๋‹ค.

 

๐Ÿ‘€ ์‹คํ–‰ ํ™”๋ฉด

๐ŸŽฅ ๋ฐ•์Šค์˜คํ”ผ์Šค ๋žญํ‚น ํ™”๋ฉด

 

๐Ÿ“บ ์˜ํ™”์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ํ™”๋ฉด

 

๐Ÿ“ ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ํ™”๋ฉด

 

๐Ÿ’ช๐Ÿป ๊ธฐ์ˆ ์  ๋„์ „

Combine

์—ฐ์†๋œ escaping closure๋ฅผ ํ”ผํ•˜๊ณ , ์„ ์–ธํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ํ†ตํ•œ ๋†’์€ ๊ฐ€๋…์„ฑ๊ณผ ์˜คํผ๋ ˆ์ดํ„ฐ๋“ค์„ ํ†ตํ•œ ํšจ์œจ์ ์ธ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด์„œ Combine์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

Coordinator

ํ™”๋ฉด ์ „ํ™˜์— ๋Œ€ํ•œ ๋กœ์ง์„ ViewController๋กœ๋ถ€ํ„ฐ ๋ถ„๋ฆฌํ•˜๊ณ  ์˜์กด์„ฑ ๊ฐ์ฒด์— ๋Œ€ํ•œ ์ฃผ์ž…์„ ์™ธ๋ถ€์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด ์ฝ”๋””๋„ค์ดํ„ฐ๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

 

Firebase-FireStore

ํ•˜๋‚˜์˜ ์ฟผ๋ฆฌ์— ์ •๋ ฌ๊ณผ ํ•„ํ„ฐ๋ง ๋ชจ๋‘ ๊ฐ€๋Šฅํ•˜์—ฌ ๋ณตํ•ฉ์ ์ธ ์ฟผ๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ณ , ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž์ฃผ ์ฝํž ๋•Œ ์‚ฌ์šฉํ•˜๊ธฐ ์ข‹์€ FireStore ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

๐Ÿ”ฅ Trouble Shooting

PassthroughSubject๋กœ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋ƒˆ๋Š”๋ฐ, ์™œ ๋ทฐ๊ฐ€ ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌ๋ฐ›์ง€ ๋ชปํ•˜๋Š” ๊ฑธ๊นŒ?

  • ๋ฌธ์ œ์ƒํ™ฉ ๋ทฐ๋ชจ๋ธ์—์„œ PassthroughSubject ํƒ€์ž…์œผ๋กœ Publisher๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ , ๋ทฐ์—๊ฒŒ ์ „๋‹ฌํ•  ๋•Œ๋Š” AnyPublisher๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๊ตฌ๋…ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ send๋ฅผ ํ†ตํ•ด ์ƒˆ๋กœ์šด ๊ฐ’์„ ์ „๋‹ฌํ–ˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ , ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” ๋ทฐ์—์„œ ์ „ํ˜€ ์ „๋‹ฌ๋ฐ›์ง€ ๋ชปํ•˜๋Š” ์ƒํ™ฉ์ด์˜€๋‹ค.
  • ์ด์œ  PassthroughSubject์˜ ๊ฒฝ์šฐ ์ƒˆ๋กœ์šด ๊ฐ’์„ ์ „๋‹ฌํ•  ๋•Œ, ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” Subscriber๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐ’์„ ์ „๋‹ฌํ•˜์ง€ ์•Š๊ณ  ์‚ญ์ œํ•˜๊ธฐ ๋•Œ๋ฌธ์ด์˜€๋‹ค.
  • ํ•ด๊ฒฐ ๊ทธ๋ž˜์„œ PassthroughSubject ํƒ€์ž…์„ CurrentValueSubject๋กœ ๋ณ€๊ฒฝํ•ด์ฃผ์–ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. CurrentValueSubject๋Š” PassthroughSubject์™€ ๋‹ฌ๋ฆฌ ๊ฐ€์žฅ ์ตœ๊ทผ์— publish๋œ element์˜ ๋ฒ„ํผ๋ฅผ ์œ ์ง€ํ•œ๋‹ค.

 

๐Ÿ˜ตโ€๐Ÿ’ซ ๊ณ ๋ฏผํ–ˆ๋˜ ์ 

๐Ÿ‘ฉโ€๐Ÿ’ป ํ™”๋ฉด์„ ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑํ•˜๋ฉด ์ข‹์„๊นŒ?

ํ”„๋กœ์ ํŠธ ์š”๊ตฌ์‚ฌํ•ญ์—๋Š” ํ™”๋ฉด์„ ์ž์œ ๋กญ๊ฒŒ ๊ตฌ์„ฑ ๋ฐ ๋ฐฐ์น˜ํ•˜๋ผ๊ณ  ๋˜์–ด์žˆ์–ด์„œ ๋ง‰๋ง‰ํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ ํ•จ๊ป˜ ํ”„๋กœ์ ํŠธ ์š”๊ตฌ์‚ฌํ•ญ์„ ์ฒดํฌํ•ด์„œ ๊ตฌํ˜„ํ•ด์•ผํ•˜๋Š” ๊ธฐ๋Šฅ ๋ช…์„ธ๋ฅผ ์ •๋ฆฌํ•˜๊ณ , ํƒ€์‚ฌ ์•ฑ๋“ค์„ ์ฐธ๊ณ ํ•ด๊ฐ€๋ฉฐ Figma๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์–ด์„คํ”„์ง€๋งŒ[?] ์ตœ๋Œ€ํ•œ ํ•  ์ˆ˜ ์žˆ๋Š” ๋งŒํผ ์‹ค๋ ฅ์„ ๋ฐœํœ˜ํ•ด์„œ UI ๋ฐฐ์น˜๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋งˆ๋ฌด๋ฆฌํ–ˆ๋‹ค. ๊ณ ๋ฏผํ–ˆ๋˜ ๋ถ€๋ถ„์€...

  • ๋ณ„์ ์„ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ๋“ฑ๋กํ•˜๋Š” ๊ฒƒ์ด ํŽธํ• ๊นŒ?
  • ๋ฐ•์Šค์˜คํ”ผ์Šค ์ˆœ์œ„๋ฅผ ๋†’์€ ๊ฐ€๋…์„ฑ์œผ๋กœ ๊ตฌ์„ฑํ•˜๊ณ  ์‹ถ๋‹ค.
  • ํ”„๋กœํ•„ ์‚ฌ์ง„์˜ ๊ฒฝ์šฐ, ์–ด๋–ค ๋™์ž‘์œผ๋กœ ๋“ฑ๋ก์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋ฉด ์ข‹์„๊นŒ?

๋‹น์‹œ ๋งŒ๋“ค์—ˆ๋˜ ํ”„๋กœํ† ํƒ€์ž… ๋ฐ”๋กœ๊ฐ€๊ธฐ

 

๐Ÿ‘ฉโ€๐Ÿ’ป ๋ทฐ์™€ ๋ทฐ๋ชจ๋ธ์„ ์–ด๋–ป๊ฒŒ ๋‚˜๋ˆŒ๊นŒ?

๊ณ ๋ฏผํ–ˆ๋˜ ์ƒํ™ฉ์€ ์ž‘์„ฑ๋˜์–ด ์žˆ๋Š” ๋ฆฌ๋ทฐ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๊ทธ ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑํ•œ ์‚ฌ๋žŒ์ด ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๊ณ , ๊ทธ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ €์žฅ๋˜์–ด์žˆ๋˜ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ์ผ์น˜ํ•˜๋ฉด ์‚ญ์ œ๊ฐ€ ๋˜๊ณ , ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์•Œ๋ฆผ์ด ๋œจ๋„๋ก ํ•ด์•ผํ–ˆ๋‹ค.

๋งจ ์ฒ˜์Œ์— ์ƒ๊ฐํ•œ ๋ฐฉ์‹์€, ๋ทฐ์—์„œ ๋ทฐ๋ชจ๋ธ๋กœ ํ•ด๋‹น ์…€์˜ index๋ฅผ ์ „๋‹ฌํ•ด์ฃผ๋ฉด ๋ทฐ๋ชจ๋ธ์—์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•˜์—ฌ UIAlertController๊นŒ์ง€ ์ƒ์„ฑํ•˜์—ฌ ๋ทฐ๋กœ ์ „๋‹ฌํ•ด์ฃผ๋„๋ก ํ•˜๋Š” ๋ฐฉ์‹์ด์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ทฐ๋ชจ๋ธ์—์„œ UIAlertController๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์ „๋‹ฌํ•ด์ฃผ๋ ค๊ณ  ํ•˜๋‹ˆ UIKit์„ importํ•ด์•ผํ•˜๊ณ , ๋˜ ๋ทฐ๋ชจ๋ธ์˜ ์—ญํ• ๊ณผ ๋งž์ง€ ์•Š๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ๋˜์—ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์•„๋ž˜์™€ ๊ฐ™์€ ์ˆœ์„œ๋กœ ๋ทฐ์™€ ๋ทฐ๋ชจ๋ธ์˜ ์—ญํ• ์„ ๋‚˜๋ˆ„์–ด ๊ตฌํ˜„ํ•ด์ฃผ์—ˆ๋‹ค.

  1. view: ์–ผ๋Ÿฟ์„ ๋„์›Œ์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ๋ฐ›๋Š”๋‹ค.
  2. view: ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ํ›„ ์‚ฌ์šฉ์ž๊ฐ€ ํ™•์ธ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํ•ด๋‹น ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ viewModel์—๊ฒŒ ์ „๋‹ฌํ•ด์ค€๋‹ค.
  3. viewModel: ์ „๋‹ฌ๋ฐ›์€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ฒ€์‚ฌํ•˜๊ณ , ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ •์ƒ์ด๋ผ๋ฉด ๋ฆฌ๋ทฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด ์—๋Ÿฌ๋ฉ”์„ธ์ง€๋ฅผ view์—๊ฒŒ ์ „๋‹ฌํ•œ๋‹ค.
  4. view: ์—๋Ÿฌ๋ฉ”์„ธ์ง€๋ฅผ ์ „๋‹ฌ๋ฐ›๋Š”๋‹ค๋ฉด ์—๋Ÿฌ ์–ผ๋Ÿฟ์„ ๋„์šด๋‹ค.

์‹ค์ œ ์ฝ”๋“œ

// ViewModel

var _errorMessage = CurrentValueSubject<String?, Never>(nil) 
var errorMessage: AnyPublisher<String?, Never> { return _errorMessage.eraseToAnyPublisher() }

func checkPassword(_ inputPassword: String, index: Int) {
    // ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ์ƒํ™ฉ
    
    _errorMessage.send("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.") // _errorMessage์— string๊ฐ’ ๋ณด๋ƒ„
}
// ViewController

func bind() {
    viewModel.output.errorMessage // ๋ทฐ๋ชจ๋ธ์˜ errorMessage๋ฅผ ๊ตฌ๋…
        .compactMap { $0 }            
        .sinkOnMainThread(receiveValue: { [weak self] message in
                self?.showAlert(message: message)
        }) 
        .store(in: &cancelable)
}

์š”์•ฝํ•˜๋ฉด, _errorMessage์—๊ฒŒ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” string๊ฐ’์„ sendํ•˜๋ฉด _errorMessage๋ฅผ AnyPublisher๋กœ ๋ฐ”๊ฟ”์ฃผ๋Š” errorMessage๊ฐ€ ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์žˆ๊ฒŒ ๋œ๋‹ค. ๋”ฐ๋ผ์„œ ๋ทฐ์—์„œ๋Š” ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” errorMessage๋ฅผ sink(๊ตฌ๋…)ํ•˜์—ฌ ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ฒ˜๋ฆฌํ•ด์ค„ ๋กœ์ง์„ ๋‚ด๋ถ€์— ์ž‘์„ฑํ•ด๋‘๋ฉด ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

 

๐Ÿ‘ฉโ€๐Ÿ’ป ์…€์˜ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ์…€์˜ ์œ„์น˜๋ฅผ ์–ด๋–ป๊ฒŒ ์•Œ๊นŒ?

๋ฆฌ๋ทฐ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ํ•ด๋‹น ์…€์˜ ์œ„์น˜๋ฅผ ์•Œ์•„์•ผ ๋ฆฌ๋ทฐ๋ฅผ ๋‹ด๊ณ ์žˆ๋Š” ๋ฐ์ดํ„ฐ์˜ ์œ„์น˜ ๋˜ํ•œ ์•Œ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— index๋ฅผ ์•Œ์•„์•ผํ–ˆ๋‹ค. ์ฐพ์•„๋ณด๋‹ˆ ์…€์˜ ์œ„์น˜๋ฅผ ์•Œ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ UIGestureRecognizer๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ delegate๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ• ๋“ฑ๋“ฑ ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์ด ์žˆ์—ˆ๋Š”๋ฐ ์šฐ๋ฆฌ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

@objc func deleteButtonDidTap(_ sender: UIButton) {
    let buttonPosition: CGPoint = sender.convert(CGPoint.zero, to:self.tableView)
    let indexPath = self.tableView.indexPathForRow(at: buttonPosition)
}

deleteButtonDidTap ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์—์„œ ๋ฒ„ํŠผ์˜ ์œ„์น˜๋ฅผ ์žก๊ณ  tableView์˜ ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ํ•œ ๋‹ค์Œ ํ•ด๋‹น ์ขŒํ‘œ์—์„œ ํ–‰์˜ indexPath๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์—ˆ๋‹ค.