Skip to content

๐Ÿงฟ๊ตญ๋‚ด ๋ฐ ํ•ด์™ธ ๋ฐ”์ด๋‹(LP) ์ˆ˜์ง‘๊ฐ€๋ฅผ ์œ„ํ•œ ์„œ๋น„์Šค ๐Ÿงฟ

Notifications You must be signed in to change notification settings

VinylaCrew/Vinyla_iOS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Vinyla_iOS

๊ตญ๋‚ด ๋ฐ ํ•ด์™ธ ๋ฐ”์ด๋‹ ์ €์žฅ ๋ฐ ๊ณต์œ , ๋‚˜๋งŒ์˜ ์žฅ๋ฅด ๋ถ„์„ ์„œ๋น„์Šค

Run Test CI


  • AppStore Link

  • ๋ชฉ์ฐจ

    • ์•ฑ ์‹คํ–‰ ํ™”๋ฉด
    • ์•„ํ‚คํ…์ณ ์„ค๊ณ„ ๋ฐ ๊ณ ๋ คํ•œ ์  ์ด๋™
    • ์„ฑ๋Šฅ
    • ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜
    • ๋ณต์žกํ•œ ์˜์กด์„ฑ
    • Test
    • ์‹ฑ๊ธ€ํ„ด ๋””์ž์ธ ํŒจํ„ด๊ณผ NSCache๋ฅผ ์ด์šฉํ•œ Image ์บ์‹ฑ
    • RxSwift ๋ฐ˜์‘ํ˜• ๊ฒ€์ƒ‰ํ™”๋ฉด

๋กœ๊ทธ์ธ ํ™”๋ฉด

ํ™ˆํ™”๋ฉด + ๋ ˆ๋ฒจ ๋””์ž์ธ ํ™”๋ฉด

๋ฐ”์ด๋‹ ๋ณด๊ด€ํ•จ ํ™”๋ฉด

๋ฐ”์ด๋‹ ๊ฒ€์ƒ‰ ํ™”๋ฉด

๋ฐ”์ด๋‹ ์ƒ์„ธ์ •๋ณด ํ™•์ธ + ์ €์žฅ ํ™”๋ฉด


๐Ÿ” ์„ค๊ณ„ ๋ฐ ๊ณ ๋ คํ•œ ์ 

์ด์ „ ํ”„๋กœ์ ํŠธ๋“ค์˜ ๋ฌธ์ œ์  (๊ฑฐ๋Œ€ํ•œ ํด๋ž˜์Šค, ๋ณต์žกํ•œ ์˜์กด์„ฑ, ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜, ์œ ์—ฐํ•˜์ง€ ๋ชปํ•œ View) ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๊นŠ์ด ๊ณ ๋ฏผ

๐Ÿง ์„ค๊ณ„์— ์•ž์„œ ๊ณ ๋ฏผํ•œ ์ 

๋ฐฐ๋ณด๋‹ค ๋ฐฐ๊ผฝ์ด ๋” ์ปค์ง€๋Š” ์ƒํ™ฉ์„ ๋ฐฉ์ง€ํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.

์•„ํ‚คํ…์ฒ˜ ๋ฐ ๋””์ž์ธ ํŒจํ„ด ๋“ฑ์„ ์ถ”๊ฐ€์ ์œผ๋กœ ์•Œ์•„๋ณด๋ฉฐ, ๋ชจ๋‘ ํšจ๊ณผ๊ฐ€ ์ข‹์•„ ๋ณด์—ฌ์„œ ๋งˆ์น˜ ์‡ผํ•‘์„ ํ•˜๋“ฏ ๋งŽ์ด ๋‹ด๊ฒŒ ๋˜์ง€ ์•Š๋„๋ก ๊ผญ ํ•„์š”ํ•˜๋ฉฐ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์งˆ๋ฌธ์— ์ ํ•ฉํ•œ์ง€ ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. ์ƒ์‚ฐ์„ฑ์„ ์˜ฌ๋ ค์ฃผ๋Š”๊ฐ€?
    1. (์•„ํ‚คํ…์ณ ๋ฐ ๋””์ž์ธ ํŒจํ„ด์˜ ๋„์ž…์ด, ์˜คํžˆ๋ ค ์„ธ๋ถ„ํ™”๋œ ์ถ”์ƒํ™”๋กœ ์ธํ•ด ์ƒ์‚ฐ์„ฑ์ด ๋–จ์–ด์ง€๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ์œ„ํ•ด)
  2. ๊ณผ๊ฑฐ์˜ ๋ฌธ์ œ์ ์„ ๊ฐœ์„ ํ•˜๊ฑฐ๋‚˜ ํ•ด๊ฒฐํ•ด์ฃผ๋Š”๊ฐ€?
  3. ๊ฐ€๋…์„ฑ ์ข‹์•„์ง€๋Š”๊ฐ€?
  4. ์ฝ”๋“œ๊ฐ€ ์—ฌ๋Ÿฌ์ƒํ™ฉ์— ์œ ์—ฐํ•ด์ง€๋Š”๊ฐ€?
  5. ์ฝ”๋”ฉํ•  ๋•Œ ๋ฌด์˜์‹์ ์ธ ์‹ค์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•ด์ค„ ์ˆ˜ ์žˆ๋Š”๊ฐ€?

