Swift์—์„œ Reactive Programming: RxSwift ์†Œ๊ฐœ ๐Ÿš€

์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - Swift์—์„œ Reactive Programming: RxSwift ์†Œ๊ฐœ ๐Ÿš€

 

 

์•ˆ๋…•ํ•˜์„ธ์š”, Swift ๊ฐœ๋ฐœ์ž ์—ฌ๋Ÿฌ๋ถ„! ์˜ค๋Š˜์€ iOS ์•ฑ ๊ฐœ๋ฐœ์˜ ์ƒˆ๋กœ์šด ํŒจ๋Ÿฌ๋‹ค์ž„์„ ์—ด์–ด์ค„ RxSwift์— ๋Œ€ํ•ด ๊นŠ์ด ์žˆ๊ฒŒ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. Reactive Programming์€ ํ˜„๋Œ€ ์•ฑ ๊ฐœ๋ฐœ์—์„œ ์ ์  ๋” ์ค‘์š”ํ•ด์ง€๊ณ  ์žˆ๋Š” ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจ๋Ÿฌ๋‹ค์ž„์ธ๋ฐ์š”, ํŠนํžˆ Swift์™€ ๊ฐ™์€ ํ˜„๋Œ€์ ์ธ ์–ธ์–ด์—์„œ ๊ทธ ์ง„๊ฐ€๋ฅผ ๋ฐœํœ˜ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ

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

ย 

์ด ๊ธ€์—์„œ๋Š” RxSwift์˜ ๊ธฐ๋ณธ ๊ฐœ๋…๋ถ€ํ„ฐ ์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€, ๊ทธ๋ฆฌ๊ณ  ๊ณ ๊ธ‰ ๊ธฐ์ˆ ๊นŒ์ง€ ํญ๋„“๊ฒŒ ๋‹ค๋ฃฐ ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. Swift ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ˆ„๊ตฌ๋‚˜ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋„๋ก ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•˜๊ฒ ์ง€๋งŒ, ๋™์‹œ์— ์‹ค์šฉ์ ์ด๊ณ  ๊นŠ์ด ์žˆ๋Š” ๋‚ด์šฉ์„ ๋‹ด์•„๋‚ด๋ ค๊ณ  ๋…ธ๋ ฅํ–ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ‘จโ€๐Ÿ’ป๐Ÿ‘ฉโ€๐Ÿ’ป

์žฌ๋Šฅ๋„ท์˜ '์ง€์‹์ธ์˜ ์ˆฒ'์—์„œ ์—ฌ๋Ÿฌ๋ถ„๊ณผ ํ•จ๊ป˜ RxSwift์˜ ์„ธ๊ณ„๋ฅผ ํƒํ—˜ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด ๊ธ€์„ ํ†ตํ•ด ์—ฌ๋Ÿฌ๋ถ„์˜ iOS ์•ฑ ๊ฐœ๋ฐœ ์Šคํ‚ฌ์ด ํ•œ ๋‹จ๊ณ„ ๋” ์„ฑ์žฅํ•˜๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค. ์ž, ๊ทธ๋Ÿผ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ์š”? ๐ŸŒŸ

1. Reactive Programming์˜ ๊ธฐ๋ณธ ๊ฐœ๋… ์ดํ•ดํ•˜๊ธฐ ๐Ÿง 

1.1 Reactive Programming์ด๋ž€?

Reactive Programming์€ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ๊ณผ ๋ณ€ํ™”์˜ ์ „ํŒŒ์— ์ค‘์ ์„ ๋‘” ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจ๋Ÿฌ๋‹ค์ž„์ž…๋‹ˆ๋‹ค. ์ด๋Š” ๊ธฐ์กด์˜ ๋ช…๋ นํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ๊ณผ๋Š” ๋‹ค๋ฅธ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์ทจํ•˜๋Š”๋ฐ, ์ฃผ๋กœ ์ด๋ฒคํŠธ๋‚˜ ๋ฐ์ดํ„ฐ์˜ ๋ณ€ํ™”์— '๋ฐ˜์‘'ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ”„๋กœ๊ทธ๋žจ์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

ย 

๐Ÿ”‘ ํ•ต์‹ฌ ํŠน์ง•:

  • ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์ค‘์‹ฌ
  • ๋ณ€ํ™”์— ๋Œ€ํ•œ ์ž๋™ ์ „ํŒŒ
  • ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ ์ฒ˜๋ฆฌ
  • ์„ ์–ธ์  ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์Šคํƒ€์ผ

ย 

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

1.2 ์™œ Reactive Programming์ธ๊ฐ€?

์ „ํ†ต์ ์ธ ๋ช…๋ นํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์—์„œ๋Š” ์ƒํƒœ ๋ณ€ํ™”์™€ ๊ทธ์— ๋”ฐ๋ฅธ ๋กœ์ง ์ฒ˜๋ฆฌ๊ฐ€ ๋ณต์žกํ•ด์งˆ์ˆ˜๋ก ์ฝ”๋“œ์˜ ๋ณต์žก๋„๊ฐ€ ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด Reactive Programming์€ ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ์šฐ์•„ํ•˜๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

ย 

๐Ÿ“Š Reactive Programming์˜ ์ด์ :

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

ย 

์ด๋Ÿฌํ•œ ์ด์ ๋“ค๋กœ ์ธํ•ด Reactive Programming์€ ํ˜„๋Œ€ ์•ฑ ๊ฐœ๋ฐœ์—์„œ ์ ์  ๋” ์ค‘์š”ํ•œ ์œ„์น˜๋ฅผ ์ฐจ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ๋ณต์žกํ•œ UI ์ƒํ˜ธ์ž‘์šฉ, ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ, ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ ๊ด€๋ฆฌ ๋“ฑ์ด ํ•„์š”ํ•œ ์•ฑ์—์„œ ๊ทธ ์ง„๊ฐ€๋ฅผ ๋ฐœํœ˜ํ•ฉ๋‹ˆ๋‹ค.

1.3 Reactive Programming์˜ ํ•ต์‹ฌ ๊ฐœ๋…

Reactive Programming์„ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ช‡ ๊ฐ€์ง€ ํ•ต์‹ฌ ๊ฐœ๋…์„ ์•Œ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฐœ๋…๋“ค์€ RxSwift๋ฅผ ๋น„๋กฏํ•œ ๋Œ€๋ถ€๋ถ„์˜ Reactive ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ๊ณตํ†ต์ ์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

ย 

