Conversation
- MVVM 구조 설계 - RxSwift 형식의 input, output 설계 - cardCell, listCell 구현 - 메인화면 설계 및 구현 - API 서비스 함수 생성 - 음악 모델 설계
| * The store could not be migrated to the current model version. | ||
| Check the error message to determine what the actual problem was. | ||
| */ | ||
| fatalError("Unresolved error \(error), \(error.userInfo)") |
There was a problem hiding this comment.
persistentContainer 초기화 실패 시 fatalError를 호출하여 앱을 강제 종료하고 있습니다. 프로덕션 환경에서는 앱이 크래시되는 대신, 오류를 기록하고 fallback UI를 보여주는 등 더 안정적인 방식으로 오류를 처리해야 합니다. 이는 앱 안정성에 치명적인 영향을 줄 수 있습니다.
// fatalError()는 프로덕션 앱에서 사용하면 안됩니다.
// 오류 로깅 및 사용자에게 알림 등 안정적인 오류 처리 방식으로 대체해야 합니다.
print("Unresolved error \(error), \(error.userInfo)")References
- 런타임 크래시를 현실적으로 유발할 수 있는 강제 언래핑 또는 강제 종료는 P0(가장 높은 심각도)로 분류해야 합니다.
fatalError는 의도적인 크래시를 유발하므로 이 규칙에 해당합니다. (link)
| // Replace this implementation with code to handle the error appropriately. | ||
| // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. | ||
| let nserror = error as NSError | ||
| fatalError("Unresolved error \(nserror), \(nserror.userInfo)") |
There was a problem hiding this comment.
saveContext에서 오류 발생 시 fatalError를 호출하고 있습니다. 데이터 저장 실패가 앱 전체를 중단시켜야 할 만큼 심각한 오류는 아닐 수 있습니다. 이 부분도 크래시 대신 오류 로깅 등으로 대체하여 앱의 안정성을 높이는 것이 좋습니다.
let nserror = error as NSError
// fatalError()는 프로덕션 앱에서 사용하면 안됩니다.
// 오류 로깅 등 안정적인 오류 처리 방식으로 대체해야 합니다.
print("Unresolved error \(nserror), \(nserror.userInfo)")References
- 런타임 크래시를 현실적으로 유발할 수 있는 강제 언래핑 또는 강제 종료는 P0(가장 높은 심각도)로 분류해야 합니다.
fatalError는 의도적인 크래시를 유발하므로 이 규칙에 해당합니다. (link)
| } | ||
|
|
||
| class APIService { | ||
| static let share = APIService() |
There was a problem hiding this comment.
APIService를 싱글톤으로 구현하면 사용하기는 편리하지만, 다른 객체들이 이 싱글톤에 직접 의존하게 되어 강한 결합(tight coupling)이 발생합니다. 이는 단위 테스트를 어렵게 만들고 유연성을 떨어뜨립니다. 의존성 주입(Dependency Injection)을 사용하여 APIService 인스턴스를 외부에서 생성하고 주입하는 방식으로 변경하는 것을 고려해 보세요.
References
- 의미 있는 단위 테스트를 어렵게 만드는 강한 결합을 도입하는 구조적 설계는 P1(높은 심각도)로 분류해야 합니다. 싱글톤 패턴은 의존성 주입을 방해하여 이 규칙에 위배될 수 있습니다. (link)
| init() { | ||
| self.vm = MainViewModel() | ||
| super.init(nibName: nil, bundle: nil) | ||
| } |
There was a problem hiding this comment.
MainViewController가 MainViewModel을 직접 생성하고 있습니다. 이는 View와 ViewModel 간의 강한 결합을 만들어 테스트(예: Mock ViewModel 사용)를 어렵게 만듭니다. 의존성 주입 패턴을 적용하여 init을 통해 외부에서 ViewModel을 주입받도록 수정하는 것이 좋습니다. 또한, vm 프로퍼티를 let으로 선언하여 불변성을 보장할 수 있습니다.
| init() { | |
| self.vm = MainViewModel() | |
| super.init(nibName: nil, bundle: nil) | |
| } | |
| init(viewModel: MainViewModel) { | |
| self.vm = viewModel | |
| super.init(nibName: nil, bundle: nil) | |
| } |
References
- 의미 있는 단위 테스트를 어렵게 만드는 강한 결합을 도입하는 구조적 설계는 P1(높은 심각도)로 분류해야 합니다. 뷰 컨트롤러가 뷰 모델을 직접 생성하는 것은 강한 결합의 예입니다. (link)
| guard let self else { return nil } | ||
|
|
||
| let section = self.dataSource.snapshot().sectionIdentifiers | ||
| let sectionType = section[sectionIndex] | ||
|
|
||
| switch sectionType { | ||
| case .spring, .autumn: | ||
| return self.cardSection() | ||
| case .summer, .winter: | ||
| return self.listSection() | ||
| } |
There was a problem hiding this comment.
createLayout 클로저 내부에서 dataSource의 스냅샷에 직접 접근하여 섹션 타입을 결정하고 있습니다. 이는 레이아웃 로직이 데이터 소스의 현재 상태(특히 섹션 순서)에 의존하게 만들어 코드를 취약하게 만듭니다. 예를 들어, 나중에 스냅샷에서 섹션 순서를 변경하면 의도치 않게 레이아웃이 변경될 수 있습니다. 레이아웃은 sectionIndex를 기반으로 SeasonKeyword.allCases와 같은 정적인 소스에서 섹션 타입을 결정하도록 하여 데이터 소스와의 결합도를 낮추는 것이 좋습니다.
guard let self else { return nil }
let sectionType = SeasonKeyword.allCases[sectionIndex]
switch sectionType {
case .spring, .autumn:
return self.cardSection()
case .summer, .winter:
return self.listSection()
}References
- 의미 있는 단위 테스트를 어렵게 만드는 강한 결합을 도입하는 구조적 설계는 P1(높은 심각도)로 분류해야 합니다. 레이아웃 로직이 데이터 소스의 동적 상태에 의존하는 것은 강한 결합의 한 형태입니다. (link)
| } | ||
|
|
||
| // API 호출 | ||
| return APIService.share.fetch(url: url) |
There was a problem hiding this comment.
MainViewModel이 APIService.share 싱글톤을 직접 참조하고 있습니다. 이는 ViewModel과 Service 간의 강한 결합을 유발하여, APIService의 구현을 교체하거나 MainViewModel을 독립적으로 테스트하기 어렵게 만듭니다. APIService를 프로토콜로 추상화하고, init을 통해 의존성을 주입받는 구조로 변경하는 것을 권장합니다.
References
- 의미 있는 단위 테스트를 어렵게 만드는 강한 결합을 도입하는 구조적 설계는 P1(높은 심각도)로 분류해야 합니다. 뷰 모델이 서비스 싱글톤을 직접 사용하는 것은 이 규칙에 위배됩니다. (link)
Revert "✨ Feat: 검색 기능 구현 (UI 제외)"
| return Observable.merge(results) | ||
| //scan 을 이용하여 스트림의 결과값들을 딕셔너리 형태로 누적 | ||
| .scan(into: [SeasonKeyword : [Music]]()) { partial, element in | ||
| let (keyword, musics) = element | ||
| partial[keyword] = musics | ||
| } |
There was a problem hiding this comment.
딕셔너리 형태로 결과값을 가져오는 이유가 궁금합니다! 스냅샷 설정을 위해서인가요??
| enum SeasonKeyword: String, CaseIterable, Hashable { | ||
| case spring = "봄" | ||
| case summer = "여름" | ||
| case autumn = "가을" | ||
| case winter = "겨울" | ||
|
|
||
| var title: String { | ||
| rawValue | ||
| } | ||
| } |
There was a problem hiding this comment.
rawValue를 String으로 미리 선언해서 사용하는 방법이 가독성이 좋네요
| let update: Observable<[SeasonKeyword : [Music]]> | ||
| } | ||
|
|
||
| func transform(input: Input) -> Output { |
There was a problem hiding this comment.
fetch 메서드에서는 오류 발생시 observer(.failure(error))로 전달하지만, 호출 받아서 처리하는 transfrom 메서드에서는 처리하는 로직이 존재하지 않습니다.
There was a problem hiding this comment.
에러처리 관련해서 어떻게 처리해야하는지 생각을 못했네요.
감사합니다!
- 상단 검색 바 기능 구현 - 검색 결과 화면 설계 및 구현
Added detailed project description, goals, and features for the iOS app inspired by Apple Music.
🛠 작업 내용 (What I did)
💻 구현 상세 (Implementation Details)
📸 스크린샷 (Screenshots)