[TIL / 25.10.11] Combine,, 실습과 함께 훑어봤어요 2

2025. 10. 11. 23:30·iOS/Swift

1. 목표

https://jsonplaceholder.typicode.com/users

유명한 open api 중 하나인 해당 주소에서 얻어온 users를 테이블뷰에 띄우는 실습을 해봤다.

 

제대로 구현이 된다면 아래와 같은 결과가 나올 것이다.

2. Presentation Layer

 

우선, 프로젝트 내 아키텍처는 새로 들어가는 프로젝트와 일부 유사하게 clean architecture 기반으로 가져가고 그 외 네트워크는 urlsession 사용한다. 전체적으로 퍼스트파티 위주로 구현한다.

 

우선 View에 대해선 반응형을 쓰지 않을때와 같았다.

Rx를 썼을 때엔 RxCocoa를 사용해 UI 컴포넌트에 대해 여러 기능들을 제공받았는데, Combine엔 RxCocoa같은 게 없다고 한다..!

사실 있는데 아직 처음 공부하는 과정이라 모르는 걸수도 있다!

 

View 코드는 다음과 같다.

Combine 관련 코드는 딱히 없었기 때문에 추가 설명은 넘기는 것으로 하고,

Publisher 등의 개념이 적용되는 부분은 ViewController, ViewModel에서 볼 수 있다.

 

ViewModel에선 Rx 환경에서 하던 것처럼 Input, Output 패턴을 유용하게 쓸 수 있기에 써보았다.
이번엔 Input, Output, transform을 바로 class에 정의하는게 아닌 protocol을 정의해서 채택하도록 구현했다.

왜 굳이 프로토콜화하는거지? 했는데, 프로토콜로 정의해두고 채택함으로써 구조를 일관적으로 가져갈 수 있게 하는 게 큰 것 같다.

 

Rx를 해보았다면 구조가 비슷해 바로 비교가 가능할 수 있을텐데, 우선 구독을 저장해두었다가 메모리 해제 시 저장되어있는 구독들을 일괄 해제하는 DisposeBag은 Combine에서 Set<AnyCancellable>이 되겠다.

만들어두고 sink같이 subscriber로 구독을 한 부분에서 해당 주소를 넣어 .store(in: $cancellabels)해주면 .disposed(by: disposeBag)과 동일한 기능을 한다.

 

그 외 Input, Output 내부에는 Void나 [Profile]을 방출하는 Publisher가 정의되어 있다.

RxSwift를 썼을 땐 아마 Input 내엔 Observable<Void>가 될 것이고, Output 내엔 Observable<[Profile]>이 될 것이다.

앞선 포스트에서 보았듯 Observable은 Combine에서 AnyPublisher이다. 단, AnyPublisher는 타입만 보여줄 게 아니라 실패 시 방출할 타입도 정의해줘야 한다.

 

그렇다면 이전 포스트에서 Combine은 Rx처럼 Relay를 별개로 가지지 않는다고 했는데, 스트림은 에러가 나면 종료가 되어 문제가 되지 않나? 그럴 땐 AnyPublisher<반환 타입, 실패 시 반환 타입>에서 실패 시 반환 타입을 Never로 두고 절대 실패하지 않을 것임을 명시하면 스트림 입장에서는 끊길 일이 없어 Relay와 동일한 역할이 되는 듯하다.

 

그럼 이제 transform 함수 내부를 보면 input에 대해 구독을 하고 완료가 되었을 때 그리고 값을 받았을 때에 어느 동작을 할지 정의할 수 있는데 이러한 점도 Rx의 subscribe(onCompleted: , onNext: )와 같다.

onNext = receiveValue, onCompleted: receiveCompletion이라고 보면 될 것 같다.

 

보통 rx할 때엔 receiveValue만 처리해주곤 했는데 여기선 completion 처리도 해주었던 이유는 에러 핸들링을 하기 위함이다.

이 부분은 Data Layer에서 보도록 하겠다.

 

VC에선 앞서 말한 내용들이 대부분 똑같이 들어간다. 바인딩을 담당하는 setBindings에서 다양한 구독을 하게 될텐데 이 구독이 있으면 해제할 수 있는 수단도 있어야 하기에 똑같이 Set<AnyCancellable>을 선언해 사용한다.

 

데이터 스트림에 값을 외부에서 주입해줄 수 있도록 send() 메서드를 쓰게 되는데, VC의 viewDidLoad 생명 주기에 send를 통해 ViewModel 측 트리거를 작동하여 api 호출 후 users를 가져온다. 프로젝트에선 users인지 모르고 profiles라고 했지만!

덧붙이자면 viewDidLoad는 어느 값을 저장해두는 성격이 아니라 이벤트의 성격을 가지기 때문에 PassthroughSubject를 사용했다.

rx에서 publishsubject가 passthroughSubject고, behaviorsubject같이 값을 담아두는 게 currentValueSubject다.

에러 타입으로 Never를 지정해서 relay와 같은 성격으로 만들었다.

 

Rx와 차이점을 느꼈던 가장 큰 부분이 이 ViewController인데 UITableView의 데이터를 주입해줄 때 Rx 기준엔 RxDataSources를 사용하여 따로 주입해주었다. 반면 Combine은 관련 기능이 없어 퍼스트파티 기준으로 DataSource, Delegate를 작성해야 했고 setBindings에서 api 통신으로 받아온 profiles를 dataSource에 바로 넣어줄 수 없었다.