๐Ÿ” ์ฃผ์š” ๊ฐœ๋…:

  1. Observable (๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด): ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ธฐ๋ณธ ๋‹จ์œ„์ž…๋‹ˆ๋‹ค. ์‹œ๊ฐ„์— ๋”ฐ๋ผ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋‚˜ ๊ฐ’์˜ ์‹œํ€€์Šค๋ฅผ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  2. Observer (๊ด€์ฐฐ์ž): Observable์„ ๊ตฌ๋…ํ•˜๊ณ  ๋ฐœํ–‰๋œ ํ•ญ๋ชฉ์— ๋ฐ˜์‘ํ•˜๋Š” ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.
  3. Operator (์—ฐ์‚ฐ์ž): Observable์„ ๋ณ€ํ™˜, ํ•„ํ„ฐ๋ง, ๊ฒฐํ•ฉํ•˜๋Š” ๋“ฑ์˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
  4. Scheduler (์Šค์ผ€์ค„๋Ÿฌ): ์ž‘์—…์ด ์‹คํ–‰๋  ์Šค๋ ˆ๋“œ๋‚˜ ์‹คํ–‰ ์ปจํ…์ŠคํŠธ๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
  5. Subscription (๊ตฌ๋…): Observable๊ณผ Observer๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๊ณผ์ •์ž…๋‹ˆ๋‹ค.

ย 

์ด๋Ÿฌํ•œ ๊ฐœ๋…๋“ค์ด ์–ด๋–ป๊ฒŒ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š”์ง€ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด Reactive Programming์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž์˜ ๋ฒ„ํŠผ ํƒญ ์ด๋ฒคํŠธ๋ฅผ Observable๋กœ ํ‘œํ˜„ํ•˜๊ณ , ์ด๋ฅผ Observer๊ฐ€ ๊ตฌ๋…ํ•˜์—ฌ ๋ฐ˜์‘ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ”„๋กœ๊ทธ๋žจ์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1.4 Reactive Programming vs ์ „ํ†ต์ ์ธ ํ”„๋กœ๊ทธ๋ž˜๋ฐ

Reactive Programming๊ณผ ์ „ํ†ต์ ์ธ ๋ช…๋ นํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์˜ ์ฐจ์ด๋ฅผ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์€ ๋งค์šฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๋ฐฉ์‹์˜ ๋น„๊ต๋ฅผ ํ†ตํ•ด Reactive Programming์˜ ์žฅ์ ์„ ๋” ๋ช…ํ™•ํžˆ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ย 

๐Ÿ”„ ๋น„๊ต:

ํŠน์„ฑ ์ „ํ†ต์ ์ธ ํ”„๋กœ๊ทธ๋ž˜๋ฐ Reactive Programming
๋ฐ์ดํ„ฐ ํ๋ฆ„ ๋ช…์‹œ์ ์ธ ์ƒํƒœ ๋ณ€๊ฒฝ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ ์ค‘์‹ฌ
๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์ฝœ๋ฐฑ, ํ”„๋กœ๋ฏธ์Šค ๋“ฑ ์‚ฌ์šฉ ์ŠคํŠธ๋ฆผ ๊ธฐ๋ฐ˜ ์ฒ˜๋ฆฌ
์ฝ”๋“œ ์Šคํƒ€์ผ ๋ช…๋ นํ˜• (How) ์„ ์–ธํ˜• (What)
์ƒํƒœ ๊ด€๋ฆฌ ๋ช…์‹œ์  ์ƒํƒœ ๋ณ€๊ฒฝ ๋ถˆ๋ณ€์„ฑ๊ณผ ์ˆœ์ˆ˜ ํ•จ์ˆ˜ ๊ฐ•์กฐ
์—๋Ÿฌ ์ฒ˜๋ฆฌ try-catch ๋ธ”๋ก ์ŠคํŠธ๋ฆผ์˜ ์ผ๋ถ€๋กœ ์ฒ˜๋ฆฌ

ย 

์ด๋Ÿฌํ•œ ์ฐจ์ด์ ๋“ค๋กœ ์ธํ•ด Reactive Programming์€ ํŠนํžˆ ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ์ž‘์—…์ด๋‚˜ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ์ƒํ™ฉ์—์„œ ํฐ ๊ฐ•์ ์„ ๋ฐœํœ˜ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ํ•™์Šต ๊ณก์„ ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ํŒ€์˜ ์ƒํ™ฉ๊ณผ ํ”„๋กœ์ ํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ์„ ๊ณ ๋ คํ•˜์—ฌ ์ ์ ˆํžˆ ๋„์ž…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

2. RxSwift: Swift๋ฅผ ์œ„ํ•œ Reactive Extensions ๐Ÿ› ๏ธ

2.1 RxSwift ์†Œ๊ฐœ

RxSwift๋Š” Swift ์–ธ์–ด๋ฅผ ์œ„ํ•œ Reactive Extensions์˜ ๊ตฌํ˜„์ฒด์ž…๋‹ˆ๋‹ค. Reactive Programming์˜ ๊ฐœ๋…์„ Swift ์ƒํƒœ๊ณ„์— ๋„์ž…ํ•˜์—ฌ, iOS ๋ฐ macOS ์•ฑ ๊ฐœ๋ฐœ์—์„œ ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ๋”์šฑ ์‰ฝ๊ณ  ํšจ์œจ์ ์œผ๋กœ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

ย 

๐ŸŒŸ RxSwift์˜ ์ฃผ์š” ํŠน์ง•:

  • Swift์˜ ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์‹œ์Šคํ…œ๊ณผ ์™„๋ฒฝํ•˜๊ฒŒ ํ†ตํ•ฉ
  • ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์˜ ์„ ์–ธ์  ์ฒ˜๋ฆฌ
  • ํ’๋ถ€ํ•œ ์—ฐ์‚ฐ์ž ์„ธํŠธ๋กœ ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๊ฐ€๋Šฅ
  • UI ๋ฐ”์ธ๋”ฉ์„ ์œ„ํ•œ RxCocoa ์ œ๊ณต
  • ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ ํ–ฅ์ƒ

ย 

RxSwift๋Š” ๋‹จ์ˆœํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋„˜์–ด์„œ iOS ์•ฑ ๊ฐœ๋ฐœ์˜ ์ƒˆ๋กœ์šด ํŒจ๋Ÿฌ๋‹ค์ž„์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ์ž‘์—…, UI ์—…๋ฐ์ดํŠธ, ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋“ฑ์„ ๋”์šฑ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.

2.2 RxSwift์˜ ๊ธฐ๋ณธ ๊ตฌ์„ฑ ์š”์†Œ

RxSwift๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๊ทธ ๊ธฐ๋ณธ ๊ตฌ์„ฑ ์š”์†Œ๋“ค์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋“ค์€ Reactive Programming์˜ ํ•ต์‹ฌ ๊ฐœ๋…์„ Swift ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ๊ตฌํ˜„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ย 

