Combine

Dophi·2023년 1월 4일
0

iOS

목록 보기
2/5

소개글

iOS 개발을 하면서 헷갈렸던 개념들을 다시 정리해보고 있습니다.
만약 틀린 내용이 있다면 피드백은 언제나 환영합니다.
더 자세하고 알고싶다면 아래쪽 참고사이트에서 확인하면 좋을 것 같습니다!
말투는 편한 말투로 작성하니 양해 부탁드립니다.

Combine

정의

비동기 작업들을 이벤트 처리 연산자로 결합하여 처리하는 프레임워크

쓰는 이유

  • Combine이 없더라도 기존에는 Delegate 패턴, 콜백 함수, completion 클로저 등을 활용하여 비동기 처리를 구현했음
  • 하지만 여러개의 비동기 처리가 연달아 있는 경우, 클로저가 중첩되어 쓰이거나 delegate가 여러 개가 되는 등 코드가 지저분해짐
  • Combine은 선언적 프로그래밍 형태기 때문에 깔끔하게 처리 가능

선언적 프로그래밍 형태
Stream 하나에 필요한 Operator들을 추가하여 쓰는 방식

// 선언형 프로그래밍인 Combine 예제 코드
$username
	  .debounce(for: 0.1, scheduler: RunLoop.main)
	  .removeDuplicates()
	  .map { $0.count >= 2 }
	  .assign(to: \.valid, on: self)
	  .store(in: &cancellableSet)

Combine 용어

Publisher, Subscriber, Subscription, Subject, Scheduler, Operator

Publisher

  • 하나 이상의 Subscriber 객체에게 값을 전달하는 역할
  • Output, Failure 타입이 제네릭이기때문에 따로 지정해줘야함
  • 사용 방법
    • 프로토콜이기 때문에 이를 채택하는 class 또는 struct를 직접 구현하여 사용 가능

      // 직접 구현
      class MyPublisher: Publisher {
          typealias Output = Int // 성공 타입
          typealias Failure = Never // 실패 타입
          
          // subscription을 만들고 subscriber에게 전달
          func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, String == S.Input {
              let subscription = CustomSubscription(subscriber: subscriber)
              subscriber.receive(subscription: subscription)
          }
      }
    • 이미 구현되어있는 AnyPublisher, Future, Just, Deferred, Empty, Fail, Record 사용 가능

      // 예시1: Sequence에 정의되어있는 Publisher
      let sequencePublisher = [1,2,3].publisher
      
      // 예시2: Future
      let futurePublisher = Future<Int, Never> { promise in
      	  promise(.success(5))
      }

Subscriber

  • Publisher 객체로부터 값을 받는 역할
  • Input, Failure 타입이 제네릭이기때문에 따로 지정해줘야함
    (Publisher의 Output, Failure 타입과 일치해야함)
  • 사용 방법
    • 프로토콜이기 때문에 이를 채택하는 class 또는 struct를 직접 구현하여 사용 가능

      // 직접 구현
      class MySubscriber: Subscriber {
      	  typealias Input = Int // 성공 타입
      	  typealias Failure = Never // 실패 타입
        
      	  func receive(_ input: Int) -> Subscribers.Demand {
      		    print("데이터 수신: \(input)")
      		    return .none
      	  }
        
      	  func receive(subscription: Subscription) {
      		    print("데이터 구독 시작")
      		    subscription.request(.unlimited) // 구독할 데이터의 갯수를 제한하지 않는 것
      	  }
        
      	  func receive(completion: Subscribers.Completion<Never>) {
      		    print("모든 데이터 발행 완료")
      	  }
      }
      
      let subscriber = MySubscriber()
      
      let subscription = sequencePublisher
      		.subscribe(subscriber)
    • 이미 구현되어있는 AnySubscriber 사용 가능

      // AnySubscriber 예시
      let subscriber = AnySubscriber<Int, Never>(
      	  receiveSubscription: { _ in
      		    print("데이터 구독 시작")
      	  },
      	  receiveValue: { input in
      		    print("데이터 수신: \(input)")
      		    return .none
      	  },
      	  receiveCompletion: { _ in
      		    print("모든 데이터 발행 완료")
      	  }
      )
      
      let subscription = sequencePublisher
      		.subscribe(subscriber)
    • sink 함수를 써서 Publisher 내부에서 Subscriber가 생성되도록 함

      let subscription = sequencePublisher
      		.sink(
      					receiveCompletion: { completion in
      					    print(completion)
      					}, 
      					receiveValue: { s in
      					    print("\(s) 받음")
      					}
      			)