이를 위해 VC에서 임시로 담아둘 수 있도록 변수를 만들어두고 전달했다. 그리고 값을 넣어주었으니 reload 또한 필요했다.

서드 파티에 의존하게 되면 사이드 이펙트를 야기할 수 있는데 퍼스트 파티로 편의성을 일부 포기하는 느낌이 들었다.. 너무 Rx에 길들여져 있었나보다.

 

3. Data Layer

Data Layer는 api 호출을 담당하는 service를 보겠다.

우선 Combine의 publisher의 성격에 맞춰 URLSession에서도 dataTask에서 디벨롭된 dataTaskPublisher를 제공한다.

이런 타입이 되는 듯한데 자세한건 잘 모르겠고, 타 Publisher처럼 쓸 수 있는 것 같다. 

그 과정 속에서 Response 모델에 decode할 수 있게 메서드가 있었고, 이렇게 디코드된 데이터들을 레포지토리에 넘겨줘야 하기 때문에

마지막엔 eraseToAnyPublisher 처리를 해주었다.

Combine의 eraseToAnyPublisher는 Rx의 asObservable과 같다고 볼 수 있다.

Rx에선 Observable인 relay, subject 등 객체들을 다시 기초적인 Observable로 만들어주는데 combine도 같은 맥락이다.

 

 

같은 기능을 원래 쓰던 방법인 dataTask 기준으로 작업하려면 해당 코드의 방향성을 가지게 되는 것 같다.

Rx에선 네트워킹 메서드에 Single을 주로 사용하는 걸 봤는데 Combine에선 Single과 같은 역할을 하는 게 Future라고 한다.

값을 한번 방출하거나 에러를 방출하면 바로 그 스트림은 끝나게 되는 점을 이용해 Future 내부에 api 호출 및 decode하는 과정을 정의했다.

Service에서 마지막에 eraseToAnyPublisher를 통해 AnyPublisher라는 최종 형태로 반환을 해주었기 때문에 이를 받아 map 연산자를 통해 도메인 레이어 내 정의되어있는 Profile 모델로 변환한다.

이 과정 마지막에도 똑같이 eraseToAnyPublisher 처리를 해준다. 왜 이렇게 하나 했는데 가장 본래의 형태인 AnyPublisher로 주고 받는게 전반적으로 더 유연할 수 있고 장점이 많다고 하는 것 같다.

 

3. Domain Layer

 

마지막으로 Data Layer와 Presentation Layer의 사이인 Domain Layer 내 UseCase에선 프로필들을 받아 별도의 비즈니스 로직을 수행해야 할 필요가 없기 때문에 그대로 전달만 하도록 구현했다.

 


 

당장 실습은 이렇게 해두었고 크게 보았을 때, RxSwift 환경에서 구현했던 과정 내 많은 요소들이 Combine에서 제공되기 때문에
어떤 키워드를 사용하면 좋을지 익혀두면 Combine도 기초 단계까지는 금방 적응할 수 있을 것 같다.

'iOS > Swift' 카테고리의 다른 글

[TIL / 25.10.11] Combine,, 실습과 함께 훑어봤어요 1  (0) 2025.10.11
[25.08.14] MOUP 리팩토링 및 트러블슈팅 - 동적 높이를 가지는 테이블뷰 셀  (3) 2025.08.14
[25.08.04] MOUP 트러블슈팅 - .xcconfig 내 url 설정 시 주의사항  (5) 2025.08.05
[25.07.31] MOUP 트러블슈팅 - tableHeaderView 레이아웃 제약 경고  (2) 2025.07.31
[25.06.24] MOUP 트러블슈팅 - Listener와 Rx의 timeout  (0) 2025.06.24
'iOS/Swift' 카테고리의 다른 글
  • [TIL / 25.10.11] Combine,, 실습과 함께 훑어봤어요 1
  • [25.08.14] MOUP 리팩토링 및 트러블슈팅 - 동적 높이를 가지는 테이블뷰 셀
  • [25.08.04] MOUP 트러블슈팅 - .xcconfig 내 url 설정 시 주의사항
  • [25.07.31] MOUP 트러블슈팅 - tableHeaderView 레이아웃 제약 경고
subkyu-ios
subkyu-ios
subkyu-ios 님의 블로그 입니다.
  • subkyu-ios
    subkyu-ios 님의 블로그
    subkyu-ios
  • 전체
    오늘
    어제
    • 분류 전체보기 (59) N
      • iOS (40) N
        • Swift (40) N
      • 내일배움캠프 (7)
      • Git, Github (3)
      • Algorithm (6)
      • 회고 (1)
      • 면접 질문 정리 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    내일배움캠프
    algorithm
    본캠프
    프로그래머스
    ios
    회고
    github
    Swift
    stackview
    Wil
    til
    반응형 프로그래밍
    KPT
    tabman
    트러블슈팅
    사전캠프
    RxSwift
    알고리즘
    의존성 주입
    UIKit
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
subkyu-ios
[TIL / 25.10.11] Combine,, 실습과 함께 훑어봤어요 2
상단으로

티스토리툴바