๐Ÿง‘โ€๐Ÿ’ป ๊ณ ๋ฏผ์ดํ›„์˜ ๊ฒฐ๊ณผ

  • MVC ์•„ํ‚คํ…์ณ์—์„œ UI Code์™€ Logic ์ฝ”๋“œ์˜ ๋ถ„๋ฆฌ ํ•„์š”์„ฑ์„ ๋Š๋‚Œ (๊ฑฐ๋Œ€ํ•œ ViewController ๋ฐ ๋ณต์žกํ•œ ์˜์กด์„ฑ ๋ฌธ์ œ)

    • MVVM์˜ ๋„์ž…
    • Presentation Logic ๋ถ„๋ฆฌ๋ฅผ ์œ„ํ•ด ์ฝ”๋”ฉํ•˜๋Š” ์‹œ๊ฐ„์ด ์กฐ๊ธˆ ๋” ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์ง€๋งŒ, ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ ธ๋„ Unit Test๊ฐ€ ํŽธ๋ฆฌ
    • ๊ณผ๊ฑฐ์˜ ๋ฌธ์ œ์  (๋ณต์žกํ•œ ์˜์กด์„ฑ, ๊ฑฐ๋Œ€ํ•œ ํด๋ž˜์Šค) ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  ํŒ๋‹จ + ViewModel์˜ ์ถ”์ƒํ™”๋กœ ์œ ์—ฐํ•ด์ง€๋Š” ๊ตฌ์กฐ
    • ๋ฌด์˜์‹์ ์œผ๋กœ ViewController๊ฐ€ ๋น„๋Œ€ํ•ด์ง€๋Š” ์ƒํ™ฉ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Œ
    • ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ํŽธํ•ด์ง€๋ฏ€๋กœ, ์‹œ๊ฐ„์ด ์ง€๋‚  ์ˆ˜๋ก ์ƒ์‚ฐ์„ฑ์ด ์ฆ๊ฐ€ํ•œ๋‹ค๊ณ  ํŒ๋‹จ
  • ๋‚˜์•„๊ฐ€ Coordinator๋ฅผ ํ†ตํ•ด View ์ „ํ™˜ Code ํ†ตํ•ฉ ๊ด€๋ฆฌ์˜ ํ•„์š”์„ฑ (์œ ์—ฐํ•œ View ์ „ํ™˜ ๋Œ€์‘)

    • View ์ „ํ™˜ ์ฝ”๋“œ ํ•˜๋“œ ์ฝ”๋”ฉ X, ๋ฉ”์†Œ๋“œ ํ•œ์ค„๋กœ ์ž์œ ๋กœ์šด View ์ „ํ™˜ (์—ฌ๋Ÿฌ ์ƒํ™ฉ์— ์œ ์—ฐ + ๊ฐ€๋…์„ฑ ์ฆ๊ฐ€)
    • View๋Š” ViewModel์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œํ˜„๋งŒํ•˜๋Š” ๋‹จ์ผ ์ฑ…์ž„์„ ์ง€๊ฒŒ๋จ
  • ARC๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ViewModel ๋ฐ Coordinator๊ฐ€ Retain Cycle์ด ์ƒ๊ธฐ์ง€ ์•Š๋„๋ก, ์„ ์ œ์ ์ธ ๋ ˆํผ๋Ÿฐ์Šค ์นด์šดํŠธ ๊ด€๋ฆฌ

ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์— ๋งž๋Š” MVVM - C ๊ตฌ์กฐ ๋„์ž…

  • Testableํ•œ ๊ตฌ์กฐ, View์—์„  ๋ถˆํ•„์š”ํ•œ ํ™”๋ฉด์ „ํ™˜ Code๋ฅผ ๊ฐ€์ง€์ง€ ์•Š์œผ๋ฉฐ ์—ฌ๋Ÿฌ ์ƒํ™ฉ์— ๋งž๋Š” ์ž์œ ๋กœ์šด View ์ „ํ™˜ ๊ฐ€๋Šฅ

  • MVVM ๊ณ„์‚ฐ๊ธฐ๋ฅผ ๋งŒ๋“ค์–ด ๋ณด๋ฉฐ Logic ๋ฐ UI Code ๋ถ„๋ฆฌ์— ๋Œ€ํ•œ ์ดํ•ด, RxSwift bind์˜ ํŽธ๋ฆฌํ•จ์„ ์•Œ๊ฒŒ๋จ

  • ์ ์žฌ์ ์†Œ์— ๋งž๋Š” ์ฝ”๋“œ ์ž‘์„ฑ / ์˜์กด์„ฑ ์ค„์ด๊ธฐ / ์ž์œ ๋กœ์šด ๋ทฐ ์ „ํ™˜ ๊ตฌ์กฐ / Testableํ•œ ๊ตฌ์กฐ

  • ํ†ต์‹ ์„ ๋‹ด๋‹นํ•˜๋Š” APIService๋Š” ViewModel์— ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐ ๋ถ„๋ฆฌ


๐Ÿ“•์„ฑ๋Šฅ

โ˜‘๏ธ ์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ์œ„ํ•ด

  • ์ƒ์†์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ class๋Š” final class๋กœ ์„ ์–ธ
  • ํด๋ž˜์Šค ๋‚ด๋ถ€์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋Š” property์— ๋Œ€ํ•˜์—ฌ ์ ๊ทน์ ์œผ๋กœ private ์„ ์–ธ
  • ํ†ต์‹  ํ™˜๊ฒฝ ๊ณ ๋ ค, Caching์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์„ ์ ๊ทน์ ์œผ๋กœ ์ฐพ์•„๋ƒ„

=> ๋ฉ”์†Œ๋“œ ์ธ๋ผ์ด๋‹๊ณผ ์ปดํŒŒ์ผ๋Ÿฌ ์ตœ์ ํ™”๋ฅผ ํ†ตํ•ด ์„ฑ๋Šฅ ๊ฐœ์„ 

=> ์œ ์ €์˜ ํ†ต์‹  ๋ฆฌ์†Œ์Šค๋Š” ์ค„์ด๊ณ , ๋ฐ์ดํ„ฐ๋Š” ์บ์‹ฑํ•˜์—ฌ ์„ฑ๋Šฅ ๊ฐœ์„ 