๐Ÿงฉ ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ:

  1. Observable<T>: ์‹œ๊ฐ„์— ๋”ฐ๋ผ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์‹œํ€€์Šค์ž…๋‹ˆ๋‹ค. T ํƒ€์ž…์˜ ์š”์†Œ๋ฅผ ๋ฐฉ์ถœํ•ฉ๋‹ˆ๋‹ค.
  2. Observer: Observable์„ ๊ตฌ๋…ํ•˜๊ณ  ์ด๋ฒคํŠธ์— ๋ฐ˜์‘ํ•˜๋Š” ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.
  3. Disposable: ๊ตฌ๋…์„ ์ทจ์†Œํ•˜๊ณ  ๋ฆฌ์†Œ์Šค๋ฅผ ํ•ด์ œํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  4. Subjects: Observable์ด์ž Observer ์—ญํ• ์„ ๋™์‹œ์— ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๋ธŒ๋ฆฟ์ง€ ๋˜๋Š” ํ”„๋ก์‹œ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.
  5. Schedulers: ์ž‘์—…์ด ์‹คํ–‰๋  ์ปจํ…์ŠคํŠธ(์˜ˆ: ๋ฉ”์ธ ์Šค๋ ˆ๋“œ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ)๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

ย 

์ด๋Ÿฌํ•œ ๊ตฌ์„ฑ ์š”์†Œ๋“ค์ด ์–ด๋–ป๊ฒŒ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š”์ง€ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด RxSwift ๋งˆ์Šคํ„ฐ์˜ ์ฒซ ๊ฑธ์Œ์ž…๋‹ˆ๋‹ค. ๊ฐ ์š”์†Œ๋“ค์€ Reactive Programming์˜ ํ•ต์‹ฌ ๊ฐœ๋…์„ Swift์˜ ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์‹œ์Šคํ…œ๊ณผ ๊ฒฐํ•ฉํ•˜์—ฌ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

2.3 Observable ์‹ฌ์ธต ํƒ๊ตฌ

Observable์€ RxSwift์˜ ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์‹œ๊ฐ„์— ๋”ฐ๋ผ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ์˜ ์‹œํ€€์Šค๋ฅผ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์˜ ๊ธฐ๋ณธ ๋‹จ์œ„์ž…๋‹ˆ๋‹ค.

ย 

๐Ÿ”ฌ Observable์˜ ์ฃผ์š” ํŠน์ง•:

  • ํƒ€์ž… ์•ˆ์ •์„ฑ: Generic์„ ์‚ฌ์šฉํ•˜์—ฌ ํŠน์ • ํƒ€์ž…์˜ ์ด๋ฒคํŠธ๋งŒ ๋ฐฉ์ถœ
  • ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ: ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ‘œํ˜„
  • ๋‹ค์–‘ํ•œ ์ƒ์„ฑ ๋ฐฉ๋ฒ•: just, from, create ๋“ฑ ๋‹ค์–‘ํ•œ ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ์ œ๊ณต
  • ์—ฐ์‚ฐ์ž ์ฒด์ด๋‹: map, filter ๋“ฑ์˜ ์—ฐ์‚ฐ์ž๋ฅผ ํ†ตํ•œ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋ฐ ์ฒ˜๋ฆฌ

ย 

Observable์€ ์„ธ ๊ฐ€์ง€ ์œ ํ˜•์˜ ์ด๋ฒคํŠธ๋ฅผ ๋ฐฉ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. Next: ์ƒˆ๋กœ์šด ์š”์†Œ๋ฅผ ๋ฐฉ์ถœ
  2. Error: ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋ฐฉ์ถœ๋˜๋ฉฐ, ์‹œํ€€์Šค๋ฅผ ์ข…๋ฃŒ
  3. Completed: ์‹œํ€€์Šค์˜ ์ •์ƒ์ ์ธ ์ข…๋ฃŒ๋ฅผ ๋‚˜ํƒ€๋ƒ„

ย 

๋‹ค์Œ์€ ๊ฐ„๋‹จํ•œ Observable ์ƒ์„ฑ ๋ฐ ์‚ฌ์šฉ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค:


let observable = Observable.of(1, 2, 3)

observable.subscribe(onNext: { element in
    print(element)
}, onError: { error in
    print("An error occurred: \(error)")
}, onCompleted: {
    print("Completed")
})

์ด ์˜ˆ์ œ์—์„œ๋Š” 1, 2, 3์„ ์ˆœ์„œ๋Œ€๋กœ ๋ฐฉ์ถœํ•˜๋Š” Observable์„ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ๊ฐ ์ด๋ฒคํŠธ์— ๋Œ€ํ•ด ์ ์ ˆํžˆ ๋ฐ˜์‘ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

2.4 Subjects: Observable๊ณผ Observer์˜ ๊ฒฐํ•ฉ

Subject๋Š” RxSwift์—์„œ ๋งค์šฐ ํŠน๋ณ„ํ•œ ์กด์žฌ์ž…๋‹ˆ๋‹ค. ์ด๋Š” Observable์ธ ๋™์‹œ์— Observer์˜ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์–ด, ์–‘๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

ย 

๐Ÿ”€ Subject์˜ ์ฃผ์š” ์œ ํ˜•:

  1. PublishSubject: ๊ตฌ๋… ์ดํ›„์— ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋งŒ ๋ฐฉ์ถœ
  2. BehaviorSubject: ๊ฐ€์žฅ ์ตœ๊ทผ ์ด๋ฒคํŠธ(๋˜๋Š” ์ดˆ๊ธฐ๊ฐ’)์™€ ์ดํ›„ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ ๋ฐฉ์ถœ
  3. ReplaySubject: ๋ฒ„ํผ ํฌ๊ธฐ๋งŒํผ์˜ ์ด์ „ ์ด๋ฒคํŠธ์™€ ์ดํ›„ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ ๋ฐฉ์ถœ
  4. AsyncSubject: ์™„๋ฃŒ ์ง์ „์˜ ๋งˆ์ง€๋ง‰ ์ด๋ฒคํŠธ๋งŒ ๋ฐฉ์ถœ

ย 

Subject๋Š” ์—ฌ๋Ÿฌ Observable์„ ํ•˜๋‚˜๋กœ ํ•ฉ์น˜๊ฑฐ๋‚˜, ์™ธ๋ถ€ ์ด๋ฒคํŠธ๋ฅผ Observable ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ์€ PublishSubject์˜ ๊ฐ„๋‹จํ•œ ์‚ฌ์šฉ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค:


let subject = PublishSubject<String>()

subject.onNext("Hello")

let subscription = subject.subscribe(onNext: { string in
    print(string)
})

subject.onNext("World")
subject.onNext("RxSwift")

subscription.dispose()

์ด ์˜ˆ์ œ์—์„œ "Hello"๋Š” ๊ตฌ๋… ์ด์ „์— ๋ฐœ์ƒํ–ˆ์œผ๋ฏ€๋กœ ์ถœ๋ ฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. "World"์™€ "RxSwift"๋งŒ ์ฝ˜์†”์— ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.

2.5 Operators: ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ ๋ณ€ํ™˜์˜ ๋งˆ๋ฒ•

