Skip to content

protocorn93/iOS-Architecture

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

14 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

[iOS] iOS Architecture

๊ฐœ์š”

์‚ฌ์‹ค ์•Œ๊ณ ์žˆ๊ณ  ์ง์ ‘ ์‚ฌ์šฉํ•˜๋Š” ์•„ํ‚คํ…์ณ๋Š” MVC๊ฐ€ ์ „๋ถ€์˜€์Šต๋‹ˆ๋‹ค. MVP, MVVM, VIPER ๋“ฑ๋“ฑ ์—ฌ๋Ÿฌ ์•„ํ‚คํ…์ณ์— ๋Œ€ํ•ด ๋“ค์–ด๋งŒ ๋ดค์„ ๋ฟ ์ง์ ‘ ๊ณต๋ถ€ํ•ด๋ณด๊ณ  ๋„์ž…ํ•ด๋ณด์ง€๋Š” ์•Š์•˜์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ Naver Hackday๋ฅผ ํ†ตํ•ด ํ”„๋กœ๊ทธ๋žจ ๊ตฌ์กฐ์˜ ์ค‘์š”์„ฑ์„ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ”„๋กœ๊ทธ๋žจ์— ์•Œ๋งž๋Š” ๊ตฌ์กฐ๋ฅผ ์ƒ๊ฐํ•˜๋Š” ํž˜์„ ๊ธฐ๋ฅด๊ธฐ ์œ„ํ•ด ์•„ํ‚คํ…์ณ์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ•ด๋ณด๊ณ  ์ด๋ฅผ ์ •๋ฆฌํ•ด๋ณด๋Š” ์‹œ๊ฐ„์„ ๊ฐ€์ง€๋ ค ํ•ฉ๋‹ˆ๋‹ค.

iOS Architecture Patterns ๊ธ€์„ ๋ฐ”ํƒ•์œผ๋กœ ์—ฌ๋Ÿฌ ๋ ˆํผ๋Ÿฐ์Šค๋“ค์„ ์ฐธ๊ณ ํ•˜๋ฉฐ ์ง€์†์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.


Architecture

์—ฌ๋Ÿฌ ์•„ํ‚คํ…์ณ ํŒจํ„ด์„ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๋“ค์–ด๊ฐ€๊ธฐ ์ „ ์ด๋Ÿฐ ์•„ํ‚คํ…์ณ๊ฐ€ ํ”„๋กœ๊ทธ๋žจ์„ ์ž‘์„ฑํ•˜๋Š”๋ฐ ํ•„์š”ํ•œ ์ด์œ ์™€ ๊ทธ ํšจ๊ณผ๋“ค์„ ๋จผ์ € ๊ณต๋ถ€ํ•ด๋ณด๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

๊ตฌ์กฐ๋ฅผ ์ƒ๊ฐํ•˜์ง€ ์•Š๊ณ  ํ”„๋กœ๊ทธ๋žจ์„ ์ž‘์„ฑํ•œ๋‹ค๊ณ  ํ”„๋กœ๊ทธ๋žจ์ด ๋Œ์•„๊ฐ€์ง€ ์•Š๋Š” ๊ฒƒ์€ ์•„๋‹™๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ํ”„๋กœ๊ทธ๋žจ์€ ๊ฐ€๋…์„ฑ์€ ๋–จ์–ด์ง€๋ฉฐ ์œ ์ง€ ๋ณด์ˆ˜์— ๊ต‰์žฅํžˆ ๋งŽ์€ ๋น„์šฉ์ด ๋“ญ๋‹ˆ๋‹ค. ๋˜ํ•œ ํ…Œ์ŠคํŒ… ๋‹จ๊ณ„์—์„œ๋Š” ํ…Œ์ŠคํŒ… ์ž์ฒด๊ฐ€ ๊ฑฐ์˜ ๋ถˆ๊ฐ€๋Šฅํ•˜๊ฑฐ๋‚˜ ํšจ๊ณผ๋ฅผ ๋ณผ ์ˆ˜ ์—†๋Š” ๋‚œ๊ด€์— ๋ด‰์ฐฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ˆœํžˆ ๋ชจ๋“ˆ(ํด๋ž˜์Šค)์„ ์—ญํ• ๋ณ„๋กœ ๋‚˜๋ˆ„์–ด ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์€ ์•„ํ‚คํ…์ณ๋ผ๊ณ  ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํŠน์ • ๊ธฐ์ค€์œผ๋กœ ์—ญํ• ์„ ์ •์˜ํ•˜๋ฉฐ ์ด๋ ‡๊ฒŒ ์—ญํ• ๋ณ„๋กœ ๋‚˜๋ˆ„์–ด์ง„ ๋ชจ๋“ˆ(ํด๋ž˜์Šค)๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ์œ ๊ธฐ์ ์œผ๋กœ ํ˜•์„ฑ์‹œํ‚ค๋Š” ๊ฒƒ์ด ์•„ํ‚คํ…์ณ๋ผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•„ํ‚คํ…์ณ์˜ ์ •๋‹ต์€ ์—†์Šต๋‹ˆ๋‹ค. ์•„ํ‚คํ…์ณ๋Š” ๊ฐ ํ”„๋กœ์ ํŠธ์˜ ์„ฑ๊ฒฉ์— ๋งž๊ฒŒ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ถ„๋ช… ์ข‹์€ ์•„ํ‚คํ…์ณ์˜ ๊ธฐ์ค€๊ณผ ํŠน์ง•์€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

  • Balanced Distribution : ๊ฐ์ฒด๋“ค์˜ ์—ญํ• ์ด ํ™•์‹คํ•˜๋ฉฐ ์ด๋Ÿฐ ์—ญํ• ๋“ค์ด ๊ท ํ˜•์žกํ˜€ ๋ถ„๋ฐฐ๋˜์–ด ์žˆ๋Š”์ง€, ์ฆ‰ ๊ฐ ๋ชจ๋“ˆ(ํด๋ž˜์Šค)์ด ๋…๋ฆฝ์ ์ธ์ง€
    • ์ด๋Ÿฌํ•œ ํ™•๊ณ ํ•œ ์—ญํ• ์˜ ๋ถ„๋ฐฐ๋Š” ํ”„๋กœ๊ทธ๋žจ์˜ ๋ณต์žก๋„๋ฅผ ๋‚ฎ์ถ˜๋‹ค.
    • ๊ฐ์ฒด์ง€ํ–ฅ์˜ 5์›์น™์ธ SOLID์˜ Single Responsibility์— ๊ธฐ๋ฐ˜
      • ํ•˜๋‚˜์˜ ๊ฐ์ฒด๋Š” ํ•˜๋‚˜์˜ ์—ญํ• ๋งŒ์„ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค๋Š” ์›์น™
    • ๋ชจ๋“ˆ(ํด๋ž˜์Šค)์˜ ๋…๋ฆฝ์„ฑ์ด ๋–จ์–ด์ง€๋ฉด ํ…Œ์ŠคํŒ…์„ ์ง„ํ–‰ํ•˜๋Š”๋ฐ ์–ด๋ ค์›€์ด ์žˆ๋‹ค.
  • Testability : ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š”์ง€
    • ํ…Œ์ŠคํŒ… ๊ณผ์ •์€ ๋Ÿฐํƒ€์ž„ ์ค‘ ๋ฐœ์ƒํ•˜๋Š” ์ด์Šˆ๋ฅผ ์‚ฌ์ „์— ์ฐพ์•„๋‚ด๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ๋‹จ๊ณ„
    • ํ…Œ์ŠคํŒ…์— ์žˆ์–ด์„œ ๊ทธ ์ž์ฒด๊ฐ€ ๋ฌธ์ œ๋ผ๊ธฐ๋ณด๋‹ค๋Š” ํ…Œ์ŠคํŒ…์„ ์ง„ํ–‰ํ•˜๋ ค๋Š” ์•„ํ‚คํ…์ณ๊ฐ€ ๋ฌธ์ œ์ธ ๊ฒฝ์šฐ ๋งŽ๋‹ค.
  • Easy of Use : ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์šด์ง€
    • ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์šด์ง€๋Š” ๊ฐœ๋ฐœ ์†๋„์™€ ๊ด€๊ณ„๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค.
  • Unidirectional Data Flow : ๋‹จ๋ฐฉํ–ฅ์„ฑ์˜ ๋ฐ์ดํ„ฐ ํ๋ฆ„
    • ๋‹จ์ˆœํ•œ ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„์€ ์ฝ”๋“œ๋ฅผ ์‰ฝ๊ฒŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋ฉฐ ์‰ฌ์šด ๋””๋ฒ„๊น…์„ ์ œ๊ณตํ•œ๋‹ค. ์—ฌ๋Ÿฌ ๊ฐ์ฒด๋“ค์„ ์˜ค๊ฐ€๋Š” ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„์€ ์˜ณ์ง€ ์•Š๋‹ค.
    • Shared Resource์˜ ์‚ฌ์šฉ๋„ ๊ธฐํ”ผํ•ด์•ผํ•œ๋‹ค. ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์›์ธ์„ ์ฐพ๊ธฐ ํž˜๋“ค์–ด์ง„๋‹ค.