1๏ธโƒฃ ๋ฐ”์ด๋‹ ๋ณด๊ด€ํ•จ ๋ฐ์ดํ„ฐ๋Š” CoreData๋ฅผ ํ†ตํ•ด ์บ์‹ฑ

์™œ ๋ณด๊ด€ํ•จ ๋ฐ์ดํ„ฐ๊ฐ€ ์บ์‹ฑ์ด ํ•„์š”ํ–ˆ๋Š”์ง€?

  • ๋ฐ”์ด๋‹ ์•จ๋ฒ”์˜ ์ธ๋„ค์ผ ์ด๋ฏธ์ง€, ์ œ๋ชฉ, Artist, ๋ฐ”์ด๋‹ ๊ณ ์œ  ID ์ •๋„๊ฐ€ ๋ณด๊ด€ํ•จ ๋ฐ์ดํ„ฐ๋กœ ์ €์žฅ
  • ์ธ๋„ค์ผ ์ด๋ฏธ์ง€ํฌ๊ธฐ๊ฐ€ ๋Œ€๋žต 200kb ์ด์ง€๋งŒ, ๋ฐ์ดํ„ฐ๊ฐ€ 100๊ฐœ๋ผ๋ฉด 20mb ํฌ๊ธฐ๋ฅผ ๊ฐ€์ง
  • ๋ณด๊ด€ํ•จ์—์„œ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€๋ฅผ ์„ค์ •ํ•˜๊ณ , ๊ฒ€์ƒ‰ํ™”๋ฉด์œผ๋กœ ์ด๋™ ํ•˜๋ฏ€๋กœ
    • ๋ณด๊ด€ํ•จ์— ์ง„์ž…ํ• ๋•Œ๋งˆ๋‹ค ํ†ต์‹ ์„ ์ง„ํ–‰ํ•˜๋ฉด ๋งŽ์€ ํ†ต์‹  ๋ฆฌ์†Œ์Šค๊ฐ€ ํˆฌ์ž…๋˜์–ด์ง
    • ๋˜ํ•œ ๋ณด๊ด€ํ•จ ํ™”๋ฉด์€ 9๊ฐœ์”ฉ ๋ฐ์ดํ„ฐ๋ฅผ Pagingํ•˜์—ฌ ๋ณด์—ฌ์ฃผ๊ธฐ์—
    • ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋ถ€๋“œ๋Ÿฌ์šด Paging์„ ์œ„ํ•ด์„œ ์บ์‹ฑ์ด ์ง„ํ–‰๋˜์–ด์•ผํ–ˆ์Œ

=> ์—ฌ๋Ÿฌ๋ฒˆ ์ด๋ฏธ์ง€ ํ†ต์‹ ์ด ์ด๋ฃจ์–ด ์ ธ์•ผํ•˜๋Š” ๋ถ€๋ถ„์„ ์ตœ์†Œํ™” ํ•˜์—ฌ ๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ๋กœ ์ €์žฅํ•˜์—ฌ ์„ฑ๋Šฅ์ƒ ์ด์ ์„ ์ฑ™๊ธฐ๋„๋กํ•จ

=> ๋ฐ”์ด๋‹ ๋ณด๊ด€์‹œ ์„œ๋ฒ„์— ๊ธฐ๋ก๋˜๋ฉฐ, ์ƒ์„ธ ๋ฐ์ดํ„ฐ๋Š” CoreData์— ์ €์žฅํ•˜์—ฌ ๋ณด๊ด€ํ•จ ํ™”๋ฉด์—์„œ ๋ณด์—ฌ์คŒ

=> ๋ณด๊ด€ํ•จ ์ง„์ž…์‹œ ๋งค๋ฒˆ ์„œ๋ฒ„์™€ ํ†ต์‹ ์„ ์ง„ํ–‰ํ•˜์—ฌ ๋ณด๊ด€ํ•จ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ์•Š์•„๋„ ๋จ

=> ์•ฑ์„ ์ง€์šฐ๊ณ  ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•œ ๊ฒฝ์šฐ, ๋‹ค๋ฅธ ์•„์ด๋””๋กœ ๋กœ๊ทธ์ธํ•˜๋Š” ๊ฒฝ์šฐ

=> ์ž๋™ ๋กœ๊ทธ์ธ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๊ณ  ์ตœ์ดˆ ๋กœ๊ทธ์ธ ์ผ ๋•Œ, ํ™ˆ ํ™”๋ฉด์—์„œ ๋ณด๊ด€ํ•จ ์กฐํšŒ ํ†ต์‹ ์„ ํ†ตํ•ด ์ƒ์„ธ ๋ฐ์ดํ„ฐ๋ฅผ CoreData์— ์ €์žฅ

=> ๋‹จ์ผ Background Thread๋ฅผ ์ด์šฉํ•˜์—ฌ CoreData ์ž‘์—… ์ˆ˜ํ–‰, UI Processing์€ Main Thread ์ด์šฉ

=> ๐Ÿ‘ ํ•œ๋ฒˆ์— ํ•˜๋‚˜์˜ ์ž‘์—…๋งŒ ์ˆ˜ํ–‰ํ•˜์—ฌ, ๋ณ‘ํ•ฉ์ถœ๋™ ์ƒํ™ฉ์„ ๋งŒ๋“ค์ง€ ์•Š๊ณ  Thread Safe ํ•˜๋„๋ก ์„ค๊ณ„

=> ๐Ÿ‘ ๊ฒฐ๋ก ์ ์œผ๋กœ, ์ตœ์ดˆ ๋กœ๊ทธ์ธ์‹œ์—๋งŒ ํ•œ๋ฒˆ์˜ ๋ณด๊ด€ํ•จ ํ†ต์‹ ์ด ์ด๋ฃจ์–ด์ง€๋ฉด์„œ ๊ธฐ์กด ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” ์ง„ํ–‰