RxSwift์˜ ์—ฐ์‚ฐ์ž(Operators)๋Š” Observable ์‹œํ€€์Šค๋ฅผ ๋ณ€ํ™˜ํ•˜๊ณ  ์กฐ์ž‘ํ•˜๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ๋กœ์ง์„ ๊ฐ„๊ฒฐํ•˜๊ณ  ์„ ์–ธ์ ์ธ ๋ฐฉ์‹์œผ๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ย 

๐Ÿ”ง ์ฃผ์š” ์—ฐ์‚ฐ์ž ์นดํ…Œ๊ณ ๋ฆฌ:

  • ๋ณ€ํ™˜ ์—ฐ์‚ฐ์ž: map, flatMap, scan ๋“ฑ
  • ํ•„ํ„ฐ๋ง ์—ฐ์‚ฐ์ž: filter, take, skip ๋“ฑ
  • ๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž: merge, zip, combineLatest ๋“ฑ
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ์—ฐ์‚ฐ์ž: catch, retry ๋“ฑ
  • ์œ ํ‹ธ๋ฆฌํ‹ฐ ์—ฐ์‚ฐ์ž: delay, timeout, do ๋“ฑ

ย 

์—ฐ์‚ฐ์ž๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ๋ช‡ ๊ฐ€์ง€ ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค:


let disposeBag = DisposeBag()

Observable.of(1, 2, 3, 4, 5)
    .filter { $0 % 2 == 0 }
    .map { $0 * 2 }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

// ์ถœ๋ ฅ: 4, 8

์ด ์˜ˆ์ œ์—์„œ๋Š” ์ง์ˆ˜๋งŒ ํ•„ํ„ฐ๋งํ•œ ํ›„ 2๋ฅผ ๊ณฑํ•˜๋Š” ๊ฐ„๋‹จํ•œ ๋ณ€ํ™˜์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

ย 

๋” ๋ณต์žกํ•œ ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:


let subject1 = PublishSubject<Int>()
let subject2 = PublishSubject<Int>()

Observable.combineLatest(subject1, subject2) { ($0, $1) }
    .filter { $0.0 % 2 == 0 && $0.1 % 2 == 0 }
    .map { $0.0 + $0.1 }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

subject1.onNext(1)
subject2.onNext(2)
subject1.onNext(2)  // ์ถœ๋ ฅ: 4
subject2.onNext(4)  // ์ถœ๋ ฅ: 6
subject1.onNext(4)  // ์ถœ๋ ฅ: 8

์ด ์˜ˆ์ œ์—์„œ๋Š” ๋‘ ๊ฐœ์˜ Subject๋ฅผ ๊ฒฐํ•ฉํ•˜๊ณ , ๋‘˜ ๋‹ค ์ง์ˆ˜์ผ ๋•Œ๋งŒ ๊ทธ ํ•ฉ์„ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. ์ด์ฒ˜๋Ÿผ ์—ฐ์‚ฐ์ž๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2.6 Schedulers: ๋™์‹œ์„ฑ ์ œ์–ด์˜ ํ•ต์‹ฌ

Scheduler๋Š” RxSwift์—์„œ ์ž‘์—…์ด ์‹คํ–‰๋  ์ปจํ…์ŠคํŠธ๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ์ค‘์š”ํ•œ ์š”์†Œ์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด Observable ์‹œํ€€์Šค์˜ ์‹คํ–‰ ์‹œ์ ๊ณผ ์Šค๋ ˆ๋“œ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ย 

โฐ ์ฃผ์š” Scheduler ์œ ํ˜•:

  • MainScheduler: UI ์—…๋ฐ์ดํŠธ์™€ ๊ฐ™์€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์ž‘์—…์— ์‚ฌ์šฉ
  • SerialDispatchQueueScheduler: ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ง๋ ฌ ํ์—์„œ ์ž‘์—… ์‹คํ–‰
  • ConcurrentDispatchQueueScheduler: ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™์‹œ ํ์—์„œ ์ž‘์—… ์‹คํ–‰
  • OperationQueueScheduler: NSOperationQueue๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ์Šค์ผ€์ค„๋Ÿฌ

ย 

Scheduler๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž‘์—…์˜ ์‹คํ–‰ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์–ด, ์•ฑ์˜ ๋ฐ˜์‘์„ฑ๊ณผ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์€ Scheduler ์‚ฌ์šฉ์˜ ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค:


let disposeBag = DisposeBag()

Observable.of(1, 2, 3, 4, 5)
    .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
    .map { $0 * 2 }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { number in
        print("Result: \(number) on thread: \(Thread.current)")
    })
    .disposed(by: disposeBag)

์ด ์˜ˆ์ œ์—์„œ๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•œ ํ›„, ๊ฒฐ๊ณผ๋ฅผ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” UI ์—…๋ฐ์ดํŠธ์™€ ๊ฐ™์€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ํŠนํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

ย 

Scheduler๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์ ์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์•ฑ์˜ ๋ฐ˜์‘์„ฑ ํ–ฅ์ƒ
  • ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…๊ณผ UI ์—…๋ฐ์ดํŠธ์˜ ๋ช…ํ™•ํ•œ ๋ถ„๋ฆฌ
  • ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ์ž‘์—…์˜ ๊ฐ„ํŽธํ•œ ๊ด€๋ฆฌ
  • ์Šค๋ ˆ๋“œ ์•ˆ์ „์„ฑ ๋ณด์žฅ

Scheduler์˜ ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ์€ RxSwift๋ฅผ ์ด์šฉํ•œ ํšจ์œจ์ ์ธ ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค.

3. RxSwift ์‹ค์ „ ํ™œ์šฉ: ์‹ค์ œ ์‚ฌ๋ก€์™€ ํŒจํ„ด ๐Ÿš€

3.1 ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ฒ˜๋ฆฌํ•˜๊ธฐ

RxSwift๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๋”์šฑ ์šฐ์•„ํ•˜๊ณ  ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ์ ์ธ ๋„คํŠธ์›Œํฌ ์ž‘์—…์„ Observable ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ย 

๐ŸŒ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์˜ˆ์ œ:


struct User: Codable {
    let id: Int
    let name: String
}

class NetworkService {
    static let shared = NetworkService()
    private init() {}
    
    func fetchUser(id: Int) -> Observable<User> {
        return Observable.create { observer in
            let task = URLSession.shared.dataTask(with: URL(string: "https://api.example.com/users/\(id)")!) { data, response, error in
                if let error = error {
                    observer.onError(error)
                    return
                }
                
                guard let data = data else {
                    observer.onError(NSError(domain: "No Data", code: 0, userInfo: nil))
                    return
                }
                
                do {
                    let user = try JSONDecoder().decode(User.self, from: data)
                    observer.onNext(user)
                    observer.onCompleted()
                } catch {
                    observer.onError(error)
                }
            }
            
            task.resume()
            
            return Disposables.create {
                task.cancel()
            }
        }
    }
}