๋ฌผ๋ก  ์ด ์„ธ ๊ฐ€์ง€์˜ ๊ธฐ์ค€์„ ์™„๋ฒฝํ•˜๊ฒŒ ์ถฉ์กฑ์‹œํ‚ค๋Š” ์•„ํ‚คํ…์ณ๋Š” ์—†์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์ง„ํ–‰ํ•˜๊ณ  ์žˆ๋Š” ํ”„๋กœ์ ํŠธ์˜ ์„ฑ๊ฒฉ์— ๋งž๊ฒŒ ์„ ํƒ์ ์œผ๋กœ ๋„์ž…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์œ„์˜ Distribution์€ ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋‚˜๋ˆ„์–ด ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.

  • Model : ํ”„๋กœ๊ทธ๋žจ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ์˜ ์กฐ์ž‘์ด ์ผ์–ด๋‚˜๊ณ  ์ด๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๋ถ€๋ถ„
  • View : ์‹œ๊ฐ์ ์ธ ๋ถ€๋ถ„์œผ๋กœ UI์— ํ•ด๋‹น. (iOS ํ™˜๊ฒฝ์—์„œ๋Š” 'UI' ์ ‘๋‘์–ด๊ฐ€ ๋ถ™์€ ๋ชจ๋“  ๊ฒƒ๋“ค์ด ์ด์— ํ•ด๋‹น)
  • Controller / Presenter / ViewModel : Model๊ณผ View ์‚ฌ์ด์˜ ์ค‘์žฌ์ž๋กœ ์ผ๋ฐ˜์ ์œผ๋กœ View๋ฅผ ํ†ตํ•ด ๋ฐœ์ƒํ•œ ์‚ฌ์šฉ์ž์˜ ์•ก์…˜์„ ๋‹ค๋ฃจ๋ฉฐ ํ•„์š”์‹œ ์ด์— ๋”ฐ๋ฅธ Model์— ๊ฐ’์˜ ์กฐ์ •์„ ์š”์ฒญํ•˜๋ฉฐ Model ๊ฐ’์˜ ๋ณ€ํ™”์— ๋งž๊ฒŒ View ๋ฅผ ๊ฐฑ์‹ ํ•˜๋Š” ์—ญํ• 

์ด๋ ‡๊ฒŒ ์„ธ ๊ฐ€์ง€์˜ ๊ธฐ์ค€์œผ๋กœ ๋‚˜๋ˆ„์–ด Distribution์„ ์ง„ํ–‰ํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด ์žฌ์‚ฌ์šฉ์„ฑ์ด ์ฆ๊ฐ€ํ•˜๋ฉฐ ๊ทธ๋“ค์„ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŒ…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ ์ด์ œ ๋ณธ์ น์ ์œผ๋กœ ๋งŽ์ด ์‚ฌ์šฉ๋˜๊ณ  ์œ ๋ช…ํ•œ ์•„ํ‚คํ…์ณ ํŒจํ„ด๋“ค์„ ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.


MVC

  • M : Model
  • V : View
  • C : Controller

๋จผ์ € ์‚ดํŽด๋ณผ ์•„ํ‚คํ…์ณ ํŒจํ„ด์€ ๋ฐ”๋กœ MVC์ž…๋‹ˆ๋‹ค. ๊ฐ€์žฅ ์œ ๋ช…ํ•˜๋ฉฐ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์‚ฌ์šฉ๋˜๋Š” ์•„ํ‚คํ…์ณ์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ์ €๋Š” ๋‘ ๊ฐ€์ง€์˜ MVC ์•„ํ‚คํ…์ณ๋ฅผ ์‚ดํŽด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค. ์ „ํ†ต์ ์ธ MVC ์•„ํ‚คํ…์ณ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

Traditional MVC

์œ„์˜ ๋‹ค์ด์–ด๊ทธ๋žจ์„ ํ†ตํ•ด ์šฐ๋ฆฌ๋Š” Model, View ๊ทธ๋ฆฌ๊ณ  Controller, ์ด ์„ธ ์š”์†Œ๊ฐ€ ์„œ๋กœ ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. View๋Š” ์‚ฌ์šฉ์ž์˜ ์•ก์…˜์„ Controller์—๊ฒŒ ์ „๋‹ฌํ•˜๊ณ  Controller๋Š” ์ด์— ๋”ฐ๋ฅธ ๋ฐ์ดํ„ฐ์˜ ๊ฐฑ์‹ ์„ Model์—๊ฒŒ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ Model์—์„œ ๋ฐ์ดํ„ฐ์˜ ๊ฐฑ์‹ ์ด ์ผ์–ด๋‚˜๊ณ  Model์€ ์ด๋Ÿฐ ์ƒํƒœ ๋ณ€ํ™”๋ฅผ View์—๊ฒŒ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ๋˜๋ฉด View ์—ญ์‹œ ๊ฐฑ์‹ ๋œ ๋ฐ์ดํ„ฐ์— ๋งž์ถ”์–ด ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋œ ์…‹์€ ๋…๋ฆฝ์„ฑ์ด ๋‚ฎ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋“ค ๊ฐ๊ฐ์˜ ์žฌ์‚ฌ์šฉ์„ฑ์€ ๊ต‰์žฅํžˆ ๋–จ์–ด์ง‘๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ํ˜„์žฌ iOS ๊ฐœ๋ฐœ์—๋Š” ์ „ํ†ต์ ์ธ MVC ์•„ํ‚คํ…์ณ๋Š” ๋งž์ง€ ์•Š๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์• ํ”Œ์—์„œ๋Š” ์ƒˆ๋กœ์šด MVC ์•„ํ‚คํ…์ณ๋ฅผ ์ œ์‹œํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