2๏ธโƒฃ ๋น ๋ฅธ Scroll์‹œ ์„ฑ๋Šฅ์ €ํ•˜ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์ด๋ฏธ์ง€ ํ†ต์‹  Cancel

์ฒ˜์Œ ๊ฒ€์ƒ‰ ํ™”๋ฉด์˜ ๋ฌธ์ œ์  ์ •์˜

  • ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ, ๊ฒ€์ƒ‰ ํ™”๋ฉด์—์„œ ์ตœ๋Œ€ ๊ฒ€์ƒ‰ ๋ฐ์ดํ„ฐ ์ˆ˜๋Š” 50๊ฐœ
  • ๋น ๋ฅธ ์Šคํฌ๋กค๋กœ ๋งจ ๋ฐ‘์œผ๋กœ TableView ์ด๋™์‹œ ์ด์ „์˜ ์Šคํฌ๋กค๋˜๋Š” ์ด๋ฏธ์ง€ ํ†ต์‹ ์ด ๋ชจ๋‘ ์ง„ํ–‰๋จ
  • ๋ฐ์ดํ„ฐ ์†๋„๊ฐ€ ๋Š๋ฆฐ ์ƒํ™ฉ์ด๋ผ๋ฉด, ์ด์ „์˜ ์…€ ์ด๋ฏธ์ง€ ํ†ต์‹ ๋•Œ๋ฌธ์— ๋ณด์—ฌ์ ธ์•ผ ํ•  ์…€์˜ ์ด๋ฏธ์ง€ ํ†ต์‹ ์ด ๋Š๋ ค์งˆ ์ˆ˜ ์žˆ์Œ
  • ๋˜ํ•œ, ์›ํ•˜๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ๋งจ๋ฐ‘์— ์žˆ์—ˆ๋‹ค๋ฉด ์ค‘๊ฐ„์— ์žˆ๋Š” ์…€๋“ค์˜ ์ด๋ฏธ์ง€ ํ†ต์‹ ์€ ์œ ์ € ์ž…์žฅ์—์„  ๋น„ํšจ์œจ์  (๋ฐ์ดํ„ฐ ์†Œ๋ชจ๊ฐ’ ์ฆ๊ฐ€)

๐Ÿง ๊ฐœ์„ ํ•˜๋ฉฐ ๊นจ๋‹ฌ์€ ์ 

=> Cell์˜ Life Cycle์„ ๊ณ ๋ คํ•˜์—ฌ, Cell์ด ์žฌํ™œ์šฉ ์ƒํƒœ๊ฐ€ ๋ ๋•Œ ํ•ด๋‹น image ๋น„๋™๊ธฐ ํ†ต์‹ ์˜ DataTask๊ฐ€ Cancel ๋˜๋„๋ก

=> ๋น ๋ฅธ ์Šคํฌ๋กค์‹œ ์ค‘๊ฐ„ ๋ถ€๋ถ„์˜ ์…€์€ ์ด๋ฏธ์ง€ ํ†ต์‹ ์ด ์ด๋ฃจ์–ด์ง€์ง€ ์•Š๊ณ , ๋งˆ์ง€๋ง‰ ๋ถ€๋ถ„์ด ๋ฐ”๋กœ ์ด๋ฏธ์ง€ ํ†ต์‹  ์ง„ํ–‰

final class SearchTableViewCell: UITableViewCell {
    private var cellImageDataTask: URLSessionDataTask?
override func prepareForReuse() {
        super.prepareForReuse()
        self.cellImageDataTask?.cancel()
        self.searchVinylImageView.image = nil
    }

๐Ÿ“—๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜

โŒ ์ด์ „ ํ”„๋กœ์ ํŠธ์˜ ๋ฌธ์ œ์ 

=> ์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ๊ฐ•ํ•œ ์ฐธ์กฐ๋กœ ์ธํ•ด, Retain Cycle์ด ๋ฐœ์ƒ

=> ๋ณต์žกํ•œ ์ฐธ์กฐ ๊ตฌ์กฐ๋กœ, ์–ด๋– ํ•œ Class๊ฐ€ ๋ˆ„์ˆ˜๋ฅผ ์ผ์œผํ‚ค๋Š”์ง€ ๋ถ„์„์ด ์–ด๋ ค์›€

โ˜‘๏ธ ํ•ด๊ฒฐ

  • View์™€ ViewModel์—์„œ ๋‹ค๋ฅธ ํด๋ž˜์Šค์˜ ์ฐธ์กฐ๋ฅผ ์ฃผ์˜์žˆ๊ฒŒ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.
    • View์™€ ViewModel์˜ ์˜์กด์„ฑ๊ณผ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ”๊ณ 
    • View์™€ ViewModel์˜ ๋ถˆํ•„์š”ํ•œ Retain Count๊ฐ€ ์ฆ๊ฐ€๋˜์ง€์•Š๋„๋ก
  • ๋˜ํ•œ ํ•ญ์ƒ ์ž์‹ ์„ ์ฐธ์กฐํ•˜๋Š” ์ƒํ™ฉ์˜ ํด๋กœ์ € ์—์„ , ์บก์ณ๋ฆฌ์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ํŠนํžˆ ViewModel์—์„œ ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ๋ฅผ ๋”์šฑ ํ™•์ธํ•˜๊ณ  ์กฐ์‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค.

View๋Š” Coordinator๋ฅผ ์•ฝํ•œ ์ฐธ์กฐํ•˜๋ฉฐ, ViewModel์„ ๊ฐ•ํ•˜๊ฒŒ ์ฐธ์กฐ

  • ์ถ”๊ฐ€ ์ฐธ์กฐ๊ฐ€ ์—†์–ด Retain Cycle์ด ์ƒ๊ธฐ์ง€ ์•Š๋„๋ก ์„ค๊ณ„ ํ•ด๋‹น View๊ฐ€ ์‚ฌ๋ผ์ง€๋ฉด ViewModel๋„ ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ๊ฐ€ ๋จ

View ๋ฐ ViewModel์—์„œ ํด๋กœ์ €๋กœ ์ธํ•œ ์ฐธ์กฐ๋กœ Retain Cycle ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด

  • weak, unowned ์บก์ณ๋ฆฌ์ŠคํŠธ ์‚ฌ์šฉ

Profile - Leaks ๋ฐ CFGetRetainCount ํ™œ์šฉํ•˜์—ฌ ๋””๋ฒ„๊น… ๋ฐ ๊ฒ€์ฆ

final class SignUpViewController: UIViewController {
  private weak var coordiNator: AppCoordinator?
  private var viewModel: SignUpViewModelProtocol
  //Coordinator type method ์„ค๋ช…์ถ”๊ฐ€
static func instantiate(viewModel: SignUpViewModelProtocol, coordiNator: AppCoordinator) -> UIViewController {
        let storyBoard = UIStoryboard(name: "SignUp", bundle: nil)
        guard let viewController = storyBoard.instantiateViewController(identifier: "SignUp") as? SignUpViewController else {
            return UIViewController()
        }
        viewController.viewModel = viewModel
        viewController.coordiNator = coordiNator
        return viewController
    }
}

๐Ÿ“˜๋ณต์žกํ•œ ์˜์กด์„ฑ

โŒ ์ด์ „ ํ”„๋กœ์ ํŠธ์˜ ๋ฌธ์ œ์ 

=> MVC ์•„ํ‚คํ…์ณ๋กœ ์ธํ•ด, ๋‹ค๋ฅธ ViewController๋ฅผ ์˜์กดํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ Class๋“ค์„ ์ง์ ‘ ํ”„๋กœํผํ‹ฐ๋กœ ์ฐธ์กฐํ•˜์—ฌ ๊ฒฐํ•ฉ๋„๊ฐ€ ๋†’์•„์ง

=> ์‹ฑ๊ธ€ํ„ด ํŒจํ„ด์œผ๋กœ ํ†ต์‹  ๊ฐ์ฒด์— ์ ‘๊ทผํ•˜๊ฒŒ ๋˜์–ด Testableํ•œ ๊ตฌ์กฐ๋ฅผ ์ง€๋‹ˆ๊ธฐ ์–ด๋ ค์›€, ๋ชจ๋“  ๊ณณ์—์„œ ํ†ต์‹  ๊ฐ์ฒด์— ์ ‘๊ทผ์ด ๊ฐ€๋Šฅ

โ˜‘๏ธ ํ•ด๊ฒฐ

View์—์„œ ์ง์ ‘ ViewModel ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ViewModel Protocol์— ์˜์กด(์˜์กด์„ฑ ๋ถ„๋ฆฌ), Coordinator๋ฅผ ํ†ตํ•ด ViewModel์„ ์ƒ์„ฑํ•˜๊ณ  ์ฃผ์ž… (์˜์กด์„ฑ ์ฃผ์ž…)

  • ์ถ”ํ›„ ์„œ๋น„์Šคํ•œ์ง€ ์–ด๋Š์ •๋„์˜ ์‹œ๊ฐ„์ด์ง€๋‚˜๋ฉด ์ƒ๊ธธ ๋ฆฌํŒฉํ† ๋ง์—์„œ View์™€ ViewModeld์˜ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ”๊ธฐ ์œ„ํ•จ

  • UI ๋ฐ Unit Test์— ๋ณด๋‹ค ์œ ์—ฐํ•˜๊ฒŒ ๋Œ€์‘ ๋ฐ Test ์ง„ํ–‰ ๊ฐ€๋Šฅํ•ด์ง

  • ๋ฆฌํŒฉํ† ๋ง์‹œ ๋ณด๋‹ค ์ ์€ ๋ฆฌ์†Œ์Šค ํˆฌ์ž…

func moveToSignUPView() {
let signUpView = SignUpViewController.instantiate(viewModel: SignUpViewModel(), coordiNator: self)
guard let windowRootViewController = self.windowRootViewController else { return }
windowRootViewController.pushViewController(signUpView, animated: true)
    }

ViewModel์—์„œ ํ†ต์‹  ํด๋ž˜์Šค๋ฅผ ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐ ๋ถ„๋ฆฌ

final class SearchViewModel {
init(searchAPIService: VinylAPIServiceProtocol = VinylAPIService()) {
        self.searchAPIService = searchAPIService
  }
 //Mock APIService Test
  init(searchAPIService: VinylAPIServiceProtocol = MockAPIService()) {
        self.searchAPIService = searchAPIService
  }
}

๐Ÿง๊ณ ๋ฏผํ•˜๋ฉฐ ๊นจ๋‹ฌ์€ ์ 

ํ†ต์‹ ์ด ํ•„์š”ํ•œ ViewModel์—๋งŒ ํ†ต์‹  API ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์คŒ

=> ์‹ฑ๊ธ€ํ„ด ํŒจํ„ด์œผ๋กœ ํ†ต์‹  ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ํŠน์ • API์˜ Mock Test๋ฅผ ์œ„ํ•ด์„œ ์ „์ฒด ๋ฉ”์†Œ๋“œ์— ์˜ํ–ฅ์ด ๊ฐ€๋Š” ์ˆ˜์ •์ด ์ด๋ฃจ์–ด์งˆ ์ˆ˜ ์žˆ๊ณ  Test ๊ณผ์ • ์ž์ฒด๊ฐ€ ๋ถˆํŽธํ•˜๊ณ  ๋ณต์žกํ•ด์ง

=> ์‹ฑ๊ธ€ํ„ด ํŒจํ„ด์œผ๋กœ ํ†ต์‹  ๊ฐ์ฒด์— ์ ‘๊ทผ ํ•˜์ง€ ์•Š์Œ

=> ํ†ต์‹ ์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ ViewModel์—์„œ ํ†ต์‹  ๊ฐ์ฒด์— ์ ‘๊ทผ ๊ฐ€๋Šฅ์„ฑ์„ ๋ฐฐ์ œ, ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ์ƒํ™ฉ๊ณผ ์ ‘๊ทผํ•˜๊ธฐ ํž˜๋“  ์ƒํ™ฉ์€ ์™„์ „ ๋‹ค๋ฅธ ๊ฒƒ์ด๋ผ๊ณ  ํŒ๋‹จ

=> ์˜์กด์„ฑ ๋ถ„๋ฆฌ๋ฅผ ํ†ตํ•ด ์œ ์—ฐํ•˜๊ฒŒ ํ†ต์‹ ์ด ๋ถ„๋ฆฌ๋œ Mock ํ†ต์‹  ๊ฐ์ฒด๋กœ API Test ๊ฐ€๋Šฅ, ๋ฏธ๋ฆฌ ์„ค์ •ํ•ด๋‘” MockAPIService ๊ฐ์ฒด๋กœ ๋น ๋ฅด๊ณ  ์ •ํ™•ํ•˜๊ฒŒ Test ๊ฐ€๋Šฅ


โœ… ํ”„๋กœ์ ํŠธ ์ ์šฉ: Search View ๋ฐ Vinyl Detail View Mock APIService Test ์ง„ํ–‰