// ์‚ฌ์šฉ ์˜ˆ
NetworkService.shared.fetchUser(id: 1)
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { user in
        print("User: \(user.name)")
    }, onError: { error in
        print("Error: \(error)")
    })
    .disposed(by: disposeBag)

์ด ์˜ˆ์ œ์—์„œ๋Š” ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ Observable๋กœ ๋ž˜ํ•‘ํ•˜์—ฌ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์„ ์–ธ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์—๋Ÿฌ ์ฒ˜๋ฆฌ์™€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ์˜ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ๋„ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3.2 UI ๋ฐ”์ธ๋”ฉ๊ณผ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ

RxCocoa์™€ ํ•จ๊ป˜ RxSwift๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด UI ์ปดํฌ๋„ŒํŠธ์™€ ๋ฐ์ดํ„ฐ๋ฅผ ์‰ฝ๊ฒŒ ๋ฐ”์ธ๋”ฉํ•˜๊ณ , ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ย 

๐Ÿ–ผ๏ธ UI ๋ฐ”์ธ๋”ฉ ์˜ˆ์ œ:


class SearchViewController: UIViewController {
    @IBOutlet weak var searchBar: UISearchBar!
    @IBOutlet weak var tableView: UITableView!
    
    let disposeBag = DisposeBag()
    let viewModel = SearchViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ๊ฒ€์ƒ‰์–ด๋ฅผ ViewModel์— ๋ฐ”์ธ๋”ฉ
        searchBar.rx.text.orEmpty
            .debounce(.milliseconds(300), scheduler: MainScheduler.instance)
            .distinctUntilChanged()
            .bind(to: viewModel.searchTerm)
            .disposed(by: disposeBag)
        
        // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ TableView์— ๋ฐ”์ธ๋”ฉ
        viewModel.searchResults
            .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, item, cell) in
                cell.textLabel?.text = item
            }
            .disposed(by: disposeBag)
        
        // TableView ์„ ํƒ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
        tableView.rx.itemSelected
            .subscribe(onNext: { [weak self] indexPath in
                self?.tableView.deselectRow(at: indexPath, animated: true)
                let item = self?.viewModel.searchResults.value[indexPath.row]
                print("Selected: \(item ?? "")")
            })
            .disposed(by: disposeBag)
    }
}

class SearchViewModel {
    let searchTerm = BehaviorRelay<String>(value: "")
    let searchResults = BehaviorRelay<[String]>(value: [])
    
    private let disposeBag = DisposeBag()
    
    init() {
        searchTerm
            .flatMapLatest { term -> Observable<[String]> in
                // ์‹ค์ œ๋กœ๋Š” ์—ฌ๊ธฐ์„œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
                return Observable.just(["Result 1 for \(term)", "Result 2 for \(term)", "Result 3 for \(term)"])
            }
            .bind(to: searchResults)
            .disposed(by: disposeBag)
    }
}

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

3.3 MVVM ์•„ํ‚คํ…์ฒ˜์™€ RxSwift

RxSwift๋Š” MVVM(Model-View-ViewModel) ์•„ํ‚คํ…์ฒ˜์™€ ํŠนํžˆ ์ž˜ ์–ด์šธ๋ฆฝ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ๊ณผ ๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์˜ ํŠน์„ฑ์ด MVVM์˜ ํ•ต์‹ฌ ๊ฐœ๋…๊ณผ ์ž˜ ๋งž๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

ย 

๐Ÿ—๏ธ MVVM + RxSwift ์˜ˆ์ œ:


// Model
struct Todo: Codable {
    let id: Int
    let title: String
    var isCompleted: Bool
}

// ViewModel
class TodoListViewModel {
    let todos = BehaviorRelay<[Todo]>(value: [])
    let isLoading = BehaviorRelay<Bool>(value: false)
    
    private let disposeBag = DisposeBag()
    
    func fetchTodos() {
        isLoading.accept(true)
        
        // ์‹ค์ œ๋กœ๋Š” ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค
        Observable.just([
            Todo(id: 1, title: "Buy groceries", isCompleted: false),
            Todo(id: 2, title: "Do laundry", isCompleted: true),
            Todo(id: 3, title: "Clean room", isCompleted: false)
        ])
        .delay(.seconds(2), scheduler: MainScheduler.instance) // ๋„คํŠธ์›Œํฌ ์ง€์—ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
        .subscribe(onNext: { [weak self] fetchedTodos in
            self?.todos.accept(fetchedTodos)
            self?.isLoading.accept(false)
        })
        .disposed(by: disposeBag)
    }
    
    func toggleTodo(at index: Int) {
        var currentTodos = todos.value
        currentTodos[index].isCompleted.toggle()
        todos.accept(currentTodos)
    }
}

// View
class TodoListViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    
    let viewModel = TodoListViewModel()
    let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupBindings()
        viewModel.fetchTodos()
    }
    
    private func setupBindings() {
        viewModel.todos
            .bind(to: tableView.rx.items(cellIdentifier: "TodoCell", cellType: UITableViewCell.self)) { (row, todo, cell) in
                cell.textLabel?.text = todo.title
                cell.accessoryType = todo.isCompleted ? .checkmark : .none
            }
            .disposed(by: disposeBag)
        
        viewModel.isLoading
            .bind(to: activityIndicator.rx.isAnimating)
            .disposed(by: disposeBag)
        
        tableView.rx.itemSelected
            .subscribe(onNext: { [weak self] indexPath in
                self?.viewModel.toggleTodo(at: indexPath.row)
            })
            .disposed(by: disposeBag)
    }
}

์ด ์˜ˆ์ œ์—์„œ๋Š” MVVM ์•„ํ‚คํ…์ฒ˜๋ฅผ RxSwift์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ Todo ๋ฆฌ์ŠคํŠธ ์•ฑ์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ViewModel์€ ๋ฐ์ดํ„ฐ์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ณ , View๋Š” UI ์—…๋ฐ์ดํŠธ์™€ ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. RxSwift๋ฅผ ํ†ตํ•œ ๋ฐ”์ธ๋”ฉ์œผ๋กœ ๋‘ ๊ณ„์ธต ๊ฐ„์˜ ํ†ต์‹ ์ด ๊ฐ„๊ฒฐํ•˜๊ณ  ํšจ์œจ์ ์œผ๋กœ ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค.

3.4 ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ์ž‘์—… ๊ด€๋ฆฌ

RxSwift๋Š” ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ์ž‘์—…์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ํŠนํžˆ ๊ฐ•๋ ฅํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์กฐํ•ฉํ•˜๊ณ  ์ˆœ์„œ๋ฅผ ์ œ์–ดํ•˜๋Š” ๋ฐ ์œ ์šฉํ•œ ์—ฐ์‚ฐ์ž๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

ย 

๐Ÿ”„ ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ์ž‘์—… ์˜ˆ์ œ:


struct User: Codable {
    let id: Int
    let name: String
}

struct Post: Codable {
    let id: Int
    let title: String
    let body: String
}

