일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- native
- 리액트 네이티브
- React Native
- requirenativecomponent
- panorama view
- 앱 성능 개선
- 뷰 정체성
- data driven construct
- 360도 이미지
- 스켈레톤 통합
- ssot
- 구조적 정체성
- 리액트
- react
- 3b52.1
- launchscreen
- 360도 뷰어
- ios
- 뷰 생명주기
- 360도 이미지 뷰어
- launch screen
- SwiftUI
- react-native-fast-image
- 네이티브
- privacyinfo.plist
- Android
- 명시적 정체성
- 파노라마 뷰
- 라이브러리 없이
- React-Native
- Today
- Total
Neoself의 기술 블로그
Combine에서 Swift Concurrency로의 전환기 본문
최근 iOS 앱의 소셜 기능 개발 과정에서 복잡한 API 호출 체인을 다루게 되었습니다. 사용자의 피드를 구성하기 위해 여러 API를 순차적으로 호출하고 데이터를 조합해야 했는데, Combine을 사용한 기존 접근 방식에서 여러 한계점을 경험했습니다. 이 글에서는 우리가 겪은 문제점들과 Swift Concurrency로의 전환을 통해 이를 해결한 과정을 공유하고자 합니다.
1. 기존 코드의 구조와 한계점
소셜 피드를 구현하기 위해 다음과 같은 연쇄적인 API 호출이 필요했습니다:
func fetchFriendsWithCalendarData(yearMonth: String) {
isLoading = true
// 1. 친구 목록 가져오기
socialService.getFriends()
.flatMap { [weak self] friends -> AnyPublisher<[Friend], APIError> in
self.friends = friends
...
// 값을 다음 단계로 전달하기 위해 단일 값을 방출하는 Just Publisher 생성
return Just(friends)
.setFailureType(to: APIError.self)
.eraseToAnyPublisher()
}
.flatMap { [weak self] friends -> AnyPublisher<[DailyStat], APIError> in
guard let self = self else {
return Fail(error: APIError.unknown).eraseToAnyPublisher()
}
// 2. 선택된 친구의 캘린더 데이터 가져오기
if let selectedFriend = self.selectedFriend {
return self.dailyStatService
.fetchMonthlyStats(for: selectedFriend.id, in: yearMonth)
.eraseToAnyPublisher()
} else {
return Just([])
.setFailureType(to: APIError.self)
.eraseToAnyPublisher()
}
}
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
if case .failure(let error) = completion {
// 에러 처리
}
} receiveValue: { [weak self] stats in
// 응답 데이터 처리
}
.store(in: &cancellables)
}
2. 발생한 문제점들
2.1 복잡한 에러 추적
여러 단계의 flatMap 체인 내부에서 에러가 발생할 경우, 어느 단계에서 문제가 발생했는지 파악하기 어려웠습니다.
firstApiCall
.flatMap {
...
return Just(firstApiResult)
.setFailureType(to: APIError.self)
.eraseToAnyPublisher()
}
.flatMap { [weak self] firstApiResult -> ...
...
return secondApiCall
}
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
if case .failure(let error) = completion {
// 첫번째 api와 두번째 api 중 어느 api에서 에러 발생했는지 파악 불가.
}
} receiveValue: { [weak self] stats in
...
}
.store(in: &cancellables)
}
하지만 이를 위해 각 체인에서 또 에러 처리 로직을 추가하자니, 안그래도 길었던 전체 코드가 더 길어지는 문제를 마주하게 되었습니다.
2.2 순환참조 방지를 위한 Boilerplate 코드
각 비동기작업마다 순환참조 방지하기 위해 아래와 같은 구문을 반복적으로 사용해야했습니다.
- [weak self](약한 참조)
- 비동기 작업이 언제든지 메모리에서 할당해제될 수 있도록 하는 guard let self 구문
- cancellable 관리 구문
2.3 코드 가독성 저하
위 문제들은 자연스레 코드 가독성의 저하로도 이어져, 유지보수 간에 발생하는 리소스의 낭비로도 이어질 수 있는 문제였습니다.
3. Swift Concurrency로의 전환
이러한 문제들을 해결하기 위해 Swift Concurrency로 코드를 전환하게 되었습니다.
@MainActor
func fetchFriendsWithCalendarData(yearMonth: String) async {
isLoading = true
// 1. 친구 목록 가져오기
do {
let fetchedFriends = try await socialService.getFriends()
friends = fetchedFriends
...
} catch {
// 첫번째 api에 대한 에러 처리
isLoading = false
return
}
// 2. 선택된 친구의 캘린더 데이터 가져오기
if let selectedFriend = selectedFriend {
do {
let stats = try await dailyStatService.fetchMonthlyStats(
for: selectedFriend.id,
in: yearMonth
)
friendDailyStats = stats
} catch {
// 두번째 api에 대한 에러 처리
friendDailyStats = []
}
} else {
friendDailyStats = []
}
isLoading = false
}
4. 결론
연속적으로 체이닝을 추가하였던 Combine 프레임워크와 달리 try-catch 패턴을 통해 에러처리를 하다보니, 코드양이 줄어드는 것이 가장 먼저 와닿았습니다. 개인적으로 저의 경우 React Native 프레임워크를 다루면서 Typescript를 통해 이미 많이 접해왔던 패턴이여서 그런지, 더욱 친숙하게 다가왔습니다.
때문에 위와 같이 연쇄적인 api 호출 구조에서 적은 코드양으로도 에러 발생 위치를 정확히 파악할 수 있게 되었으며, await 패턴을 통해 마치 동기코드를 확인하는 것처럼 직관적으로 코드 흐름을 파악할 수 있게 되어, 실행 로직 추적이 더 용이해졌음을 체감할 수 있었습니다.
감사합니다.
'개발지식 정리 > Swift' 카테고리의 다른 글
WidgetKit 활용해 캘린더 위젯 구현하기 (0) | 2024.12.24 |
---|---|
SwiftUI 심층정리 (0) | 2024.12.16 |
EnvironmentObject로 인한 SwiftUI 뷰 재구성 이슈와 해결 과정 (1) | 2024.12.02 |
Unit Test로 찾아낸 Form Validation 오류와 해결과정 (1) | 2024.11.28 |
Keychain+UserDefaults+Combine을 활용해 iOS 로그인 플로우 고도화하기 (1) | 2024.11.07 |