  • Search API ๊ตฌํ˜„์ „ Mock Test ์„ ์ œ ์ง„ํ–‰ํ•˜์—ฌ ๊ฐœ๋ฐœ
    • Mock Data๋กœ ๋‚ด๋ถ€ CoreData ๋กœ์ง ๊ตฌํ˜„ ๋ฐ Test ์ง„ํ–‰ ๊ฐ€๋Šฅ (์ƒ์‚ฐ์„ฑ ์ฆ๊ฐ€)
  • Vinyl Detail View Mock Test ์ง„ํ–‰
    • ๋ฐ”์ด๋‹ ์ƒ์„ธ API ์—ฐ์† ์กฐํšŒ์‹œ ์‘๋‹ต์ด ์˜ค์ง€ ์•Š๋Š” ์—๋Ÿฌ ์ด์Šˆ ์ฆ‰์‹œ ๋ฐœ๊ฒฌ

๐Ÿง๊ณ ๋ฏผ ๋ฐ ๊นจ๋‹ฌ์€ ์ 

  • Test๊ฐ€ ํ•„์š”ํ•˜์ง€๋งŒ, ๊ฒ€์ฆํ•˜๊ธฐ๊ฐ€ ๊นŒ๋‹ค๋กœ์šด Code ๋ถ€๋ถ„์„ ์ •ํ™•ํ•˜๊ฒŒ Unit Test์ง„ํ–‰ (์ตœ๊ณ  ๋ ˆ๋ฒจ๋””์ž์ธ)
  • Exceptation๊ณผ fulfill, wait์œผ๋กœ ์‹ค์ œ ํ†ต์‹ ์„ ํ•˜์—ฌ Test ์ง„ํ–‰์ด ๊ฐ€๋Šฅ
  • ํ•˜์ง€๋งŒ, ์„œ๋ฒ„๊ฐ€ ๋‹ค์šด๋˜๊ฑฐ๋‚˜ ๊ฐœ๋ฐœํ™˜๊ฒฝ์—์„œ ์ธํ„ฐ๋„ท์ด ๋Š๊ธด ์ƒํ™ฉ์ด๋ผ๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅ
  • ์„œ๋ฒ„ํ†ต์‹ ์ด ๊ฐ€๋Šฅํ•œ ์ƒํ™ฉ์—์„œ๋„ Test๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ํ†ต์‹ ์ด ๋ถ„๋ฆฌ๋œ Test์ง„ํ–‰๋„ ๊ฐ€๋Šฅํ•ด์•ผํ•จ
  • ๋”ฐ๋ผ์„œ, MockAPIService ์„œ๋ฒ„ํ†ต์‹ ์ด ๋ถ„๋ฆฌ๋œ Test ์ง„ํ–‰์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ๋ณ€๊ฒฝ
func testGetViynlDetailAPI() {
        let testMockAPIService = MockAPIService()
        let mockSampleData = APITarget.getVinylDetail(pathVinylID: 1234).sampleData
        let expectedResponseData = try? JSONDecoder().decode(VinylInformation.self,from: mockSampleData)
	//๋ฐ”์ด๋‹ ์ƒ์„ธ API MockTest ์ง„ํ–‰
        testMockAPIService.getVinylDetail(vinylID: 12345)
            .subscribe(onNext: { data in
                XCTAssertEqual(expectedResponseData?.data?.artist, data?.artist)
                XCTAssertEqual(expectedResponseData?.data?.tracklist?[0], data?.tracklist?[0])
                print(data)
            })
    }

โœ… ๋ณด๊ด€ํ•จ ์ตœ๊ณ  ๋ ˆ๋ฒจ๋””์ž์ธ Paging Unit Test ์ง„ํ–‰

  • ์ตœ๊ณ  ๋ ˆ๋ฒจ์€, ๋ณด๊ด€ํ•จ ๊ฐฏ์ˆ˜๊ฐ€ 500๊ฐœ๋ฅผ ๋„˜๊ธธ์‹œ ๋‹ฌ์„ฑํ•จ
  • Unit Test๋ฅผ ์ง„ํ–‰ํ•˜์—ฌ, 500๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅ์‹œํ‚ค๊ณ  ์˜ฌ๋ฐ”๋ฅธ ์ˆœ์„œ๋Œ€๋กœ 9๊ฐœ์”ฉ Paging ๋˜๋Š”์ง€ Unit Test ์ง„ํ–‰
  • ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€์—์„œ ๋ฐ์ดํ„ฐ Index๊ฐ€ ๋งž์ง€ ์•Š์•„ Test ์‹คํŒจ
    • ์˜ฌ๋ฐ”๋ฅด๊ฒŒ Paging ๋˜๋„๋ก Code ์ˆ˜์ •ํ•˜์—ฌ, Unit Test ์„ฑ๊ณต

๐Ÿง๊ณ ๋ฏผ ๋ฐ ๊นจ๋‹ฌ์€ ์ 