class NetworkService {
    static let shared = NetworkService()
    private init() {}
    
    func fetchUser(id: Int) -> Observable<User> {
        // ์‹ค์ œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋Œ€์‹  ์‹œ๋ฎฌ๋ ˆ์ด์…˜
        return Observable.just(User(id: id, name: "User \(id)"))
            .delay(.seconds(1), scheduler: MainScheduler.instance)
    }
    
    func fetchPosts(for userId: Int) -> Observable<[Post]> {
        // ์‹ค์ œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋Œ€์‹  ์‹œ๋ฎฌ๋ ˆ์ด์…˜
        return Observable.just([
            Post(id: 1, title: "Post 1", body: "Body 1"),
            Post(id: 2, title: "Post 2", body: "Body 2")
        ])
        .delay(.seconds(1), scheduler: MainScheduler.instance)
    }
}

class UserPostsViewModel {
    let userId: Int
    let user = PublishSubject<User>()
    let posts = PublishSubject<[Post]>()
    let isLoading = BehaviorRelay<Bool>(value: false)
    let error = PublishSubject<Error>()
    
    private let disposeBag = DisposeBag()
    
    init(userId: Int) {
        self.userId = userId
    }
    
    func fetchUserAndPosts() {
        isLoading.accept(true)
        
        let userObservable = NetworkService.shared.fetchUser(id: userId)
        let postsObservable = userObservable.flatMap { user -> Observable<[Post]> in
            return NetworkService.shared.fetchPosts(for: user.id)
        }
        
        Observable.zip(userObservable, postsObservable)
            .observe(on: MainScheduler.instance)
            .do(onNext: { [weak self] _ in self?.isLoading.accept(false) },
                onError: { [weak self] _ in self?.isLoading.accept(false) })
            .subscribe(onNext: { [weak self] (user, posts) in
                self?.user.onNext(user)
                self?.posts.onNext(posts)
            }, onError: { [weak self] error in
                self?.error.onNext(error)
            })
            .disposed(by: disposeBag)
    }
}

// ์‚ฌ์šฉ ์˜ˆ
let viewModel = UserPostsViewModel(userId: 1)

viewModel.isLoading
    .subscribe(onNext: { isLoading in
        print("Is loading: \(isLoading)")
    })
    .disposed(by: disposeBag)

viewModel.user
    .subscribe(onNext: { user in
        print("Fetched user: \(user.name)")
    })
    .disposed(by: disposeBag)

viewModel.posts
    .subscribe(onNext: { posts in
        print("Fetched \(posts.count) posts")
    })
    .disposed(by: disposeBag)

viewModel.error
    .subscribe(onNext: { error in
        print("Error occurred: \(error)")
    })
    .disposed(by: disposeBag)

viewModel.fetchUserAndPosts()

์ด ์˜ˆ์ œ์—์„œ๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ ํ›„ ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ๊ฒŒ์‹œ๋ฌผ์„ ๊ฐ€์ ธ์˜ค๋Š” ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ์ž‘์—…์„ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. RxSwift์˜ `flatMap`๊ณผ `zip` ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋Ÿฌํ•œ ์—ฐ์†์ ์ธ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ๋กœ๋”ฉ ์ƒํƒœ์™€ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋„ ๋ฐ˜์‘ํ˜•์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

4. RxSwift์˜ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๊ณผ ์ตœ์ ํ™” ๐Ÿš€

4.1 ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ์™€ Dispose ํŒจํ„ด

RxSwift๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋Š” ๋งค์šฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ ๊ตฌ๋…(Subscription)์ด ๋” ์ด์ƒ ํ•„์š”ํ•˜์ง€ ์•Š์„ ๋•Œ ์ ์ ˆํžˆ ํ•ด์ œํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

ย 

๐Ÿ—‘๏ธ Dispose ํŒจํ„ด ์˜ˆ์ œ:


class ExampleViewController: UIViewController {
    private let disposeBag = DisposeBag()
    private var subscription: Disposable?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // DisposeBag์„ ์‚ฌ์šฉํ•œ ์ž๋™ ํ•ด์ œ
        Observable.interval(.seconds(1), scheduler: MainScheduler.instance)
            .subscribe(onNext: { [weak self] _ in
                self?.updateUI()
            })
            .disposed(by: disposeBag)
        
        // ์ˆ˜๋™์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ตฌ๋…
        subscription = Observable.interval(.seconds(5), scheduler: MainScheduler.instance)
            .subscribe(onNext: { [weak self] _ in
                self?.performHeavyTask()
            })
    }
    
    func updateUI() {
        // UI ์—…๋ฐ์ดํŠธ ๋กœ์ง
    }
    
    func performHeavyTask() {
        // ๋ฌด๊ฑฐ์šด ์ž‘์—… ์ˆ˜ํ–‰
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        // ์ˆ˜๋™์œผ๋กœ ๊ตฌ๋… ํ•ด์ œ
        subscription?.dispose()
    }
}

์ด ์˜ˆ์ œ์—์„œ๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์˜ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค:

  1. DisposeBag์„ ์‚ฌ์šฉํ•œ ์ž๋™ ํ•ด์ œ: ViewController๊ฐ€ ํ•ด์ œ๋  ๋•Œ ์ž๋™์œผ๋กœ ๋ชจ๋“  ๊ตฌ๋…์ด ํ•ด์ œ๋ฉ๋‹ˆ๋‹ค.
  2. ์ˆ˜๋™ ๊ด€๋ฆฌ: ํŠน์ • ์‹œ์ (์—ฌ๊ธฐ์„œ๋Š” viewWillDisappear)์— ๋ช…์‹œ์ ์œผ๋กœ ๊ตฌ๋…์„ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค.

์ ์ ˆํ•œ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋Š” ์•ฑ์˜ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ๋ฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

4.2 ์—๋Ÿฌ ์ฒ˜๋ฆฌ์™€ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜

RxSwift์—์„œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” ๋งค์šฐ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. ํŠนํžˆ ๋„คํŠธ์›Œํฌ ์š”์ฒญ๊ณผ ๊ฐ™์€ ์‹คํŒจ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ์ž‘์—…์—์„œ ์ ์ ˆํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ์™€ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

ย 

๐Ÿ”„ ์—๋Ÿฌ ์ฒ˜๋ฆฌ์™€ ์žฌ์‹œ๋„ ์˜ˆ์ œ:


enum NetworkError: Error {
    case serverError
    case connectionError
}

class NetworkService {
    func fetchData() -> Observable<String> {
        return Observable.create { observer in
            // ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
            let success = Bool.random()
            if success {
                observer.onNext("Data fetched successfully")
                observer.onCompleted()
            } else {
                observer.onError(NetworkError.serverError)
            }
            return Disposables.create()
        }
    }
}

let networkService = NetworkService()