Apple's MVC

์• ํ”Œ์ด ์ œ์‹œํ•œ Cocoa MVC์—์„œ Controller๋Š” View์™€ Model์˜ ์ค‘์žฌ์ž๋กœ View์™€ Model์˜ ์ง์ ‘์ ์ธ ์—ฐ๊ฒฐ์„ ๋ง‰์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ „ํ†ต์ ์ธ MVC๋ณด๋‹ค ๋†’์€ ๋…๋ฆฝ์„ฑ์˜ ๋ณด์žฅ์„ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Ÿฌํ•œ ๊ธฐ๋Œ€๊ฐ€ ์‹ค์ œ ๊ฐœ๋ฐœ์— ํฐ ํšจ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ฌ๊นŒ์š”? ๋จผ์ € Cocoa MVC ํŒจํ„ด์˜ ๋‹ค์ด์–ด๊ทธ๋žจ์„ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์œ„์˜ ๋‹ค์ด์–ด๊ทธ๋žจ์„ ์–ผํ•๋ณด๋ฉด View์™€ Model์˜ ๋…๋ฆฝ์„ฑ์ด ๋ณด์žฅ๋˜๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ์‹ค์ œ ๊ฐœ๋ฐœ์€ ์–ด๋–ป๊ฒŒ ์ด๋ฃจ์–ด์งˆ๊นŒ์š”?

Cocoa MVC ์•„ํ‚คํ…์ณ์—์„œ Controller์˜ ์—ญํ• ์€ UIViewController๊ฐ€ ๋‹ด๋‹นํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  UIViewController๋Š” View๋ฅผ ์†Œ์œ ํ•˜๊ฒŒ ๋˜๊ณ  View๋“ค์˜ Lify Cycle๊ณผ ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— View์™€ Controller์˜ ๋ถ„๋ฆฌ๊ฐ€ ์‰ฝ์ง€ ์•Š์œผ๋ฉฐ Controller์˜ ์žฌ์‚ฌ์šฉ์ด ์–ด๋ ค์›Œ์ง€๊ณ  ์ด๋กœ์ธํ•ด ์—ฐ๊ด€๋˜์–ด ์žˆ๋Š” View์˜ ์žฌ์‚ฌ์šฉ ์—ญ์‹œ ์–ด๋ ค์›Œ์ง‘๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ View์™€ Controller๊ฐ€ ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๊ธฐ์— ํ…Œ์ŠคํŒ…์˜ ๊ณผ์ • ์—ญ์‹œ ๊ต‰์žฅํžˆ ํž˜๋“ค์–ด์ง‘๋‹ˆ๋‹ค. ๋…๋ฆฝ์ ์ด๋ผ๊ณ  ๋งํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์€ Model์ด ์ „๋ถ€์ž…๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  View ์œ„์—์„œ์˜ ์‚ฌ์šฉ์ž์˜ ์•ก์…˜๊ณผ ์ด์— ๋”ฐ๋ฅธ ๋ฉ”์†Œ๋“œ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ UIViewController์—์„œ ์ผ์–ด๋‚˜๋Š” ๊ฐ์ข… ํ–‰์œ„๋กœ (๋„คํŠธ์›Œํฌ ํ†ต์‹ , Delegation ๋“ฑ) Controller๋Š” ๋ฐฉ๋Œ€ํ•ด์ง€๊ณ  ์ด๋ฅผ ํ”ํžˆ Massive ViewController๋ผ๊ณ  ๋ถ€๋ฅด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์‹ค์ œ ๋‹ค์ด์–ด๊ทธ๋žจ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ๋ฆ„์„ ๊ฐ–๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ๋ฐฉ๋Œ€ํ•ด์ง„ UIViewController๋ฅผ ์ค„์ด๋Š” ํ–‰์œ„, View Controller Offloading์€ iOS ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ์ค‘์š”ํ•œ ๊ณผ์ œ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Cocoa MVC ์•„ํ‚คํ…์ณ๋ฅผ ๊ตฌํ˜„ํ•œ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

import UIKit
import PlaygroundSupport

struct Person { // Model
    let firstName:String
    let lastName:String
}

class GreetingViewController: UIViewController { // Controller

    var person:Person!

    // Views are belong to Controller => tightly COUPLED
    lazy var showGreetingButton: UIButton = {
        let button = UIButton()
        button.setTitle("Click me", for: .normal)
        button.setTitle("You badass", for: .highlighted)
        button.setTitleColor(UIColor.white, for: .normal)
        button.setTitleColor(UIColor.red, for: .highlighted)
        button.addTarget(self, action: #selector(didTapButton(sender:)), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    var greetingLabel: UILabel = {
        let label = UILabel()
        label.textColor = UIColor.white
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
        self.setupLayout()
    }

    // Layout codes in Controller
    func setupLayout() {
        self.setupButton()
        self.setupLabel()
    }

    private func setupButton() {
        self.view.addSubview(showGreetingButton)
        showGreetingButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        showGreetingButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
    }

    private func setupLabel() {
        self.view.addSubview(greetingLabel)
        greetingLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        greetingLabel.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -30).isActive = true
    }
    
    @objc func didTapButton(sender: UIButton) { // Update View
        self.greetingLabel.text = "Hello " + self.person.firstName + " " + self.person.lastName
    }
}

let model = Person(firstName: "Wasin", lastName: "Thonkaew")
let vc = GreetingViewController()
vc.person = model

PlaygroundPage.current.liveView = vc.view