  • Test๊ฐ€ ํ•„์š”ํ•˜์ง€๋งŒ, ๊ฒ€์ฆํ•˜๊ธฐ๊ฐ€ ๊นŒ๋‹ค๋กœ์šด Code ๋ถ€๋ถ„์„ ์ •ํ™•ํ•˜๊ฒŒ Unit Test์ง„ํ–‰ (์ตœ๊ณ  ๋ ˆ๋ฒจ๋””์ž์ธ)
  • Logic์ด ๋“ค์–ด๊ฐ€๋Š” View์—์„œ Code์˜ ๊ฒ€์ฆ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์€ Unit Test์˜ ํ•„์š” ๋ฐ ํ•„์ˆ˜
    • ์ถ”ํ›„ ๋ฆฌํŒฉํ† ๋ง ์ง„ํ–‰์‹œ Side Effect๋ฅผ Unit Test๋กœ ์ค„์ผ ์ˆ˜ ์žˆ์Œ
    • ๋ฐฐํฌ์ค€๋น„์‹œ Unit Test๊ฐ€ ์‹คํŒจํ•œ ๋ถ€๋ถ„์„ ์ค‘์ ์ ์œผ๋กœ ๋‹ค์‹œ ํ™•์ธํ•˜๋ฉด ๋จ

๐Ÿ”ต ์‹ฑ๊ธ€ํ„ด ๋””์ž์ธ ํŒจํ„ด๊ณผ NSCache๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ image ์บ์‹ฑ

๊ฒ€์ƒ‰ํ™”๋ฉด ๋ฐ ์ƒ์„ธํ™”๋ฉด์˜ image๋Š” NSCache๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์บ์‹ฑ

๐Ÿง๊ณ ๋ฏผํ•˜๋ฉฐ ๊นจ๋‹ฌ์€ ์ 

=> ํ•œ๋ฒˆ ๊ฒ€์ƒ‰ํ•˜์—ฌ ๋ณด๊ด€ํ•œ ๊ฐ€์ˆ˜ ๋ฐ ์•จ๋ฒ”์˜ ์ œ๋ชฉ์„, ๋‹ค์Œ์— ์•ฑ์„ ์‹คํ–‰ํ•˜์—ฌ ๋˜ ๋‹ค์‹œ ๊ฒ€์ƒ‰ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ ์„ ๊ฒƒ์œผ๋กœ ์ƒ๊ฐํ•˜์—ฌ ๋””๋ฐ”์ด์Šค ๋‚ด๋ถ€ ์ €์žฅ ๊ณต๊ฐ„์— ์บ์‹ฑ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ ํƒํ•˜์ง€ ์•Š์Œ

=> ์‹ฑ๊ธ€ํ„ด ๋””์ž์ธ ํŒจํ„ด์˜ ์‚ฌ์šฉ์œผ๋กœ ์•ฑ์ด ์ข…๋ฃŒ๋˜๋ฉด ์บ์‹ฑ ๋ฐ์ดํ„ฐ๋„ ์†Œ๋ฉธํ•˜๋„๋ก ์„ค๊ณ„, ๋˜ํ•œ ์ด๋ฏธ์ง€ ์บ์‹ฑ์„ Thread Safe ํ•˜๊ฒŒ์ง„ํ–‰ (Swift์˜ ์‹ฑ๊ธ€ํ„ด ํŒจํ„ด์€ ์‚ฌ์šฉ์‹œ์ ์— ์ดˆ๊ธฐํ™”๋˜๊ณ , dispatchonce ์ ์šฉ๋˜์–ด ์“ฐ๋ ˆ๋“œ Safe ํ•˜๋ฏ€๋กœ)

=> ์ด๋ฏธ์ง€ ์บ์‹ฑ๊นŒ์ง€ ๋””๋ฐ”์ด์Šค์— ํŒŒ์ผ๋””์Šคํฌ ํ˜•ํƒœ๋กœ ์ด๋ฃจ์–ด์ง€๋ฉด, ์‹œ๊ฐ„์ด ์ง€๋‚ ์ˆ˜๋ก ์•ฑ์„ ํ†ตํ•ด ๋งŽ์€ ๊ณต๊ฐ„์„ ์ฐจ์ง€ํ•˜์—ฌ ์œ ์ €์˜ ๋ฆฌ์Šคํฌ ์ฆ๊ฐ€

=> ๊ฒ€์ƒ‰๊ณผ์ •์—์„œ ๊ฐ™์€ Word๋ฅผ ๋‹ค์‹œ ๊ฒ€์ƒ‰ํ•˜๊ณ  ๋น ๋ฅธ ์Šคํฌ๋กค์„ ์œ„ํ•œ ์ด๋ฏธ์ง€ ์บ์‹ฑ์€ ํ•„์ˆ˜๋ผ๊ณ  ์ƒ๊ฐ

class NSCacheManager {
    static let shared = NSCache<NSString, UIImage>()
    private init() {
    }
}
func setImageURLAndChaching(_ imageURL: String?) {

        guard let imageURL = imageURL else { return }

        DispatchQueue.global(qos: .background).async {

            let cachedKey = NSString(string: imageURL)

            if let cachedImage = NSCacheManager.shared.object(forKey: cachedKey) {
                DispatchQueue.main.async {
                    self.image = cachedImage
                }
                return
            }

            guard let url = URL(string: imageURL) else { return }

            let 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) {
                        NSCacheManager.shared.setObject(image, forKey: cachedKey)
                        self?.image = image
                    }
                }
            }
                dataTask.resume()

        }
    }