networkService.fetchData()
    .retry(3)
    .catch { error -> Observable<String> in
        print("Error occurred: \(error)")
        return Observable.just("Fallback data")
    }
    .subscribe(onNext: { data in
        print(data)
    }, onError: { error in
        print("Final error: \(error)")
    })
    .disposed(by: disposeBag)

์ด ์˜ˆ์ œ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ „๋žต์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:

  1. `retry` ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹คํŒจ ์‹œ ์ตœ๋Œ€ 3๋ฒˆ๊นŒ์ง€ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.
  2. `catch` ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ์žฌ์‹œ๋„๊ฐ€ ์‹คํŒจํ•œ ๊ฒฝ์šฐ ํด๋ฐฑ(fallback) ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฐฉ์‹์œผ๋กœ ๋„คํŠธ์›Œํฌ ๋ถˆ์•ˆ์ • ๋“ฑ์œผ๋กœ ์ธํ•œ ์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๋ฅผ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4.3 ํ…Œ์ŠคํŠธ์™€ ๋ชจ์˜ ๊ฐ์ฒด(Mocking)

RxSwift๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ฝ”๋“œ์˜ ํ…Œ์ŠคํŠธ๋Š” ๋งค์šฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. RxTest์™€ RxBlocking์„ ์‚ฌ์šฉํ•˜๋ฉด ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ย 

๐Ÿงช ํ…Œ์ŠคํŠธ ์˜ˆ์ œ:


import XCTest
import RxSwift
import RxTest
import RxBlocking

class UserViewModel {
    func fetchUser(id: Int) -> Observable<String> {
        // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค
        return Observable.just("User \(id)")
            .delay(.seconds(1), scheduler: MainScheduler.instance)
    }
}

class UserViewModelTests: XCTestCase {
    var viewModel: UserViewModel!
    var scheduler: TestScheduler!
    var disposeBag: DisposeBag!
    
    override func setUp() {
        super.setUp()
        viewModel = UserViewModel()
        scheduler = TestScheduler(initialClock: 0)
        disposeBag = DisposeBag()
    }
    
    func testFetchUser() {
        // Given
        let observer = scheduler.createObserver(String.self)
        
        // When
        scheduler.createColdObservable([.next(10, 1)])
            .flatMap { self.viewModel.fetchUser(id: $0) }
            .bind(to: observer)
            .disposed(by: disposeBag)
        
        scheduler.start()
        
        // Then
        XCTAssertEqual(observer.events, [.next(1010, "User 1"), .completed(1010)])
    }
    
    func testFetchUserBlocking() {
        do {
            let user = try viewModel.fetchUser(id: 1).toBlocking().first()
            XCTAssertEqual(user, "User 1")
        } catch {
            XCTFail("Error occurred: \(error)")
        }
    }
}

์ด ํ…Œ์ŠคํŠธ ์˜ˆ์ œ์—์„œ๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์˜ ํ…Œ์ŠคํŠธ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค:

  1. TestScheduler๋ฅผ ์‚ฌ์šฉํ•œ ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ: ๋น„๋™๊ธฐ ์ž‘์—…์˜ ํƒ€์ด๋ฐ์„ ์ •ํ™•ํ•˜๊ฒŒ ์ œ์–ดํ•˜๊ณ  ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. RxBlocking์„ ์‚ฌ์šฉํ•œ ๋™๊ธฐ์‹ ํ…Œ์ŠคํŠธ: ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋™๊ธฐ์‹์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๊ฐ„๋‹จํ•˜๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ํ…Œ์ŠคํŠธ ๋ฐฉ์‹์„ ํ†ตํ•ด RxSwift๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋น„๋™๊ธฐ ์ฝ”๋“œ์˜ ์ •ํ™•์„ฑ์„ ํšจ๊ณผ์ ์œผ๋กœ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4.4 ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

RxSwift๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๋Š” ๊ฒƒ์€ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ ๋ช‡ ๊ฐ€์ง€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

ย 

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ:

  1. share() ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ: ๋™์ผํ•œ Observable์„ ์—ฌ๋Ÿฌ ๋ฒˆ ๊ตฌ๋…ํ•  ๋•Œ ์ค‘๋ณต ์ž‘์—…์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  2. ๋ถˆํ•„์š”ํ•œ ๊ตฌ๋… ํ”ผํ•˜๊ธฐ: ํ•„์š”ํ•˜์ง€ ์•Š์€ ๊ตฌ๋…์€ ์ฆ‰์‹œ ํ•ด์ œํ•˜์—ฌ ๋ฆฌ์†Œ์Šค๋ฅผ ์ ˆ์•ฝํ•ฉ๋‹ˆ๋‹ค.
  3. ์ ์ ˆํ•œ ์Šค์ผ€์ค„๋Ÿฌ ์‚ฌ์šฉ: ๋ฌด๊ฑฐ์šด ์ž‘์—…์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค์ผ€์ค„๋Ÿฌ์—์„œ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  4. debounce์™€ throttle ํ™œ์šฉ: ๋นˆ๋ฒˆํ•œ ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ ๋ถˆํ•„์š”ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ์ค„์ž…๋‹ˆ๋‹ค.
  5. ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€: ์ˆœํ™˜ ์ฐธ์กฐ๋ฅผ ํ”ผํ•˜๊ณ  DisposeBag์„ ์ ์ ˆํžˆ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

ย 

์˜ˆ์ œ ์ฝ”๋“œ:


class OptimizedViewModel {
    private let searchSubject = PublishSubject<String>()
    private let disposeBag = DisposeBag()
    
    init() {
        setupBindings()
    }
    
    private func setupBindings() {
        let sharedSearch = searchSubject
            .debounce(.milliseconds(300), scheduler: MainScheduler.instance)
            .distinctUntilChanged()
            .share(replay: 1, scope: .whileConnected)
        
        sharedSearch
            .flatMapLatest { [weak self] query -> Observable<[String]> in
                guard let self = self else { return Observable.just([]) }
                return self.performSearch(query)
            }
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] results in
                self?.updateUI(with: results)
            })
            .disposed(by: disposeBag)
    }
    
    func performSearch(_ query: String) -> Observable<[String]> {
        // ์‹ค์ œ ๊ฒ€์ƒ‰ ๋กœ์ง ๊ตฌํ˜„
        return Observable.just(["Result 1", "Result 2"])
            .delay(.seconds(1), scheduler: ConcurrentDispatchQueueScheduler(qos: .background))
    }
    
    func updateUI(with results: [String]) {
        // UI ์—…๋ฐ์ดํŠธ ๋กœ์ง
    }
    
    func search(_ query: String) {
        searchSubject.onNext(query)
    }
}

์ด ์˜ˆ์ œ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

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

์ด๋Ÿฌํ•œ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์ ์šฉํ•˜๋ฉด RxSwift๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์•ฑ์˜ ์„ฑ๋Šฅ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

5. ๊ฒฐ๋ก  ๋ฐ ์ถ”๊ฐ€ ๋ฆฌ์†Œ์Šค ๐Ÿ“š

