- ํ๋ก์ ํธ ๊ธฐ๊ฐ: 2024. 03. 08. ~ 2024. 03. 24.
- App Store ์ถ์์ (๋งํฌ)
- ํ๊ตญ์ ์ฃผ์ ๋์๋ค์ ๊ด๊ด์ง, ๋ง์ง ๋ฑ์ ์ฝ๊ฒ ๊ฒ์ํ๊ณ ๋ถ๋งํฌ์ ์ ์ฅํ์ฌ ์ฌํ์ ๋์์ ์ค ์ ์๋ ์ดํ
- Configuration: iOS 16.0+
- ๋ค๊ตญ์ด(ํ๊ตญ์ด, ์์ด) ์ ์ฉ
- ๋คํฌ๋ชจ๋ ์ง์
- ํ๊ตญ ์ฃผ์ ๋์๋ณ, ๊ด๊ด ์นดํ ๊ณ ๋ฆฌ๋ณ ๊ฒ์ ๊ธฐ๋ฅ
- ์ํ๋ ๋ ์ง์ ์งํํ๋ ์ถ์ , ๊ณต์ฐ, ํ์ฌ ๊ฒ์ ๊ธฐ๋ฅ
- ํค์๋๋ก ๊ด๊ด์ง ๊ฒ์ ๋ฐ ๊ธฐ๋ก ์ ์ฅ ๊ธฐ๋ฅ
- ๊ด๊ด์ง ๋ถ๋งํฌ ์ ์ฅ ๊ธฐ๋ฅ
- ๊ด๊ด์ง ๋ํ ์ผ ์ ๋ณด ํ์ธ ๊ธฐ๋ฅ
- ์์ ์ ์์น์ ์ ํํ ๊ด๊ด์ง์ ๊ฑฐ๋ฆฌ ํ์ธ ๊ธฐ๋ฅ
-
Framework
- Code Base UIKit
-
Pattern
- MVVM
- UI์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ถ๋ฆฌํ์ฌ ์ ์ง๋ณด์์ ํ ์คํธ๊ฐ ์ฉ์ดํ๋๋ก ํ๊ธฐ ์ํด MVVM ํจํด์ ์ฌ์ฉ
- MVVM
-
Library
-
๋ฐ์ดํฐ๋ฒ ์ด์ค
- Realm
- ๋ฐ์ดํฐ์ UI๊ฐ์ ๋๊ธฐํ ๋ฐ ์ฑ์ ์๋ต ์๋๋ฅผ ํฅ์์ํค๋ฅผ ์ํด ์ฌ์ฉ
- Realm
-
๋ฐฑ์๋ ์๋น์ค
- Firebase
- ๋ฉ์์ง ๋ฐ ์ค๋ฅ ๋ณด๊ณ ๋ฅผ ์์งํ๊ธฐ ์ํด ์ฌ์ฉ
- Firebase
-
๋คํธ์ํฌ
- Alamofire
- ๋คํธ์ํฌ ์์ฒญ์ ๊ฐํธํ๊ณ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ฌ์ฉ
- Alamofire
-
์ด๋ฏธ์ง ์ฒ๋ฆฌ
- Kingfisher
- ์ด๋ฏธ์ง๋ฅผ ๋ค์ด๋ก๋ํ๊ณ ์บ์ฑํ๋ ์์ ์ ๊ฐํธํ๊ฒ ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ฌ์ฉ
- Kingfisher
-
UI ๊ตฌ์ฑ
- Tabman
- ์ง๊ด์ ์ด๊ณ ์ฌ์ฉํ๊ธฐ ์ฌ์ด ํญ ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํ๊ธฐ ์ํด ์ฌ์ฉ
- Snapkit
- ์คํ ๋ ์ด์์์ ์ฝ๋๋ก ์ฝ๊ฒ ์์ฑํ๊ณ ๊ด๋ฆฌํ๊ธฐ ์ํด ์ฌ์ฉ
- Toast
- ์ฌ์ฉ์์๊ฒ ๊ฐ๋จํ ํ์ ๋ฉ์์ง๋ฅผ ํ์ํ์ฌ ์ธํฐํ์ด์ค๋ฅผ ์ง๊ด์ ์ผ๋ก ๋ง๋ค๊ธฐ ์ํด ์ฌ์ฉ
- SVProgressHUD
- ๋ ์ง๊ด์ ์ผ๋ก ๋ก๋ฉ์ ๋ํ HUD ํ์๋ฅผ ์ํด ์ฌ์ฉ
- FSCalendar
- Customize๊ฐ ์ฉ์ดํ ์บ๋ฆฐ๋๋ฅผ ์ํด ์ฌ์ฉ
- Tabman
-
![]() |
!![]() |
|---|---|
| ์ ์ฉ ์ | ์ ์ฉ ํ |
-
์ฌ์ฉ์ ์ ์ฅ์ ๊ณ ๋ คํ์ฌ ๋ถ๋งํฌ์ Incidator๋ฅผ ๊ตฌํํ์์ง๋ง, ์คํ๋ ค ๋๋ฌด ์ฆ์ Indicator ๋จ์ฉ์ผ๋ก ์ธํด ์ฌ์ฉ์์๊ฒ ์คํธ๋ ์ค๋ฅผ ์ค ๊ฒ
-
๊ฒ๋ค๊ฐ ์ด๋ฒ ํ๋ก์ ํธ์ ์ฌ์ฉํ ํ๊ตญ๊ด๊ด๊ณต์ฌ API ๋ ๊ฐ๋ ์๋ฒํต์ ์ด ์ ์๋ ๋๊ฐ ์์ด์ ์ฆ์ Indicator ๋ ์ฌ์ฉ์ ์ ์ฅ์์ ๋ ๋ต๋ตํจ
-
์ฃผ์์ฝ๋
@objc private func bookmarkButtonClickedInBottomCV(\_ sender: UIButton) { guard let data = viewModel.outputFestivalData.value?.response.body.items?.item?[sender.tag] else { return } // ๋ถ๋งํฌ ์ํ ํ์ธ let isBookmarked = viewModel.repository.isBookmarked(contentId: data.contentid) // Optimistic UI ์ ๋ฐ์ดํธ sender.setImage(UIImage(systemName: isBookmarked ? "bookmark" : "bookmark.fill"), for: .normal) // ๋ถ๋งํฌ ์ํ์ ๋ฐ๋ผ ๋ถ๋งํฌ ์ถ๊ฐ ๋๋ ์ญ์ if isBookmarked { viewModel.repository.deleteBookmark(data: data) // ์ญ์ ํ UI ์ ๋ฐ์ดํธ ํ์ ์์ } else { viewModel.repository.addBookmark(id: data.contentid) { success in DispatchQueue.main.async { if !success { // ์์ฒญ ์คํจ ์ UI ๋๋๋ฆฌ๊ธฐ sender.setImage(UIImage(systemName: "bookmark"), for: .normal) // ์คํจ ํผ๋๋ฐฑ ์ ๊ณต } } } }
-
๋ถ๋งํฌ ์ญ์ ์ Button.tag๋ค์ ๊ธฐ์กด tag ๊ฐ๋ค์ ๊ทธ๋๋ก ๊ฐ์ง๊ณ ์์ด์ Item๋ค์ row ๊ฐ๊ณผ tag ๊ฐ์ ๋ถ์ผ์น ๋ฐ์
-
indexPath ๊ธฐ๋ฐ์ด ์๋, identifier์ ์ด์ฉํ์ฌ ์ ์ญ์ ๊ธฐ๋ฅ์ ๊ตฌํํ๋, tag ๋ถ์ผ์น, ์ ๋๋ฉ์ด์ ํจ๊ณผ ๋ฑ ๋ชจ๋ ๋ฌธ์ ์ ์ด ํด๊ฒฐ ๋์์ผ๋ฉฐ DiffableDatasource์ ๋ ํจ์จ์
์ฃผ์์ฝ๋
//๊ธฐ์กด ์ฝ๋ private func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration<ResultCollectionViewCell, Bookmark> { (cell, indexPath, identifier) in cell.updateUIInBookmarkVC(identifier) cell.bookmarkButton.tag = indexPath.item cell.bookmarkButton.addTarget(self, action: #selector(self.bookmarkButtonClicked), for: .touchUpInside) cell.bookmarkButton.setImage(UIImage(systemName: "bookmark.fill"), for: .normal) ... } @objc private func bookmarkButtonClicked(_ sender: UIButton) { viewModel.repository.deleteBookmarkInBookmarkView(data: Array(viewModel.outputBookmarks.value ?? [])[sender.tag]) } private func updateSnapshot() { var snapshot = NSDiffableDataSourceSnapshot<Section, Bookmark>() snapshot.appendSections([.main]) let bookmarks = viewModel.outputBookmarks.value ?? [] snapshot.appendItems(bookmarks, toSection: .main) dataSource.apply(snapshot, animatingDifferences: true) //reloadData self.updateMapView(with: bookmarks) DispatchQueue.main.asyncAfter(deadline: .now() + 0.3){ self.bookmarkCollectionView.reloadData() } } //๋ณ๊ฒฝ ์ฝ๋ cell.bookmarkButton.accessibilityIdentifier = identifier.contentid
3. ํ๊ตญ๊ด๊ด๊ณต์ฌ API ์๋ฒ ์ํ ๋ถ์์ ์ผ๋ก ์ธํ ๋คํธ์ํฌ ํต์ ์คํจ์, ์ฌํธ์ถ ๊ตฌํ
-
์๋ฒ๊ฐ ๋ถ์์ ํ์ฌ ์๊ฐ๋์ ๋ฐ๋ผ, ๊ฐ์ Parameter๋ก ํธ์ถ์ ํ์ฌ๋ ๋คํธ์ํฌ ํต์ ์ด ์คํจํ๋ ๊ฒฝ์ฐ๊ฐ ์ฆ์ (ํ๊ท ์๋ฒฝ์๋ 3๋ฒ ํธ์ถ์ 1๋ฒ ์คํจ, ์คํ์๋ 100๋ฒ ํธ์ถ์ 1๋ฒ ์คํจ)
-
Errorํ์ธ ๊ฒฐ๊ณผ, Routing Error ๋ก ํ์ธ๋์์ผ๋ฉฐ ์ ๊ฐ ์๋ Server์์ ํด๊ฒฐํด์ค์ผ ํ๋ ๋ฌธ์ (ํ๊ตญ๊ด๊ด๊ณต์ฌ API๋ฅผ ์ ๊ณตํด์ฃผ๋ ๊ณต๊ณต๋ฐ์ดํฐํฌํธ์ ๋ฌธ์ํด๋ณด์์ง๋ง, ์ด ๋ฌธ์ ๋ฅผ ์ธ์งํ๊ณ ์์ผ๋ ๋น์ฅ์ ํด๊ฒฐํ์ง ๋ชป ํ๋ค๋ ๋ต๋ณ)
-
ํธ์ถ์, ์ฌ์๋ ํ์๋ฅผ ์ค์ (retryCount)
-
์คํจํ ๊ฒฝ์ฐ, 3์ด์ ๊ฐ๊ฒฉ์ ๋๊ณ retryCount๋ฅผ 1์ฉ ์ฐจ๊ฐํ๋ฉฐ ์ฌํธ์ถํ๊ฒ ๊ตฌํ
-
retryCount๋ฅผ ๋ชจ๋ ์ฐจ๊ฐํ๊ณ ๋ ์คํจํ ๊ฒฝ์ฐ๋ ์๋ฒ๊ฐ ์๋์ด ์๋๋ ์์ ์ผ๋ก ๊ฐ์ฃผํ์ฌ, completionHandler๋ฅผ ํ์ฉํ์ฌ View์์ ๋์ค์ ์ฌ์๋ ํ๋ผ๋ Alert์ ๋์ฐ๋๋ก ๊ตฌํ
์ฃผ์์ฝ๋
func request<T: Decodable>(type: T.Type, api: API, retryCount: Int = 2, completionHandler: @escaping (T?, AFError?) -> Void) { session.request(api.endPoint, method: api.method, parameters: api.parameter, encoding: api.encoding).responseDecodable(of: T.self) { response in switch response.result { case .success(let success): print("๋คํธ์ํฌ ํต์ ์ฑ๊ณต!") completionHandler(success, nil) case .failure(let failure): print("์๋ฌ") if retryCount > 0 { DispatchQueue.main.asyncAfter(deadline: .now()) { self.request(type: type, api: api, retryCount: retryCount - 1, completionHandler: completionHandler) print(retryCount) } } else { print("์ฌ๊ธฐ๋ก์ค๋") completionHandler(nil, failure) } } } }
![]() |
![]() |
![]() |
![]() |
|---|---|---|---|
| animate ํ์ฉํ ๋ฐ์น์คํฌ๋ฆฐ | ๋์๋ณ ๊ฒ์ | ์ปจํ ์ธ ๋ณ ๊ฒ์ | ๋ ์ง๋ณ ํ์ฌ ๊ฒ์ |
![]() |
![]() |
![]() |
|---|---|---|
| ํค์๋ ๊ฒ์ | ๋ถ๋งํฌ ์ ์ฅ | ์ธ๋ถ์ฌํญ ์กฐํ |










