-
๋ชฉ์ฐจ
- ์ฑ ์คํ ํ๋ฉด
- ์ํคํ ์ณ ์ค๊ณ ๋ฐ ๊ณ ๋ คํ ์ ์ด๋
- ์ฑ๋ฅ
- ๋ฉ๋ชจ๋ฆฌ ๋์
- ๋ณต์กํ ์์กด์ฑ
- Test
- ์ฑ๊ธํด ๋์์ธ ํจํด๊ณผ NSCache๋ฅผ ์ด์ฉํ Image ์บ์ฑ
- RxSwift ๋ฐ์ํ ๊ฒ์ํ๋ฉด
๋ก๊ทธ์ธ ํ๋ฉด
ํํ๋ฉด + ๋ ๋ฒจ ๋์์ธ ํ๋ฉด
๋ฐ์ด๋ ๋ณด๊ดํจ ํ๋ฉด
๋ฐ์ด๋ ๊ฒ์ ํ๋ฉด
๋ฐ์ด๋ ์์ธ์ ๋ณด ํ์ธ + ์ ์ฅ ํ๋ฉด
์ด์ ํ๋ก์ ํธ๋ค์ ๋ฌธ์ ์ (๊ฑฐ๋ํ ํด๋์ค, ๋ณต์กํ ์์กด์ฑ, ๋ฉ๋ชจ๋ฆฌ ๋์, ์ ์ฐํ์ง ๋ชปํ View) ํด๊ฒฐํ๊ธฐ ์ํด ๊น์ด ๊ณ ๋ฏผ
๐ง ์ค๊ณ์ ์์ ๊ณ ๋ฏผํ ์
๋ฐฐ๋ณด๋ค ๋ฐฐ๊ผฝ์ด ๋ ์ปค์ง๋ ์ํฉ์ ๋ฐฉ์งํ๊ณ ์ ํ์ต๋๋ค.
์ํคํ ์ฒ ๋ฐ ๋์์ธ ํจํด ๋ฑ์ ์ถ๊ฐ์ ์ผ๋ก ์์๋ณด๋ฉฐ, ๋ชจ๋ ํจ๊ณผ๊ฐ ์ข์ ๋ณด์ฌ์ ๋ง์น ์ผํ์ ํ๋ฏ ๋ง์ด ๋ด๊ฒ ๋์ง ์๋๋ก ๊ผญ ํ์ํ๋ฉฐ ๋ค์๊ณผ ๊ฐ์ ์ง๋ฌธ์ ์ ํฉํ์ง ํ๋จํ์ต๋๋ค.
- ์์ฐ์ฑ์ ์ฌ๋ ค์ฃผ๋๊ฐ?
- (์ํคํ ์ณ ๋ฐ ๋์์ธ ํจํด์ ๋์ ์ด, ์คํ๋ ค ์ธ๋ถํ๋ ์ถ์ํ๋ก ์ธํด ์์ฐ์ฑ์ด ๋จ์ด์ง๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ์ํด)
- ๊ณผ๊ฑฐ์ ๋ฌธ์ ์ ์ ๊ฐ์ ํ๊ฑฐ๋ ํด๊ฒฐํด์ฃผ๋๊ฐ?
- ๊ฐ๋ ์ฑ ์ข์์ง๋๊ฐ?
- ์ฝ๋๊ฐ ์ฌ๋ฌ์ํฉ์ ์ ์ฐํด์ง๋๊ฐ?
- ์ฝ๋ฉํ ๋ ๋ฌด์์์ ์ธ ์ค์๋ฅผ ๋ฐฉ์งํด์ค ์ ์๋๊ฐ?
๐งโ๐ป ๊ณ ๋ฏผ์ดํ์ ๊ฒฐ๊ณผ
-
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์ด ์๊ธฐ์ง ์๋๋ก, ์ ์ ์ ์ธ ๋ ํผ๋ฐ์ค ์นด์ดํธ ๊ด๋ฆฌ
-
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 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)
})
}
- ์ต๊ณ ๋ ๋ฒจ์, ๋ณด๊ดํจ ๊ฐฏ์๊ฐ 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๊ฐ ์คํจํ ๋ถ๋ถ์ ์ค์ ์ ์ผ๋ก ๋ค์ ํ์ธํ๋ฉด ๋จ
๐ง๊ณ ๋ฏผํ๋ฉฐ ๊นจ๋ฌ์ ์
=> ํ๋ฒ ๊ฒ์ํ์ฌ ๋ณด๊ดํ ๊ฐ์ ๋ฐ ์จ๋ฒ์ ์ ๋ชฉ์, ๋ค์์ ์ฑ์ ์คํํ์ฌ ๋ ๋ค์ ๊ฒ์ํ๋ ๊ฒฝ์ฐ๊ฐ ์ ์ ๊ฒ์ผ๋ก ์๊ฐํ์ฌ ๋๋ฐ์ด์ค ๋ด๋ถ ์ ์ฅ ๊ณต๊ฐ์ ์บ์ฑ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ฐฉ๋ฒ์ ์ ํํ์ง ์์
=> ์ฑ๊ธํด ๋์์ธ ํจํด์ ์ฌ์ฉ์ผ๋ก ์ฑ์ด ์ข ๋ฃ๋๋ฉด ์บ์ฑ ๋ฐ์ดํฐ๋ ์๋ฉธํ๋๋ก ์ค๊ณ, ๋ํ ์ด๋ฏธ์ง ์บ์ฑ์ 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()
}
}
- ์ผ๋ฐ์ ์ธ ์ด์ค์ผ์ดํ ํด๋ก์ ๋ฅผ ํตํ ํต์ ํจ์ ์ฌ์ฉ ๋์
- 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)