์œ„์˜ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด View์˜ ์ƒ์„ฑ๊ณผ ๋ฐฐ์น˜์— ๊ด€๋ จ๋œ ์ฝ”๋“œ๋“ค๋„ Controller์•ˆ์— ์œ„์น˜ํ•˜๊ฒŒ ๋˜๊ณ  ์ด๋“ค์„ ๊ฐฑ์‹ ํ•˜๋Š” ์ฝ”๋“œ ์—ญ์‹œ Controller ์•ˆ์— ์œ„์น˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ฝ”๋“œ๋กœ๋งŒ ๋ณด์•„๋„ Controller์™€ View๊ฐ€ ๊ต‰์žฅํžˆ ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ View์˜ ํ…Œ์ŠคํŒ… ๊ณผ์ • ์—ญ์‹œ Controller์˜ View Life Cycle ๊ด€๋ จ ๋ฉ”์†Œ๋“œ(viewDidLoad, viewWillAppear ๋“ฑ)์˜ ํ˜ธ์ถœ์ด ์—†๋‹ค๋ฉด ์ง„ํ–‰ํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์—ญ์‹œ ์ด ๋‘˜์ด ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ ์—ฌ๊ธฐ์„œ MVC ์•„ํ‚คํ…์ณ๋Š” ์œ„์—์„œ ์–ธ๊ธ‰ํ–ˆ๋˜ ์ข‹์€ ์•„ํ‚คํ…์ณ์˜ ๊ธฐ์ค€๋“ค์— ์–ผ๋งˆ๋‚˜ ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • Distribution : View์™€ Model์€ ํ™•์‹คํžˆ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ View์™€ Controller๋Š” ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • Testability : View์™€ Controller๊ฐ€ ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์˜ค๋กœ์ง€ Model๋งŒ ํ…Œ์ŠคํŒ…์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Easy of Use : ์—ฌ๋Ÿฌ ์•„ํ‚คํ…์ณ ์ค‘ ๊ฐ€์žฅ ์ ์€ ์ฝ”๋“œ๋ฅผ ํ•„์š”๋กœ ํ•˜๋ฉฐ ๊ฐ€์žฅ ์นœ์ˆ™ํ•œ ์•„ํ‚คํ…์ณ ํŒจํ„ด์œผ๋กœ ๋งŽ์€ ๊ฒฝํ—˜์ด ์—†๋Š” ๊ฐœ๋ฐœ์ž๋“ค๋„ ์‰ฝ๊ฒŒ ์œ ์ง€ ๋ณด์ˆ˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐœ๋ฐœ ์ง„ํ–‰ ์†๋„์— ์žˆ์–ด์„œ๋Š” ๊ฐ€์žฅ ๋น ๋ฅธ ์•„ํ‚คํ…์ณ ํŒจํ„ด์ด๋ผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

iOS ๊ฐœ๋ฐœ์— ์žˆ์–ด์„œ ์•„ํ‚คํ…์ณ์— ํฌ๊ฒŒ ์‹ ๊ฒฝ์„ ์“ธ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ์ง€์‹์ด ์ „๋ฌดํ•˜๋‹ค๋ฉด ๊ฐ€์žฅ ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์šด ํŒจํ„ด์ด ๋ฐ”๋กœ MVC์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Š” ์•„์ฃผ ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ผ ํ•˜๋”๋ผ๋„ ๋งŽ์€ ์œ ์ง€ ๋ณด์ˆ˜ ๋น„์šฉ์ด ๋“ค์–ด๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


MVP

  • M : Model
  • V : View (UIView ๊ทธ๋ฆฌ๊ณ /ํ˜น์€ UIViewController)
  • P : Presenter

๋จผ์ € ๋‹ค์ด์–ด๊ทธ๋žจ์„ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์œ„์—์„œ ์‚ดํŽด๋ณธ Cocoa MVC์™€ ๊ต‰์žฅํžˆ ๋น„์Šทํ•œ ๋ชจ์Šต์„ ํ•˜๊ณ ์žˆ๋Š” ๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์‹ค์ œ๋กœ๋„ Cocoa MVC์™€ ์œ ์‚ฌํ• ๊นŒ์š”? ์ „ํ˜€ ๊ทธ๋ ‡์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋จผ์ € MVC์™€๋Š” ๋‹ค๋ฅด๊ฒŒ UIView๋‚˜ UIViewController ๋‘˜ ๋ชจ๋‘ View์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค. Cocoa MVC์—์„œ UIViewController๋Š” Controller์— ํ•ด๋‹นํ–ˆ์—ˆ๊ณ  ๊ทธ๋กœ์ธํ•ด View์™€ ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋‘˜์„ View๋กœ ๋ถ„๋ฅ˜ํ•˜๋Š” ๋Œ€์‹  MVP ํŒจํ„ด์—์„œ๋Š” Presenter๋ผ๋Š” ๊ฒƒ์ด ๋“ฑ์žฅํ•ฉ๋‹ˆ๋‹ค.

Presenter๋Š” Cocoa MVC์™€๋Š” ๋‹ค๋ฅด๊ฒŒ View(UIView, UIViewController)์˜ Life Cycle์— ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š๊ณ  ๋ ˆ์ด์•„์›ƒ ์ฝ”๋“œ ์—ญ์‹œ Presenter์— ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ณด๋‹ค Controller์˜ ์—ญํ• ๋‹ต๊ฒŒ View๋ฅผ ๋ฐ์ดํ„ฐ์™€ ์ƒํƒœ์— ๋งž์ถ”์–ด ๊ฐฑ์‹ ํ•˜๋Š” ์—ญํ• ์„ ๊ฐ–๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰ Presenter๋Š” Model๋กœ ๋ถ€ํ„ฐ ๊ฐฑ์‹ ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์™€ ๋ทฐ๋ฅผ ๊ฐฑ์‹ ํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

์œ„์—์„œ ์–ธ๊ธ‰ํ–ˆ๋“ฏ์ด Cocoa MVC์™€ ๋‹ค๋ฅด๊ฒŒ MVP ํŒจํ„ด์—์„œ UIViewController์™€ ์ด๋ฅผ ์ƒ์†๋ฐ›๋Š” ํด๋ž˜์Šค๋“ค์€ Presenter(Controller)๊ฐ€ ์•„๋‹ˆ๋ผ View์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋ณด๋‹ค ํ…Œ์ŠคํŒ…์˜ ํšจ๊ณผ๋ฅผ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ๋กœ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

import UIKit
import PlaygroundSupport

struct Person { // Model
    let firstName:String
    let lastName:String
}

protocol GreetingView:class { // View Protocol
    func setGreeting(greeting:String)
}

protocol GreetingViewPresenter { // Presenter Protocol
    init(view: GreetingView, person: Person)
    func showGreeting()
}

class GreetingPresenter : GreetingViewPresenter { // Presenter
    weak var view: GreetingView?
    let person: Person

    required init(view: GreetingView, person: Person) {
        self.view = view
        self.person = person
    }
    // 3.
    func showGreeting() { // Update View
        let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
        self.view?.setGreeting(greeting: greeting)
    }
}

class GreetingViewController : UIViewController, GreetingView { // View
    var presenter: GreetingViewPresenter!
    ...
    // Properties
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
        setupLayout()
        self.showGreetingButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
    }
    ...
    // Layout Code
    // 2. 
    @objc func didTapButton(button: UIButton) {
        self.presenter.showGreeting()  // Send Action to Presenter
    }
    // 1.
    func setGreeting(greeting: String) {
        self.greetingLabel.text = greeting
    }
    // layout code goes here
}
// Present the view controller in the Live View window
// Assembling of MVP
let model = Person(firstName: "Wasin", lastName: "Thonkaew")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter

PlaygroundPage.current.liveView = view

๋‹ค์ด์–ด๊ทธ๋žจ๊ณผ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ์‚ดํŽด๋ณด๊ณ  ๊ฐ€์•ผํ•  ๋ช‡ ๊ฐ€์ง€๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

