Completion(escaping) 와 Combine

2025. 1. 26. 15:52iOS

현재 진행 중인 프로젝트는 SwiftUI를 사용하여 전체 앱을 리뉴얼하는 상황입니다.
서버 통신 역시 기존 Completion Handler(escaping closure)가 아닌 Combine을 통해 구현하고 있습니다. 여기서 의문점을 가지게 되었습니다. 단순히 “SwiftUI니까 Combine을 써야지!”라는 생각이 아니라, 두 방식을 비교해보고 Combine을 사용하면 어떤 점에서 이점이 있는지 이해하고자 합니다.

왜? 그냥 SwiftUI를 쓰느니깐 거기에 맞는 Combine을 쓰는거 아닐까? 라는 생각을할 수 도있지만 그래도 이유를 알고 쓰는점 또한 어떤한 문제점에서는 Combine이 아닌 CompletionHandler로 통신을할 수 도있기에 차이점에 대해서 알아보고자 합니다.

이스케이핑 클로저(Completion Handler)방식

  • 순수그자체?인점, Swift 초창기부터 사용된 전통적인 방법이므로, 대부분의 라이브러리나 예제 코드가 Completion Handler 기반입니다.
  • 네트워크 요청을 하고, 완료되는 시점에 completion(결과)을 호출합니다.
  • 단일 요청을 처리할 경우, 구현이 간단하고 직관적입니다.
  • 단일 호출만 할 때는 간단해 보이지만, 연속적 또는 병렬적인 여러 네트워크 요청이 필요할 때마다 콜백(Completion Handler)이 중첩되고, 로직이 복잡해질 수 있습니다.
  • 로직이 조금만 복잡해져도 “콜백 지옥(Callback Hell)”처럼 가독성이 떨어지는 문제를 일으킬 수 있습니다.
  • 복잡한 시나리오(연속적인 여러 API 호출, 분기 처리, 예외 처리)가 필요한 경우, 콜백이 중첩되어 코드의 구조가 깊어집니다.
  • 중첩된 Switch나 If 구문이 점점 늘어나 가독성이 떨어지고, 에러 처리가 어려워질 수 있습니다.

그렇다면 여기서 escaping에서 콜백 지옥? 음 아직 나는 겪어본적이 없는거같은데?(그렇다 한번에 많은 API를...) 라는 의문을 가지게 되었습니다.

