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 ํ๋ ์์ํฌ์์ ๊ณตํต์ ์ผ๋ก ์ฌ์ฉ๋ฉ๋๋ค.
ย
๐ ์ฃผ์ ๊ฐ๋ :
- Observable (๊ด์ฐฐ ๊ฐ๋ฅํ ๊ฐ์ฒด): ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ๋ํ๋ด๋ ๊ธฐ๋ณธ ๋จ์์ ๋๋ค. ์๊ฐ์ ๋ฐ๋ผ ๋ฐ์ํ๋ ์ด๋ฒคํธ๋ ๊ฐ์ ์ํ์ค๋ฅผ ํํํฉ๋๋ค.
- Observer (๊ด์ฐฐ์): Observable์ ๊ตฌ๋ ํ๊ณ ๋ฐํ๋ ํญ๋ชฉ์ ๋ฐ์ํ๋ ๊ฐ์ฒด์ ๋๋ค.
- Operator (์ฐ์ฐ์): Observable์ ๋ณํ, ํํฐ๋ง, ๊ฒฐํฉํ๋ ๋ฑ์ ์์ ์ ์ํํ๋ ํจ์์ ๋๋ค.
- Scheduler (์ค์ผ์ค๋ฌ): ์์ ์ด ์คํ๋ ์ค๋ ๋๋ ์คํ ์ปจํ ์คํธ๋ฅผ ์ง์ ํฉ๋๋ค.
- 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 ํ๊ฒฝ์ ๋ง๊ฒ ๊ตฌํํ ๊ฒ์ ๋๋ค.
ย
๐งฉ ์ฃผ์ ๊ตฌ์ฑ ์์:
- Observable<T>: ์๊ฐ์ ๋ฐ๋ผ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํค๋ ์ํ์ค์ ๋๋ค. T ํ์ ์ ์์๋ฅผ ๋ฐฉ์ถํฉ๋๋ค.
- Observer: Observable์ ๊ตฌ๋ ํ๊ณ ์ด๋ฒคํธ์ ๋ฐ์ํ๋ ๊ฐ์ฒด์ ๋๋ค.
- Disposable: ๊ตฌ๋ ์ ์ทจ์ํ๊ณ ๋ฆฌ์์ค๋ฅผ ํด์ ํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.
- Subjects: Observable์ด์ Observer ์ญํ ์ ๋์์ ์ํํ ์ ์๋ ๋ธ๋ฆฟ์ง ๋๋ ํ๋ก์ ๊ฐ์ฒด์ ๋๋ค.
- Schedulers: ์์ ์ด ์คํ๋ ์ปจํ ์คํธ(์: ๋ฉ์ธ ์ค๋ ๋, ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋)๋ฅผ ๊ฒฐ์ ํฉ๋๋ค.
ย
์ด๋ฌํ ๊ตฌ์ฑ ์์๋ค์ด ์ด๋ป๊ฒ ์ํธ์์ฉํ๋์ง ์ดํดํ๋ ๊ฒ์ด RxSwift ๋ง์คํฐ์ ์ฒซ ๊ฑธ์์ ๋๋ค. ๊ฐ ์์๋ค์ Reactive Programming์ ํต์ฌ ๊ฐ๋ ์ Swift์ ๊ฐ๋ ฅํ ํ์ ์์คํ ๊ณผ ๊ฒฐํฉํ์ฌ ๊ตฌํํฉ๋๋ค.
2.3 Observable ์ฌ์ธต ํ๊ตฌ
Observable์ RxSwift์ ํต์ฌ ๊ตฌ์ฑ ์์์ ๋๋ค. ์ด๋ ์๊ฐ์ ๋ฐ๋ผ ๋ฐ์ํ๋ ์ด๋ฒคํธ์ ์ํ์ค๋ฅผ ๋ํ๋ด๋ฉฐ, ๋น๋๊ธฐ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ๊ธฐ๋ณธ ๋จ์์ ๋๋ค.
ย
๐ฌ Observable์ ์ฃผ์ ํน์ง:
- ํ์ ์์ ์ฑ: Generic์ ์ฌ์ฉํ์ฌ ํน์ ํ์ ์ ์ด๋ฒคํธ๋ง ๋ฐฉ์ถ
- ๋น๋๊ธฐ ์ฒ๋ฆฌ: ์๊ฐ์ ๋ฐ๋ฅธ ์ด๋ฒคํธ ์ฒ๋ฆฌ๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ํํ
- ๋ค์ํ ์์ฑ ๋ฐฉ๋ฒ: just, from, create ๋ฑ ๋ค์ํ ํฉํ ๋ฆฌ ๋ฉ์๋ ์ ๊ณต
- ์ฐ์ฐ์ ์ฒด์ด๋: map, filter ๋ฑ์ ์ฐ์ฐ์๋ฅผ ํตํ ๋ฐ์ดํฐ ๋ณํ ๋ฐ ์ฒ๋ฆฌ
ย
Observable์ ์ธ ๊ฐ์ง ์ ํ์ ์ด๋ฒคํธ๋ฅผ ๋ฐฉ์ถํ ์ ์์ต๋๋ค:
- Next: ์๋ก์ด ์์๋ฅผ ๋ฐฉ์ถ
- Error: ์๋ฌ ๋ฐ์ ์ ๋ฐฉ์ถ๋๋ฉฐ, ์ํ์ค๋ฅผ ์ข ๋ฃ
- 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์ ์ฃผ์ ์ ํ:
- PublishSubject: ๊ตฌ๋ ์ดํ์ ๋ฐ์ํ๋ ์ด๋ฒคํธ๋ง ๋ฐฉ์ถ
- BehaviorSubject: ๊ฐ์ฅ ์ต๊ทผ ์ด๋ฒคํธ(๋๋ ์ด๊ธฐ๊ฐ)์ ์ดํ ๋ฐ์ํ๋ ์ด๋ฒคํธ ๋ฐฉ์ถ
- ReplaySubject: ๋ฒํผ ํฌ๊ธฐ๋งํผ์ ์ด์ ์ด๋ฒคํธ์ ์ดํ ๋ฐ์ํ๋ ์ด๋ฒคํธ ๋ฐฉ์ถ
- 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()
}
}
์ด ์์ ์์๋ ๋ ๊ฐ์ง ๋ฐฉ์์ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ๋ฅผ ๋ณด์ฌ์ค๋๋ค:
- DisposeBag์ ์ฌ์ฉํ ์๋ ํด์ : ViewController๊ฐ ํด์ ๋ ๋ ์๋์ผ๋ก ๋ชจ๋ ๊ตฌ๋ ์ด ํด์ ๋ฉ๋๋ค.
- ์๋ ๊ด๋ฆฌ: ํน์ ์์ (์ฌ๊ธฐ์๋ 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)
์ด ์์ ์์๋ ๋ค์๊ณผ ๊ฐ์ ์๋ฌ ์ฒ๋ฆฌ ์ ๋ต์ ์ฌ์ฉํฉ๋๋ค:
- `retry` ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ์ฌ ์คํจ ์ ์ต๋ 3๋ฒ๊น์ง ์ฌ์๋ํฉ๋๋ค.
- `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)")
}
}
}
์ด ํ ์คํธ ์์ ์์๋ ๋ ๊ฐ์ง ๋ฐฉ์์ ํ ์คํธ๋ฅผ ๋ณด์ฌ์ค๋๋ค:
- TestScheduler๋ฅผ ์ฌ์ฉํ ์๊ฐ ๊ธฐ๋ฐ ํ ์คํธ: ๋น๋๊ธฐ ์์ ์ ํ์ด๋ฐ์ ์ ํํ๊ฒ ์ ์ดํ๊ณ ๊ฒ์ฆํ ์ ์์ต๋๋ค.
- RxBlocking์ ์ฌ์ฉํ ๋๊ธฐ์ ํ ์คํธ: ๋น๋๊ธฐ ์์ ์ ๋๊ธฐ์์ผ๋ก ๋ณํํ์ฌ ๊ฐ๋จํ๊ฒ ํ ์คํธํ ์ ์์ต๋๋ค.
์ด๋ฌํ ํ ์คํธ ๋ฐฉ์์ ํตํด RxSwift๋ฅผ ์ฌ์ฉํ๋ ๋น๋๊ธฐ ์ฝ๋์ ์ ํ์ฑ์ ํจ๊ณผ์ ์ผ๋ก ๊ฒ์ฆํ ์ ์์ต๋๋ค.
4.4 ์ฑ๋ฅ ์ต์ ํ ํ
RxSwift๋ฅผ ์ฌ์ฉํ ๋ ์ฑ๋ฅ์ ์ต์ ํํ๋ ๊ฒ์ ์ค์ํฉ๋๋ค. ์ฌ๊ธฐ ๋ช ๊ฐ์ง ์ฑ๋ฅ ์ต์ ํ ํ์ ์๊ฐํฉ๋๋ค.
ย
๐ ์ฑ๋ฅ ์ต์ ํ ํ:
- share() ์ฐ์ฐ์ ์ฌ์ฉ: ๋์ผํ Observable์ ์ฌ๋ฌ ๋ฒ ๊ตฌ๋ ํ ๋ ์ค๋ณต ์์ ์ ๋ฐฉ์งํฉ๋๋ค.
- ๋ถํ์ํ ๊ตฌ๋ ํผํ๊ธฐ: ํ์ํ์ง ์์ ๊ตฌ๋ ์ ์ฆ์ ํด์ ํ์ฌ ๋ฆฌ์์ค๋ฅผ ์ ์ฝํฉ๋๋ค.
- ์ ์ ํ ์ค์ผ์ค๋ฌ ์ฌ์ฉ: ๋ฌด๊ฑฐ์ด ์์ ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค์ผ์ค๋ฌ์์ ์ฒ๋ฆฌํฉ๋๋ค.
- debounce์ throttle ํ์ฉ: ๋น๋ฒํ ์ด๋ฒคํธ ๋ฐ์ ์ ๋ถํ์ํ ์ฒ๋ฆฌ๋ฅผ ์ค์ ๋๋ค.
- ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง: ์ํ ์ฐธ์กฐ๋ฅผ ํผํ๊ณ 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 ์ฑ์ ๋ง๋ค์ด ๋๊ฐ๋ ์ฌ์ ์ ์ฆ๊ธฐ์๊ธฐ ๋ฐ๋๋๋ค! ๐
- ์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์ฌ๋ฅ๋ท์ ์ต์ AI ๊ธฐ์ ๊ณผ ๋ฒ๋ฅ ์ ๊ธฐ๋ฐํ์ฌ ์์ฌ์ ์ง์ ์ฌ์ฐ๊ถ์ ์ ๊ทน์ ์ผ๋ก ๋ณดํธํ๋ฉฐ,
๋ฌด๋จ ์ฌ์ฉ ๋ฐ ์นจํด ํ์์ ๋ํด ๋ฒ์ ๋์์ ํ ๊ถ๋ฆฌ๋ฅผ ๋ณด์ ํฉ๋๋ค.
ยฉ 2025 ์ฌ๋ฅ๋ท | All rights reserved.
๋๊ธ 0๊ฐ