๋จผ์ € View๋Š” Presenter๋ฅผ ์†Œ์œ ํ•˜๊ณ  ์žˆ์–ด์•ผ ํ•˜๋ฉฐ Presenter๋Š” ์œ ์ € ์•ก์…˜, ๋ฐ์ดํ„ฐ ๊ฐฑ์‹ , ์ƒํƒœ ๊ฐฑ์‹ ์— ๋”ฐ๋ผ View๋ฅผ ๊ฐฑ์‹ ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์ฝ”๋“œ๋กœ์จ ๊ตฌํ˜„ํ•  ๋•Œ View๋Š” Presenter๋ฅผ ๊ฐ•ํ•œ ์ฐธ์กฐ๋กœ ์†Œ์œ ํ•˜๊ณ  ์žˆ๊ณ  Presenter๋Š” ์•ฝํ•œ ์ฐธ์กฐ๋กœ View๋ฅผ ๋‹จ์ˆœํžˆ ๊ฐ€๋ฆฌํ‚ค๊ณ ๋งŒ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— View์˜ Life Cycle์˜ ์˜ํ–ฅ๊ณผ ๋ ˆ์ด์•„์›ƒ ์ฝ”๋“œ์™€ ์•ก์…˜ ์ฝ”๋“œ๊ฐ€ ๊ณต์กดํ•˜๋Š” ๋“ฑ์˜ ์˜์กด์„ฑ์—์„œ๋Š” ๋ฒ—์–ด๋‚  ์ˆ˜ ์žˆ์ง€๋งŒ ์ฐธ์กฐ์— ์˜ํ•œ 1:1 ์˜์กด์„ฑ์—์„œ๋Š” ๋ฒ—์–ด๋‚  ์ˆ˜ ์—†๋‹ค๋Š” ํ•œ๊ณ„๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ์œผ๋กœ๋Š” GreetingViewController์„ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด ๊ณณ์—๋Š” ๋ ˆ์ด์•„์›ƒ๊ณผ ์œ ์ €์˜ ์•ก์…˜์„ ์ „๋‹ฌํ•˜๋Š” ์ฝ”๋“œ๋งŒ์ด ์œ„์น˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ํ๋ฆ„์„ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  1. ํ”„๋กœํ† ์ฝœ ๋ฉ”์†Œ๋“œ๋กœ ๋ทฐ๋ฅผ ๊ฐฑ์‹ ํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ์ •์˜ (ํ˜ธ์ถœ์ด ์•„๋‹˜์„ ๋ช…์‹ฌํ•˜์ž.)
  2. View ์œ„์— ์กด์žฌํ•˜๋Š” ๋ฒ„ํŠผ์— .touchUpInside ์•ก์…˜์ด ๋“ค์–ด์˜ค๋ฉด View๋Š” didTapButton ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด Presenter์— ์ด๋Ÿฌํ•œ ์‚ฌ์‹ค์„ ์•Œ๋ฆฝ๋‹ˆ๋‹ค.
  3. Presenter๋Š” ์œ ์ €์˜ ์•ก์…˜์— ๋Œ€ํ•ด Model๋กœ๋ถ€ํ„ฐ ๊ฐ’์„ ๊ฐ€์ ธ์™€ ๋ทฐ๋ฅผ ๊ฐฑ์‹œํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœ (ํ˜ธ์ถœ์ด๋ผ๋Š” ํ–‰์œ„๋Š” Presenter์— ์˜ํ•ด ํ–‰ํ•ด์ง„๋‹ค.)

MVC์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ MVP๋Š” ์ข‹์€ ์•„ํ‚คํ…์ณ์˜ ๊ธฐ์ค€์— ์–ผ๋งˆ๋‚˜ ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  • Distribution : ์ „ํ†ต์ ์ธ MVC์—์„œ ๋ฐœ์ƒํ•œ Model๊ณผ View์˜ ์˜์กด์„ฑ ๋ฌธ์ œ๋Š” ํ•ด๊ฒฐํ•˜์˜€๋‹ค. ํ•˜์ง€๋งŒ ์ฐธ์กฐ์— ์˜ํ•œ View์™€ Controller์˜ ์˜์กด์„ฑ์€ ์กด์žฌํ•˜์ง€๋งŒ ๋น„๊ต์  ์…‹ ๋ชจ๋‘ ์—ญํ• ๋ณ„๋กœ ์ ์ ˆํžˆ ๋‚˜๋ˆ„์–ด์ ธ ์žˆ๋‹ค๊ณ  ๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Testability : ๊ฐ๊ฐ์˜ ์š”์†Œ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŒ…ํ•˜๊ธฐ ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค.
  • Easy of Use : Presenter์˜ ์ถ”๊ฐ€์™€ ์ด๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•œ ํ”„๋กœํ† ์ฝœ๋“ฑ์˜ ์ถ”๊ฐ€๋กœ ์ฝ”๋“œ๊ฐ€ MVC๋ณด๋‹ค ๊ธธ์–ด์ง‘๋‹ˆ๋‹ค.

MVVM

MVVM ํŒจํ„ด์€ RxSwift์— ๋Œ€ํ•œ ๊ฒฝํ—˜์ด ์—†๋Š” ๊ด€๊ณ„๋กœ ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  MVVM์„ ์†Œ๊ฐœํ•˜๊ณ  ์žˆ๋Š” How not to get desperate with MVVM implementation์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • M : Model
  • V : View
  • VM : ViewModel

๋จผ์ € ๋‹ค์ด์–ด๊ทธ๋žจ์„ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

MVVM์˜ ์ •์˜์— ์˜ํ•˜๋ฉด View๋Š” ์˜ค์ง ์‹œ๊ฐ์ ์ธ ์š”์†Œ๋กœ๋งŒ ์ด๋ฃจ์–ด์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค. View์—์„œ๋Š” ๋ ˆ์ด์•„์›ƒ, ์• ๋‹ˆ๋งค์ด์…˜ ๊ทธ๋ฆฌ๊ณ  UI ์š”์†Œ๋“ค์— ๋Œ€ํ•œ ์ดˆ๊ธฐํ™” ์ž‘์—… ์ฝ”๋“œ๋“ค๋งŒ์ด ์œ„์น˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. MVVM์—์„œ View์™€ Model ์‚ฌ์ด์— ViewModel์ด ์œ„์น˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ViewModel์€ View์˜ ๊ฐ UI ์š”์†Œ๋“ค์— ๋Œ€ํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. View์˜ UI ์š”์†Œ๋“ค๊ณผ ViewModel์˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์—ฐ๊ฒฐ์‹œํ‚ค๋Š” ์ž‘์—…์„ "๋ฐ”์ธ๋”ฉ(Binding)" ์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

MVVM์—์„œ View์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ViewModel์— ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉฐ ์ด์— ๋งž์ถฐ View๊ฐ€ ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด Date ๋ฅผ String ์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ž‘์—…์€ ViewModel์—์„œ ์ง„ํ–‰๋˜๊ณ  View์—์„œ๋Š” ์ด์— ๋งž์ถฐ ๊ฐฑ์‹ ๋งŒ ์ผ์–ด๋‚˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— View๊ฐ€ ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋Š”์ง€์™€ ์ƒ๊ด€์—†์ด View์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๋Œ€ํ•ด์„œ ํ…Œ์ŠคํŒ…์ด ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค.