๐Ÿ”ด RxSwift๋ฅผ ์ด์šฉํ•œ ๋ฐ˜์‘ํ˜• ๊ฒ€์ƒ‰ํ™”๋ฉด

Input ๋ฐ์ดํ„ฐ๋ฅผ ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ํ†ตํ•œ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ฒ•์„ ์ดํ•ด

  • ์ผ๋ฐ˜์ ์ธ ์ด์Šค์ผ€์ดํ•‘ ํด๋กœ์ €๋ฅผ ํ†ตํ•œ ํ†ต์‹  ํ•จ์ˆ˜ ์‚ฌ์šฉ ๋Œ€์‹ 
    • Observable์„ ํ†ตํ•œ return์ด ๊ฐ€๋Šฅํ•œ ๋น„๋™๊ธฐ ์ฝ”๋“œ ์‚ฌ์šฉ
.flatMapLatest{ [unowned self] vinyl -> Observable<[SearchModel.Data?]> in
                return self.searchAPIService.searchVinyl(vinylName: vinyl)
            }

(๋น„๋™๊ธฐ ํ†ต์‹  Code ๋ถ€๋ถ„์˜ ๊ฐ€๋…์„ฑ ์ฆ๊ฐ€)

๐Ÿง๊ณ ๋ฏผํ•˜๋ฉฐ ๊นจ๋‹ฌ์€ ์ 

  • TextField์— addTarget .editingChanged ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด ๊ฒ€์ƒ‰API๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, 1๊ธ€์ž์˜ ๋ณ€ํ™”์ƒํƒœ๋งˆ๋‹ค ํ†ต์‹ ์ด ์ด๋ฃจ์–ด์ง€๋ฏ€๋กœ ๋น„ํšจ์œจ์ ์œผ๋กœ ํŒ๋‹จ.

    • DispatchQueue๋ฅผ ํ†ตํ•ด Delay ์ƒํƒœ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ ์ €ํ•˜
    • ๋ณ„๋„์˜ DispatchQueue ์ž‘์—…์ด ์ด๋ฃจ์–ด์ง€๋ฏ€๋กœ ์“ฐ๋ ˆ๋“œ์— ๊ด€๋ จํ•ด ๋”์šฑ ์กฐ์‹ฌํžˆ ๋””๋ฒ„๊น… ๋ฐ ์ฝ”๋”ฉ์ด ์ง„ํ–‰ (์ถ”๊ฐ€ ๋ฆฌ์†Œ์Šค ๋ฐœ์ƒ)
  • debounce ๋ฐ observeOn ์„ ํ†ตํ•ด ์ง๊ด€์ ์ด๋ฉฐ ๊ฐ„ํŽธํ•˜๊ฒŒ ์“ฐ๋ ˆ๋“œ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅ

    • ๊ฐ€๋…์„ฑ ์ฆ๊ฐ€ ๋ฐ ์ง๊ด€์ ์ธ ์“ฐ๋ ˆ๋“œ ๊ด€๋ฆฌ ํ‘œ์‹œ
  • ์œ ์ €๊ฒฝํ—˜์„ ๋†’์ž„

    • ์›ํ•˜๋Š” Word๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ณ ๋‚˜๋ฉด ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ฅด์ง€ ์•Š์•„๋„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๊ฒ€์ƒ‰ ์ง„ํ–‰ ๋ฐ ๊ฒฐ๊ณผ ํ‘œ์‹œ

1๏ธโƒฃ Vinyl ์ด๋ฆ„์„ ViewModel์˜ VinylName์— bind ์ง„ํ–‰

//View
vinylSearchBar.rx.text
            .orEmpty
            .distinctUntilChanged() // ์ค‘๋ณต ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ ๋ฐ˜๋ณต X
            .debounce(.seconds(1), scheduler: MainScheduler.instance)
            .skip(1)
            .bind(to: viewModel.vinylName)
            .disposed(by: disposeBag)

2๏ธโƒฃ ViewModel ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด, VinylName์œผ๋กœ ๊ฒ€์ƒ‰ ํ†ต์‹  ์ง„ํ–‰ ๋ฐ ์ŠคํŠธ๋ฆผ ์ƒ์„ฑ

//ViewModel
init(searchAPIService: VinylAPIServiceProtocol = VinylAPIService()) {
        self.searchAPIService = searchAPIService
        _ = vinylName
            .flatMapLatest{ [unowned self] vinyl -> Observable<[SearchModel.Data?]> in
                return self.searchAPIService.searchVinyl(vinylName: vinyl)
            }
            .bind(to: vinylsData)
            .disposed(by: disposeBag)
}

3๏ธโƒฃ ํ†ต์‹ ๋œ Data๋ฅผ ํ†ตํ•ด TableView Update

//View
viewModel.vinylsData
            .observeOn(MainScheduler.instance) // UI ์—…๋ฐ์ดํŠธ๋Š” ๋ฉ”์ธ์“ฐ๋ ˆ๋“œ์—์„œ ์ด๋ฃจ์–ด์ง€๋„๋ก
            .catchErrorJustReturn([])
            .bind(to: searchTableView.rx.items) { tableView, index, element in
                //Cell Vinyl๊ด€๋ จ UI์š”์†Œ ์—…๋ฐ์ดํŠธ
                return cell
            }.disposed(by: disposeBag)

About

๐Ÿงฟ๊ตญ๋‚ด ๋ฐ ํ•ด์™ธ ๋ฐ”์ด๋‹(LP) ์ˆ˜์ง‘๊ฐ€๋ฅผ ์œ„ํ•œ ์„œ๋น„์Šค ๐Ÿงฟ

Resources

Stars

Watchers

Forks

Packages

No packages published