5.1 RxSwift์˜ ๋ฏธ๋ž˜

RxSwift๋Š” iOS ๊ฐœ๋ฐœ ์ƒํƒœ๊ณ„์—์„œ ์ค‘์š”ํ•œ ์œ„์น˜๋ฅผ ์ฐจ์ง€ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๊ทธ ์ค‘์š”์„ฑ์€ ์•ž์œผ๋กœ๋„ ๊ณ„์†๋  ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์˜ ๋ณต์žก์„ฑ์„ ํšจ๊ณผ์ ์œผ๋กœ ๋‹ค๋ฃจ๋Š” RxSwift์˜ ๋Šฅ๋ ฅ์€ ํ˜„๋Œ€ ์•ฑ ๊ฐœ๋ฐœ์— ๋งค์šฐ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

ย 

๐Ÿ”ฎ RxSwift์˜ ์ „๋ง:

  • SwiftUI์™€์˜ ํ†ตํ•ฉ: RxSwift์™€ SwiftUI๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ๊ด€์‹ฌ์ด ์ฆ๊ฐ€ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  • ์„ฑ๋Šฅ ๊ฐœ์„ : ์ง€์†์ ์ธ ์ตœ์ ํ™”๋กœ ๋”์šฑ ํšจ์œจ์ ์ธ ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์ด ๊ฐ€๋Šฅํ•ด์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  • ํ•™์Šต ์ž๋ฃŒ ์ฆ๊ฐ€: ์ปค๋ฎค๋‹ˆํ‹ฐ์˜ ์„ฑ์žฅ์œผ๋กœ ๋” ๋งŽ์€ ํ•™์Šต ๋ฆฌ์†Œ์Šค๊ฐ€ ์ œ๊ณต๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  • ๊ธฐ์—…์—์„œ์˜ ์ฑ„ํƒ ์ฆ๊ฐ€: ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์—์„œ RxSwift์˜ ์‚ฌ์šฉ์ด ๋”์šฑ ๋ณดํŽธํ™”๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

RxSwift๋Š” ๊ณ„์†ํ•ด์„œ ์ง„ํ™”ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, Swift ์–ธ์–ด์˜ ๋ฐœ์ „๊ณผ ํ•จ๊ป˜ ๋”์šฑ ๊ฐ•๋ ฅํ•ด์งˆ ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค.

5.2 ํ•™์Šต์„ ์œ„ํ•œ ์ถ”๊ฐ€ ๋ฆฌ์†Œ์Šค

RxSwift๋ฅผ ๋” ๊นŠ์ด ์žˆ๊ฒŒ ํ•™์Šตํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ๋‹ค์Œ์˜ ๋ฆฌ์†Œ์Šค๋“ค์„ ์ฐธ๊ณ ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค:

ย 

๐Ÿ“š ์ถ”์ฒœ ํ•™์Šต ์ž๋ฃŒ:

  • ๊ณต์‹ ๋ฌธ์„œ: RxSwift GitHub - ๊ฐ€์žฅ ์ตœ์‹ ์˜ ์ •๋ณด์™€ ์˜ˆ์ œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ์ฑ…: "RxSwift: Reactive Programming with Swift" by raywenderlich.com - RxSwift์˜ ๊ธฐ์ดˆ๋ถ€ํ„ฐ ๊ณ ๊ธ‰ ์ฃผ์ œ๊นŒ์ง€ ๋‹ค๋ฃจ๋Š” ์ข…ํ•ฉ์ ์ธ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค.
  • ์˜จ๋ผ์ธ ๊ฐ•์ขŒ: Udemy, Coursera ๋“ฑ์˜ ํ”Œ๋žซํผ์—์„œ ์ œ๊ณตํ•˜๋Š” RxSwift ๊ด€๋ จ ๊ฐ•์ขŒ๋“ค์„ ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋ธ”๋กœ๊ทธ: Medium์˜ "RxSwift" ํƒœ๊ทธ๋ฅผ ํŒ”๋กœ์šฐํ•˜๋ฉด ๋‹ค์–‘ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์˜ ๊ฒฝํ—˜๊ณผ ํŒ์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ปจํผ๋Ÿฐ์Šค ์˜์ƒ: WWDC, try! Swift ๋“ฑ์˜ ์ปจํผ๋Ÿฐ์Šค์—์„œ ๋ฐœํ‘œ๋œ RxSwift ๊ด€๋ จ ์„ธ์…˜๋“ค์„ ์ฐพ์•„๋ณด์„ธ์š”.
  • ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ: GitHub์—์„œ RxSwift๋ฅผ ์‚ฌ์šฉํ•œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ๋“ค์„ ๋ถ„์„ํ•ด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ํ•™์Šต ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฆฌ์†Œ์Šค๋“ค์„ ํ†ตํ•ด RxSwift์— ๋Œ€ํ•œ ์ดํ•ด๋ฅผ ๊นŠ์ด ์žˆ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๋Š” ๋ฐ ๋„์›€์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

5.4 ๋งˆ๋ฌด๋ฆฌ

RxSwift๋Š” iOS ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์˜ ๋ณต์žก์„ฑ์„ ์ค„์ด๊ณ , ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์ด๋ฉฐ, ๋ฐ˜์‘ํ˜• ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.

ย 

๐ŸŒŸ RxSwift ํ•™์Šต์˜ ์ด์ :

  • ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ์ž‘์—…์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • UI ์ด๋ฒคํŠธ์™€ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ˜„๋Œ€์ ์ธ iOS ์•ฑ ๊ฐœ๋ฐœ ํŠธ๋ Œ๋“œ๋ฅผ ๋”ฐ๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ธฐ์ˆ ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ย 

RxSwift์˜ ํ•™์Šต ๊ณก์„ ์ด ์ฒ˜์Œ์—๋Š” ๊ฐ€ํŒŒ๋ฅด๊ฒŒ ๋Š๊ปด์งˆ ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ทธ ๊ฐœ๋…์„ ์ดํ•ดํ•˜๊ณ  ๋‚˜๋ฉด iOS ์•ฑ ๊ฐœ๋ฐœ์˜ ์ƒˆ๋กœ์šด ์ฐจ์›์„ ๊ฒฝํ—˜ํ•˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ง€์†์ ์ธ ํ•™์Šต๊ณผ ์‹ค์Šต์„ ํ†ตํ•ด RxSwift์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

RxSwift๋กœ์˜ ์—ฌ์ •์„ ์‹œ์ž‘ํ•˜์‹  ์—ฌ๋Ÿฌ๋ถ„๊ป˜ ํ–‰์šด์ด ์žˆ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค. ํ•จ๊ป˜ ๋” ๋‚˜์€ iOS ์•ฑ์„ ๋งŒ๋“ค์–ด ๋‚˜๊ฐ€๋Š” ์—ฌ์ •์„ ์ฆ๊ธฐ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค! ๐Ÿš€