์ „์ฒด์ ์ธ ํ๋ฆ„์œผ๋กœ ๋ณด์•˜์„ ๋•Œ ViewModel์€ View๋กœ๋ถ€ํ„ฐ ์‚ฌ์šฉ์ž์˜ ์•ก์…˜์„ ๋ฐ›์•„์˜ค๊ณ  Model๋กœ๋ถ€ํ„ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์™€ ์ด๋ ‡๊ฒŒ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ View์—์„œ ๋ณด์—ฌ์ค„ ๊ฐ’(Ready-To-Display Property)์œผ๋กœ ๊ฐ€๊ณต์„ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ์™€ ๋™์‹œ์— View๋Š” ViewModel์˜ ์ด๋Ÿฌํ•œ Ready-To-Display Property ๊ฐ’์„ observingํ•˜๊ณ  ์žˆ์–ด ๊ฐ’์ด ๊ฐฑ์‹ ๋˜๋ฉด ์ด์— ๋งž์ถฐ View๋ฅผ ๊ฐฑ์‹ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

MVP์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ UIView์™€ UIViewController๋ฅผ View๋กœ ๋ฌถ์–ด ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— View์—์„œ๋Š” ๋‹ค์Œ์˜ ์ž‘์—…๋“ค๋งŒ ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

  1. Initiate/Layout/Present UI components.
  2. Bind UI components with the ViewModel.

๊ทธ๋ฆฌ๊ณ  ViewModel์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ž‘์—…์„ ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

  1. Write controller logics such as pagination, error handling, etc.
  2. Write presentational logic, provide interfaces to the View.

๊ทธ๋Ÿผ ์ด์ œ ์ด๋ฅผ ๊ตฌํ˜„ํ•œ ์ฝ”๋“œ๋กœ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ๋กœ ๋ฐ”๋กœ MVVM ์•„ํ‚คํ…์ณ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด๋Š” ๊ฒƒ์ด ์•„๋‹Œ MVC ์•„ํ‚คํ…์ณ๋กœ ๋งŒ๋“ค์–ด์ง„ ํ”„๋กœ์ ํŠธ๋ฅผ MVVM์œผ๋กœ ๊ณ ์ณ๊ฐ€๋ฉฐ ํ•˜๋‚˜ํ•˜๋‚˜ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋งŒ๋“ค์–ด ๋ณผ ์˜ˆ์ œ๋Š” ๊ธฐ๋ณธ์ ์ธ ํ…Œ์ด๋ธ” ๋ทฐ์™€ ๊ทธ ์…€์˜ ์„ธ๊ทธ๋กœ ์—ฐ๊ฒฐ๋˜๋Š” ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋กœ ๋„˜์–ด๊ฐ€๋Š” ์ •๋„์˜ ๊ฐ„๋‹จํ•œ ์ˆ˜์ค€์ž…๋‹ˆ๋‹ค.

์•ฑ์˜ ์™„์„ฑ๋œ ๊ฒฐ๊ณผ๋ฅผ ๋งํฌ๋ฅผ ํ†ตํ•ด ๋จผ์ € ํ™•์ธํ•ด์ฃผ์„ธ์š”.

MVC version

๋จผ์ € MVC ์•„ํ‚คํ…์ณ๋กœ ๊ตฌํ˜„ํ•œ ๋ช‡๋ช‡ ์ฝ”๋“œ๋“ค์„ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด ์ฝ”๋“œ๋“ค์€ ์ƒ๋‹นํžˆ ๋‚ฏ์— ์ต์„ ๊ฒƒ์ด๋ผ๊ณ  ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค! (์ €๋„ ๊ทธ๋žฌ๊ฑฐ๋“ ์š”!)

์˜ˆ์ œ์—์„œ Model์„ ๋‹ด๋‹นํ•˜๋Š” Photo ๊ตฌ์กฐ์ฒด์ž…๋‹ˆ๋‹ค.

struct Photo {
    let id: Int
    let name: String
    let description: String?
    let created_at: Date
    let image_url: String
    let for_sale: Bool
    let camera: String?
}

Model์„ ์ฑ„์›Œ์ค„ ๋ฐ์ดํ„ฐ๋Š” ์˜ˆ์ œ ๋‚ด์˜ APIService๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ›์•„์™€ ํ…Œ์ด๋ธ” ๋ทฐ์— ๋ฟŒ๋ ค์ฃผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค. ํŒจ์น˜์˜ ํ–‰์œ„๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ํ…Œ์ด๋ธ” ๋ทฐ๋ฅผ reloadData() ํ•ด์คŒ์œผ๋กœ์จ ์…€์„ ๋ฐ์ดํ„ฐ์— ๋งž์ถ”์–ด ๊ฐฑ์‹ ํ•ด์ฃผ๋Š” ์ž‘์—…์ž…๋‹ˆ๋‹ค.

self?.activityIndicator.startAnimating()
self.tableView.alpha = 0.0
apiService.fetchPopularPhoto { [weak self] (success, photos, error) in  DispatchQueue.main.async {
    self?.photos = photos
    self?.activityIndicator.stopAnimating()
    self?.tableView.alpha = 1.0
    self?.tableView.reloadData()
  }
}

๊ทธ๋ฆฌ๊ณ  UITableViewDataSource ํ”„๋กœํ† ์ฝœ ๋ฉ”์†Œ๋“œ ์—ญ์‹œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ชจ์Šต์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // ....................
    let photo = self.photos[indexPath.row]
    //Wrap the date
    let dateFormateer = DateFormatter()
    dateFormateer.dateFormat = "yyyy-MM-dd"
    cell.dateLabel.text = dateFormateer.string(from: photo.created_at)
    //.....................
}
  
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.photos.count
}

์œ„์˜ ๋ฉ”์†Œ๋“œ๋Š” ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ •์˜ํ•ด์ฃผ์—ˆ๊ณ  ํ™”๋ฉด์— ๋ฟŒ๋ ค์ฃผ๊ณ  ์ด๋ฅผ ๊ฐ€๊ณตํ•˜๋Š” ์ž‘์—…๊นŒ์ง€ ๋ชจ๋‘ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ง„ํ–‰๋˜๊ณ  ์žˆ๋Š”๊ฑธ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งˆ์ง€๋ง‰์œผ๋กœ ๋‹ค์Œ์€ UITableViewDelegate ํ”„๋กœํ† ์ฝœ ๋ฉ”์†Œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    let photo = self.photos[indexPath.row]
    if photo.for_sale { // If item is for sale 
        self.selectedIndexPath = indexPath
        return indexPath
    }else { // If item is not for sale 
        let alert = UIAlertController(title: "Not for sale", message: "This item is not for sale", preferredStyle: .alert)
        alert.addAction( UIAlertAction(title: "Ok", style: .cancel, handler: nil))
        self.present(alert, animated: true, completion: nil)
        return nil
    }
}