콜백 지옥(Callback Hell)이란?
콜백 지옥이란, 여러 비동기 작업을 순차적으로 처리할 때 각 단계마다 콜백(Completion Handler)을 중첩해 호출해야 하면서, 코드가 지나치게 복잡해지고 가독성이 떨어지는 상황을 말합니다.
예를 들어, API1API2API3처럼 연속적으로 네트워크 요청을 보내야 할 때, 이전 요청이 성공해야 다음 요청을 보낼 수 있는 구조를 계속 콜백을 써서 이어가게 되면, 코드가 점점 깊어져서 “피라미드” 구조를 띠게 됩니다.

 fetchUserProfile(userId: userId) { result1 in 가장먼저 첫 API를 통해서 통신을 진행
        switch result1 {
        case .success(let user):
            // 첫 번째 콜백 진입
            self.fetchUserPosts(userId: user.id) { result2 in 첫 API가 통신에 성공했을경우 빠져나가는것이 아니라 다음 API 통신진행의 상태를 초례할 수 있다는점입니다.
                switch result2 {
                case .success(let posts):
                    // 두 번째 콜백 진입
                    guard let firstPost = posts.first else {
  • 콜백 지옥의 문제점
    가독성 저하: 한눈에 로직을 파악하기 힘들고, 코드가 수직으로 계속 늘어집니다. 아도겐코드??
    확장성 문제: 추가 기능(예: 네 번째, 다섯 번째 API 호출)을 붙일 때마다 더 깊은 중첩 구조가 생깁니다.
    에러 처리 어려움: 콜백 각각에서 발생하는 오류를 처리하고 그 흐름을 전체적으로 관리하기가 복잡해집니다.

이런 문제점들 너무 명확하게 나타나고 있습니다.
그래서 이러한 부분들을 해결하기 위해 여러가지 방법들을 생각하게 되는거같습니다..

콜백 지옥을 해결하는 방법
먼저, 구조적으로 변경하는방법?이 있지않을까 생각됩니다.

  • 단계별로 메서드 분리: 각 콜백 함수를 작게 나누고, 결과 처리 로직을 공통화하는 식으로 가독성을 높일 수 있습니다.
  • 프로젝트가 커지고 API 체인이 많아질수록 근본적으로는 콜백 자체가 제한점이 되기 때문에, 더 나은 비동기 프로그래밍 기법을 고려하게 됩니다.
  • Combine(또는 RxSwift)처럼 반응형을 도입하거나 iOS15부터 나타난 async/await(Swift Concurrency)를 사용하는 방법이존재할꺼같습니다.

여러가지의 API호출이 존재할 수 있기에.. 이스케이핑이 아닌 Combine으로 구현을 진행했다고 볼수 있을꺼 같습니다.
적다보니 왜? 부정적인 글이 너무 길게 되어서 당황스럽긴하네요... 하지만 이부분에 대해서 알면 알수록 서버통신에 있어서 잘할 수 있지않을까 생각됩니다.


Combine 방식

  • Publisher와 Subscriber의 개념을 통해 비동기 흐름을 “연쇄적으로” 또는 “반복적으로” 구성할 수 있습니다.
  • map, flatMap, filter 등 다양한 오퍼레이터로 데이터 변환이나 에러 처리를 일관성 있게 적용할 수 있어 로직이 한눈에 보이는 구조를 만들기 쉽습니다.
  • 여러 비동기 작업을 체이닝(chaining)하거나, 병렬적으로 수행하고 결과를 합치거나 하는 등의 작업을 선언적으로 표현할 수 있습니다.

체이닝(Chaining): 연속적인 네트워크 요청을 연쇄적으로 연결하기 쉬워집니다.
가독성 향상: 여러 API가 연결되어 있어도 중첩 클로저 대신 오퍼레이터 체인을 사용해 한눈에 로직이 드러납니다.
에러 처리 일관성: 스트림 도중 어느 단계에서 에러가 발생해도, 체인의 마지막에서 통합적으로 처리할 수 있습니다.
SwiftUI 친화적: SwiftUI 역시 Combine과 밀접하게 연동되도록 설계되어 있어, @Published나 ObservableObject 등을 활용하면 UI 업데이트와 비동기 작업을 일관성 있게 처리할 수 있습니다.

func fetchUserProfile(userId: String) -> AnyPublisher<User, Error>
func fetchUserPosts(userId: String) -> AnyPublisher<[Post], Error>
func fetchComments(postId: String) -> AnyPublisher<[Comment], Error>

func loadUserData(userId: String) -> AnyPublisher<[Comment], Error> {
    return fetchUserProfile(userId: userId)
        .flatMap { user in
            self.fetchUserPosts(userId: user.id)
        }
        .tryMap { posts in
            guard let firstPost = posts.first else {
                throw CustomError.noPosts
            }
            return firstPost
        }
        .flatMap { firstPost in
            self.fetchComments(postId: firstPost.id)
        }
        .eraseToAnyPublisher()
}

 


콜백 지옥(Callback Hell)

  • 여러 비동기 작업을 이스케이핑 클로저(Completion Handler)로 순차 연결할 때, 콜백이 깊게 중첩되어 가독성과 유지보수성이 떨어지는 문제.

 

Combine 사용 이유

  • SwiftUI와의 시너지: SwiftUI의 데이터 흐름과 Combine의 퍼블리셔-구독자 구조가 잘 어울립니다.
  • 체이닝이 쉽다: 여러 API를 연쇄적으로 호출할 때, 중첩 클로저 없이 연산자 체인으로 명확하게 표현할 수 있습니다.
  • 에러 처리 간편: .catch나 .tryMap 등 오퍼레이터를 통해 중간 단계 에러를 쉽게 처리 가능합니다.
    정도로 생각을 할 수 있을꺼 같습니다.

결론적으로, “왜 Combine을 선택했나?" 라면....
SwiftUI와 호환성, 비동기 처리 체이닝과 에러 처리의 가독성
콜백 중첩으로 인한 유지보수라는 점을 말할 수 있을꺼 같습니다.

 

'iOS' 카테고리의 다른 글

Equatable와 Hashable 프로토콜  (0) 2025.02.01
비동기 테스트  (1) 2024.07.08
컴파일 최적화 방법  (0) 2024.05.05
Concurrency (await)  (0) 2024.04.16
스파게티코드 해결방안 고민하기  (3) 2024.04.15