Subscription

  • Publisher와 Subscriber의 연결을 나타내는 프로토콜
  • Subscription 자체도 프로토콜이며, Cancellable이란 프로토콜을 채택함
  • 연결을 끊고 싶을 때 cancel 함수 사용
subscription.cancel()

Subject

  • Publisher의 일종 (Publisher 프로토콜을 채택)
  • 값을 보낼 수 있는 send 함수가 존재
  • PassthroughSubject와 CurrentValueSubject이 존재
    • CurrentValueSubject는 최신값 저장 O, PassthrougSubject는 최신값 저장 X

      // CurrentValueSubject 예시
      let currentValueSubject = CurrentValueSubject<String, Never>("첫번째 값")
      currentValueSubject
          .sink(
      					receiveCompletion: { completion in
      					    print(completion)
      					}, 
      					receiveValue: { s in
      					    print("\(s) 받음")
      					}
      			)
      
      currentValueSubject.send("두번째 값")
      currentValueSubject.send("세번째 값")
      currentValueSubject.send(completion: .finished)
      
      // -- 콘솔 --
      // 첫번째 값 받음
      // 두번째 값 받음
      // 세번째 값 받음
      // finished
      
      
      // PassthroughSubject 예시
      let passthroughSubject = PassthroughSubject<String, Never>()
      passthroughSubject 
          .sink(
      					receiveCompletion: { completion in
      					    print(completion)
      					}, 
      					receiveValue: { s in
      					    print("\(s) 받음")
      					}
      			)
      
      passthroughSubject .send("두번째 값")
      passthroughSubject .send("세번째 값")
      passthroughSubject .send(completion: .finished)
      
      // -- 콘솔 --
      // 두번째 값 받음
      // 세번째 값 받음
      // finished

Scheduler

  • DispatchQueue.main, DispatchQueue.global과 같은 쓰레드를 지정하는 함수
  • 굳이 지정하지 않더라도 기본적으로 element와 생성된 스레드와 동일한 스레드 사용
    let subject = PassthroughSubject<Int, Never>()
    
    subject .sink(
    		receiveValue: { value in
    	      print(Thread.isMainThread) // true
    		}
    )
    
    subject.send(1)
    let subject = PassthroughSubject<Int, Never>()
    
    subject .sink(
    		receiveValue: { value in
    	      print(Thread.isMainThread) // false
    		}
    )
    DispatchQueue.global().async {
    		subject.send(1)
    }
  • receive와 subscribe가 존재
    • receive는 호출한 시점부터 해당 쓰레드로 변경

      publisher
          .map { _ in print(Thread.isMainThread) } // true
          .receive(on: DispatchQueue.global()) 
          .map { print(Thread.isMainThread) } // false
    • subscribe는 어느 시점에서 호출하든 상관없이 시작하는 시점의 쓰레드를 변경

      publisher
          .map { _ in print(Thread.isMainThread) } // false
          .subscribe(on: DispatchQueue.global()) 
          .map { print(Thread.isMainThread) } // false
      
      publisher
          .map { _ in print(Thread.isMainThread) } // true
          .subscribe(on: DispatchQueue.main()) 
          .map { print(Thread.isMainThread) } // true
      

Operator

  • Publisher에서 Upstream의 값을 받아서 원하는대로 처리하고 Downstream으로 전달
  • map, filter, max, contains 등 매우 다양함
// map 예시
let subscription = [1, 2, 3].publisher
	  .map { $0 + 2 }
	  .sink(receiveValue: { print($0) })

// -- 콘솔 --
// 3
// 4
// 5

참고 사이트

https://icksw.tistory.com/271
https://ios-development.tistory.com/1112
https://hereismyblog.tistory.com/14
https://zeddios.tistory.com/972

profile
개발을 하며 경험한 것들을 이것저것 작성해보고 있습니다!

0개의 댓글

관련 채용 정보