์…€์„ ์„ ํƒํ•œ ์‚ฌ์šฉ์ž์˜ ์•ก์…˜์„ ๋ฐ›๊ณ  ์„ ํƒํ•œ ์…€์— ๋”ฐ๋ผ alert๋ฅผ ๋„์–ด์ค„์ง€ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋กœ ๋„˜์–ด๊ฐˆ ๊ฒƒ์ธ์ง€๋ฅผ ๊ฒฐ์ •ํ•˜๊ณ  ์‹คํ–‰ํ•˜๋Š” ์—ญํ• ๊นŒ์ง€ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ์˜ ํ•˜๋‚˜์˜ ๋ฉ”์†Œ๋“œ์•ˆ์—์„œ ์ง„ํ–‰๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ฌธ์„œ๋ฅผ ์œ„์—์„œ๋ถ€ํ„ฐ ์ฝ์–ด์˜ค์…จ๋‹ค๋ฉด ๋ฌด์–ธ๊ฐ€ ๋„ˆ๋ฌด ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋Š๋ผ์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์œ„์˜ ์ฝ”๋“œ๋“ค์„ ๊ฐ„๋žตํ•˜๊ฒŒ ์†Œ๊ฐœํ•˜๋Š” ๋ถ€๋ถ„์—์„œ ์–ธ๊ธ‰ํ•œ ๊ฒƒ๋“ค๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋Š” APIService์— ๋Œ€ํ•ด ์˜์กด์„ฑ ๋ฌธ์ œ๋ฅผ ๊ฐ–๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ๋งŽ์€ ๊ฒƒ๋“ค์ด ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ๋‚ด์—์„œ ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๊ณ  ์˜์กด์„ฑ์ด ์กด์žฌํ•œ๋‹ค๋ฉด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ๊ฐ€ ๋งค์šฐ ๊นŒ๋‹ค๋กœ์›Œ์งˆ ๊ฒƒ์ด๊ณ  ์›ํ•˜๋Š” ํ…Œ์ŠคํŒ… ์„ฑ๋Šฅ์„ ๋ฝ‘์•„๋‚ผ ์ˆ˜ ์—†์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ์ด์ œ ์ด๋“ค์„ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ณด๋‹ค ํ…Œ์ŠคํŒ…์— ์šฉ์ดํ•  ์ˆ˜ ์žˆ๋Š” MVVM ์•„ํ‚คํ…์ณ๋กœ ์ˆ˜์ •ํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

MVVM version

์œ„์˜ ๋ฌธ์ œ์ ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๊ฐ€์žฅ ๋จผ์ € ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ์˜ ๋ถ€๋‹ด์„ ์ค„์—ฌ์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ๋จผ์ € ์˜ˆ์ œ์—์„œ ํ•„์š”ํ•œ UI ์š”์†Œ๋“ค์„ ์‚ดํŽด๋ณด๊ณ  ๊ทธ๋“ค์„ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๋ ˆ์ด์•„์›ƒ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์ด ์˜ˆ์ œ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ธ ๊ฐ€์ง€์˜ UI ์š”์†Œ๊ฐ€ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

  1. activityIndicator (Loading / Finish)
  2. tableView (Show / Hide)
  3. cells (title, description. created date)

์ด๋“ค์„ View์™€ ViewModel๋กœ ๋‚˜๋ˆˆ ๊ฒƒ์„ ์ถ”์ƒํ™”ํ•œ๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹ค์ด์–ด๊ทธ๋žจ์œผ๋กœ ํ‘œํ˜„๋  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ฐ๊ฐ์˜ UI ์š”์†Œ๋Š” ViewModel์˜ ํ”„๋กœํผํ‹ฐ์— ์ผ๋Œ€์ผ ๋Œ€์‘ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ์ด๋Ÿฐ ๋ฐ”์ธ๋”ฉ์„ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ์š”? ์Šค์œ„ํ”„ํŠธ์—์„œ๋Š” ์ด๋Ÿฌํ•œ ์ž‘์—…์„ ๋‹ค์Œ์˜ ๋ฐฉ๋ฒ•๋“ค๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. KVO (Key-Value Observing) ํŒจํ„ด
  2. RxSwift๋‚˜ ReactiveCocoa๊ฐ™์€ FRP(Functional Reactive Programming) ๋ผ์ด๋ธŒ๋Ÿฌ๋ฅผ ํ™œ์šฉ.
  3. Delegation
  4. Property Observer

์ €๋Š” ์ฐธ๊ณ ํ•˜๊ณ  ์žˆ๋Š” ๋ธ”๋กœ๊ทธ์˜ ๊ธ€์„ ๋”ฐ๋ผ Property Observer์™€ Closure๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ๋ชจ์–‘์ƒˆ์™€ ์‚ฌ์šฉ ์šฉ๋„๋งŒ์„ ์ฝ”๋“œ๋กœ ๊ฐ„๋‹จํžˆ ์‚ดํŽด๋ณด์ž๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

ViewModel

var prop: T {
    didSet{ // Property Observer
        self.propChanged?()
    }
}

View

viewModel.propChanged = { [weak self] in
  DispatchQueue.main.async {
      // View์˜ ์—…๋ฐ์ดํŠธ ์ž‘์—….
  }
}

View์—์„œ ViewModel์˜ ๋ฐ”์ธ๋”ฉ Closure๋“ค์„ ๊ตฌํ˜„ํ•ด์คŒ์œผ๋กœ์จ View์˜ ๊ฐฑ์‹ ์— ๋Œ€ํ•œ ์ฝ”๋“œ๋ฅผ ์ •์˜ํ•ด์ฃผ๊ณ  ๊ฐ’์˜ ๋ณ€ํ™”์— ๋”ฐ๋ฅธ ๋ทฐ ๊ฐฑ์‹ ์„ ํ˜ธ์ถœํ•˜๋Š” ํ–‰์œ„๋Š” ViewModel์— ์œ„์น˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰ ๋ฐ์ดํ„ฐ์— ๋”ฐ๋ผ ๋ทฐ์˜ ๊ฐฑ์‹ ์„ ๋ช…๋ นํ•˜๋Š” ํ–‰์œ„๋Š” ViewModel์—์„œ ์ด๋ฃจ์–ด์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ๋ฐ”์ธ๋”ฉ ๊ณผ์ •์„ ํ†ตํ•˜๋ฉด ViewModel์€ MVP์˜ Presenter์—์„œ ํ”„๋กœํ† ์ฝœ์˜ ํ˜•ํƒœ๋กœ๋ผ๋„ View์˜ ์กด์žฌ๋ฅผ ์•Œ๋˜ ๊ฒƒ๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ ์ „ํ˜€ View์— ๋Œ€ํ•œ ์–ด๋– ํ•œ ์ฐธ์กฐ๋„ ์กด์žฌํ•˜์ง€ ์•Š๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ์ œ์˜ ์ „์ฒด ์ฝ”๋“œ๋Š” ํ˜„์žฌ ๋ฌธ์„œ์™€ ๋™์ผํ•œ ๋ ˆํฌ์ง€ํ„ฐ๋ฆฌ์— ์žˆ์œผ๋ฏ€๋กœ ํ•ด๋‹น ํด๋”๋ฅผ ํ™•์ธํ•ด์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„  ์œ„์™€ ๊ฐ™์€ ๋ฐฉ์‹์˜ ์ฝ”๋“œ๊ฐ€ ์‹ค์ œ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ๋Š”์ง€๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ํ…Œ์ด๋ธ” ๋ทฐ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ฟŒ๋ ค์ฃผ๊ธฐ ์œ„ํ•œ ๋ฐ”์ธ๋”ฉ Closure์™€ ํ˜ธ์ถœ์„ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

ViewModel

let apiService: APIServiceProtocol

//MARK: Initializer
init( apiService: APIServiceProtocol = APIService()) {
    self.apiService = apiService
}

...
// Activity Indicator
var isLoading: Bool = false {
    didSet{
        // notify
        self.updateLoadingStatus?()
    }
}
// Table View
private var cellViewModels:[PhotoListCellViewModel] = [PhotoListCellViewModel]() {
    didSet{
        // notify
        self.reloadTableViewClosure?()
    }
}
// Number of cells
var numberOfCells: Int {
    return cellViewModels.count
}

//MARK: Binding Closures
var reloadTableViewClosure: (()->())?
var updateLoadingStatus: (()->())?
...

// Request Data
func requestFetchData(){
    self.isLoading = true // trigger activity indicator startAnimating
    apiService.fetchPopularPhoto { [weak self] (success, photos, error) in
        // Compelete Fetching Data
        self?.isLoading = false // trigger activity indicator stopAnimating
        if let error = error {
            self?.alertMessage = error.rawValue
        }else {
            self?.processFetchedPhoto(photos: photos)
        }
    }
}
// Generate cell's ViewModel
private func processFetchedPhoto( photos: [Photo] ) {
    self.photos = photos // Cache
    var viewModels = [PhotoListCellViewModel]() // TableViewCellViewModel
    photos.forEach({viewModels.append(createCellViewModel(photo: $0))})
    self.cellViewModels = viewModels // trigger photoListTableView reloadData
}

// Get Cell
func getCellViewModel( at indexPath: IndexPath ) -> PhotoListCellViewModel {
    return cellViewModels[indexPath.row]
}

๊ฐ€์žฅ ๋จผ์ € APIService๋Š” ๋” ์ด์ƒ View(ViewController)์— ์œ„์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋‹ค์Œ ์ฝ”๋“œ์˜ ์ „์ฒด์ ์ธ ํ๋ฆ„์„ ์‚ดํŽด๋ณด์ž๋ฉด requestFetchData ๋ฉ”์†Œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ์ค‘๊ณผ ๋๋‚œ ์ƒํ™ฉ์—์„œ isLoading์— ์ ์ ˆํ•œ ๊ฐ’์„ ํ• ๋‹นํ•ด์ฃผ์–ด didSet์„ ํ†ตํ•œ View์˜ activityIndicator ํ–‰์œ„๋ฅผ ์กฐ์ž‘ํ•ด์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ํ–‰์œ„๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ๋‹ค๋ฉด Cell์˜ ViewModel์„ ๋งŒ๋“œ๋Š” ๊ณผ์ •์„ ๊ฑฐ์ณ cellViewModels์— ํ…Œ์ด๋ธ” ๋ทฐ ์œ„์— ๋ฟŒ๋ ค์ค„ ๋ฐ์ดํ„ฐ๊ฐ€ ํ• ๋‹น๋ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ๊ฐ’์ด ํ• ๋‹น๋˜๋ฉด ์—ญ์‹œ didSet์„ ํ†ตํ•ด tableView์˜ reloadData ์ž‘์—…์ด ์ง„ํ–‰๋˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ์ด์— ๋Œ€ํ•œ View์˜ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

View

//MARK: Outlets
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!

//MARK: ViewModel For TableView
lazy var viewModel: PhotoListViewModel = {
    return PhotoListViewModel()
}()

//MARK: Life cycle
override func viewDidLoad() {
    super.viewDidLoad()
    ...
    initializeViewModel()
}

//MARK: Setup ViewModel
func initializeViewModel(){
    ...
    
    viewModel.updateLoadingStatus = { [weak self] in
        DispatchQueue.main.async {
            let isLoading = self?.viewModel.isLoading ?? false             
            if isLoading {
                self?.activityIndicator.startAnimating()
                UIView.animate(withDuration: 0.2, animations: {
                    self?.tableView.alpha = 0
                })
            }else{
                self?.activityIndicator.stopAnimating()
                UIView.animate(withDuration: 0.2, animations: {
                    self?.tableView.alpha = 1
                })
            }
        }
    }
        
    viewModel.reloadTableViewClosure = { [weak self] in
        DispatchQueue.main.async {
            self?.tableView.reloadData()
        }
    }
        
    viewModel.requestFetchData()
}

//MARK: TableView DataSource
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "photoCellIdentifier", for: indexPath) as? PhotoListTableViewCell else {
        fatalError("Cell not exists in storyboard")
    }
    // get data from cellViewModel
    let cellVieWModel = viewModel.getCellViewModel(at: indexPath)
    cell.setupViews(viewModel: cellVieWModel)
    return cell
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return viewModel.numberOfCells
}

initializeViewModel ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ViewModel์˜ ๋ฐ”์ธ๋”ฉ Closure๋“ค์„ ์ •์˜ํ•ด์ฃผ๊ณ  ๋งˆ์ง€๋ง‰์— requestFetchData ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•จ์œผ๋กœ์จ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

UITableViewDataSource ํ”„๋กœํ† ์ฝœ ๋ฉ”์†Œ๋“œ๋„ ์—ญ์‹œ ViewModel๋กœ๋ถ€ํ„ฐ ๊ฐ’์„ ๋ฐ›์•„์™€ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

User Interaction์€ ์ „์ฒด ์ฝ”๋“œ์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌํ•˜์—ฌ ์ „์ฒด์ ์ธ ๊ทธ๋ฆผ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

MVP์™€์˜ ์ฐจ์ด์ 

์ œ๊ฐ€ ๋Š๋ผ๊ธฐ์— ๊ฐ€์žฅ ํฐ ์ฐจ์ด์ ์€ Presenter๋Š” View์™€ ์—ฐ๊ฒฐ์„ฑ์ด ์•ฝํ•˜์ง€๋งŒ ํ”„๋กœํ† ์ฝœ๋กœ์จ ๊ฐ„์ ‘์ ์œผ๋กœ ์ด๋ฅผ ์ฐธ์กฐํ•˜๊ณ  ์žˆ๊ณ  ViewModel์€ ๋ฐ”์ธ๋”ฉ ์ž‘์—…์„ ํ†ตํ•ด ViewModel์—์„œ View์— ๊ด€ํ•œ ์–ด๋– ํ•œ ์˜์กด์„ฑ์ด๋‚˜ ์—ฐ๊ฒฐ์„ฑ๋„ ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

MVVM์ด ๋ฌผ๋ก  ์™„๋ฒฝํ•˜๋‹ค๊ณ  ํ•  ์ˆœ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์€ MVVM์˜ ๋‹จ์ ์„ ์†Œ๊ฐœํ•˜๊ณ  ์žˆ๋Š” ๊ธ€๋“ค์ž…๋‹ˆ๋‹ค.

๋‹จ์  ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ ์œ„์—์„œ ์ฝ”๋“œ๋ฅผ ์ž ๊น ์‚ดํŽด๋ณด์•˜๋˜ ๊ฒƒ๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ViewModel์—์„œ ๋„ˆ๋ฌด ๋งŽ์€ ์ผ๋“ค์„ ํ•œ๋‹ค๋Š” ๊ฒƒ๋„ ํ•˜๋‚˜์˜ ๋ฌธ์ œ์ ์œผ๋กœ ์ง€์ ๋˜๊ณค ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์‹ค์ œ๋กœ Builder๋‚˜ Router์˜ ๊ฐœ๋…์ด ๋„์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์—ญ์‹œ ๋‹ค์Œ์˜ ๊ธ€๋“ค์„ ์ฐธ๊ณ ํ•ด์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.


VIPER

Releases

No releases published

Packages

No packages published

Languages