Combine

[WWDC 19] Combine in Practice

Beom 2024. 7. 8. 11:16

 

 

 

 

  • combine의 publisher들은 기본적으로 Publisher라는 프로토콜을 준수한다.
  • 해당 프로토콜에는 2가지의 유형이 존재한다. 출력값과 실패(error)
  • Subscriber의 Input과 Failure가 Publisher의 유형과 동일해야 한다.

 

 

  • NotificationCenter는 publisher를 지원한다.
  • Publisher에서의 중요한 점은 실제 ouput 및 failure의 타입이 무엇인지 아는 것이 중요하다.
  • Notification은 실패할 수 없기 때문에
    • Output은 Notification
    • Failure는 Never

 

  • 기존에 쓰던 Map과 비슷한기능을 제공한다.
  • 그렇기 때문에 publisher의 output인 Notification내부 userInfo에 Data로 변환이 가능한 값이 있기 때문에 map을 통해 Data로 변환이 된다.
  • Output은 Notification → Data
  • Failure는 Never

 

  • 데이터를 Json을 통해 MagicTrick으로 받아오기 위해 tryMap을 사용한다.
  • decoding을 할때 try구문을 사용하기 때문
  • try구문을 사용하기 때문에 기존에 없던 error가 생길 수 있게 된다.
  • Output은 Notification → Data →MagicTrick
  • Failure는 Never → Error

 

  • decode작업은 매우 일상적인 작업이기 때문에 combine 내부에 decode Operator가 존재
  • Output은 Notification → Data →MagicTrick
  • Failure는 Never → Error

 

  • 모든 publisher는 실패가 가능하기 때문에 이것에 대한 적절한 처리가 필요하다.

 

  • failure의 가장 간단한 처리 중 하나는 failure가 발생할 수 없다고 단정하는 것
  • 이러한 assertNoFailure를 사용하면 다시 failure는 Never가 된다.
  • Output은 Notification → Data →MagicTrick
  • Failure는 Never → Error →Never

 

물론 assertNoFailure를 선언한 곳에서 error가 들어오게 되면 fatal error가 발생한다.

 

  • Failure의 Handling Operator는 여러가지가 존재한다.
  • 다음에 다 정리해 보겠다.

 

  • 그 중 catch라는 operator는 upstream publisher에서 발생한 오류를 recovery publisher로 보내는 것이 유용하다.

 

  • 기존과 동일하게 사용을 하다 오류가 오면 기존의 publisher와의 연결을 끊고 제공된 recovery closure를 호출하여 새로운 publisher를 생성하고 값을 받아온다.

 

  • 기존의 코드로 돌아와서 catch를 사용하면 error가 발생시 조치를 취할 수 있다.
  • Just publisher는 항상 값이 존재한다. 오류로 실패 할 수 없다.
  • Output은 Notification → Data →MagicTrick
  • Failure는 Never → Error(오류 발생시 조치를 취함) →Never

  • 지금까지의 과정은 그림과 같다.
  • 하지만 여기서 decode의 error가 발생해 catch로 placeholder publisher를 내보내면 기존에 연결이 되어있던 notification이랑 연결이 끊겨 더 이상 알림을 못받는다.

  • flatMap을 통해 에러가 발생하면 placeholder를 내보내지만 업스트림과 연결이 끊기지 않게 할 수 있다.

 

  • 이것을 코드에 적용하면 그림과 같다.
  • Just가 앞에 추가가 되었는데 decode가 실패하면 catch를 통해 MagicTrick의 placeholder가 나오게 되고 성공하는 경우에는 기존과 동일하게 MagicTrick이 나오게 되므로 실패가 불가능한 Just를 작성하여 사용해 준다.

 

  • publisher(for:) 오퍼레이터로 내부의 값을 publisher로 생성이 가능하다.

 

  • 언제 어디서 전달되는지 설명하는데 도움이 되고
  • RunLoop와 DispatchQueue의 사용이 가능하다.

 

여러가지가 존재한다

 

  • 메인에서 실행이 되도록 확정 짓는다.

 

다음과 같은 흐름으로 진행이 된다

 

 

 

 

  • publisher와 마찬가지로 subscriber도 2가지의 타입이 존재

 

  • subscibe 호출에 대한 응답으로 publisher는 receive(subscription:)을 정확히 한 번 호출한다.

 

 

  • receive(_:)  => publisher가 subscriber를 요청한 뒤 다운 스트림으로 0개 이상의 값을 제공하는 것이 가능하다.

 

  • receive(completion: ) publisher는 최대 하나의 completion을 보낼 수 있고 해당 completion은 publisher가 완료하거나 오류가 발생했음을 나타낼 수 있다.
  • completion이후에는 값을 방출하지 않는다.

 

  • subscription은 1번
  • value는 보내는 만큼
  • completion은 최대 1번
    • 이전 코드의 notification처럼 많은 스트림이 있어서 무한할 수 있다.( completion이 존재하지 않음)

 

 

  • 첫 번째 Key Path Assignment 방식
  • assign은 publisher로 부터 받은 값을 someObject의 someProperty에 할당 할 수 있도록 해준다.
  • assign은 Failure가 Never일때만 사용이 가능하다.

 

  • publisher로부터 값을 받아오지 않기 위해서 cancel이라는 메서드를 실행 가능하다.
  • 직접적인 호출도 가능하지만
  • deinit시 자동으로 cancel를 호출하는 anyCancellabel이라는 기능을 제공하기도 한다.

 

  • publisher로 부터 받은 값을 내가 원하는 형태로 가공이 가능하다.
  • assign과 마찬가지로 cancellable을 반환한다.
  • Failure가 Never일 때 사용이 가능하다.

 

subscriber와 publisher의 중간의 느낌이다.

 

  • 수신한 모든 값을 모든 다운스트림 subscriber에게 전달한다.

 

 

  • 값이 업스트림 publisher에 의해 생성된 경우도 똑같다

 

  • 2가지의 subject가 존재한다.
  • 값을 저장하지 않는 passthrough 그렇기 때문에 subject를 subscribe한 경우에만 값을 볼 수 있다.
  • 값을 저장하는 CurrentValue는 새로운 subscriber가 나중에 구독을 해도 값을 알 수 있다.

 

 

 

  • subject는 subscriber의 특징을 가지기도 하기 때문에 publisher를 구독할 수 있다
  • share를 통해 cancellable를 당 receive를 하는 것이 아닌 동일한 값을 받게 된다.

 

let pub = (1...3).publisher
    .delay(for: 1, scheduler: DispatchQueue.main)
    .map( { _ in return Int.random(in: 0...100) } )
    .print("Random")
    .share()


cancellable1 = pub
    .sink { print ("Stream 1 received: \($0)")}
cancellable2 = pub
    .sink { print ("Stream 2 received: \($0)")}


// Prints:
// Random: receive value: (20)
// Stream 1 received: 20
// Stream 2 received: 20
// Random: receive value: (85)
// Stream 1 received: 85
// Stream 2 received: 85
// Random: receive value: (98)
// Stream 1 received: 98
// Stream 2 received: 98

공식 문서에 나와 있는 share예시 코드이다

share를 사용할때와 안할때의 출력 차이가 나게 된다.

Random: receive value: (54)
Stream 2 received: 54
Stream 1 received: 54
Random: receive value: (26)
Stream 2 received: 26
Stream 1 received: 26
Random: receive value: (85)
Stream 2 received: 85
Stream 1 received: 85
// share사용한 경우



Random: receive value: (58)
Stream 1 received: 58
Random: receive value: (20)
Stream 1 received: 20
Random: receive value: (27)
Stream 1 received: 27

Random: receive value: (41)
Stream 2 received: 41
Random: receive value: (29)
Stream 2 received: 29
Random: receive value: (24)
Stream 2 received: 24
// share 사용 안한 경우

 

 

4번째는 SwiftUI다

 

 

  • 실패하지 않는 것을 확정받는 publisher다
  • 언어의 내부적 시스템이 업스트림 오류가 publisher에 도달하기 전 처리 하도록 강제되기 때문에 UI 프레임워크 작업에 좋다
  • 하나의 프로퍼티를 지정하면 didchange라는 프로퍼티가 변경될때 이것을 알리는 publisher를 생성한다.
  • 이름이 ObservableObject로 변경이 되었다고 한다

  • 이런 식으로 사용한다.

  • 강력하고 90개 이상의 operator가 존재한다고 한다

 

 

예제를 통해 알아보자

  • 이름이 유효한지 서버로 부터 살펴보고
  • password가 2개가 일치하는지, 8글자 이상인지 확인
  • 모든 조건이 맞으면 UI를 활성화 시켜준다.
  • 이 예제에는 비동기 동작과 로컬 동작이 모두 있다.

  • value의 변경을 IBAction으로 연결

 

  • 기존에 이렇게 작성된 코드를 combine의 publisher로 만들기 위해서
  • @Published라는 Property Wrapper를 작성해주면 된다

 

 

 

 

  • published로 작성된 프로퍼티를 $를 사용하여 작성하게 되면 password의 값을 변경시 $로 작성한 내부의 값이 실행되게 된다

 

  • 기존의 password와 passwordAgain을 동시에 조건에 맞는 비밀번호인지 판단을 해야 한다.

 

  • combineLatest를 사용하면 $가 붙은 property wrapper를 참조할 수 있다.

 

 

  • eraseToAnyPublisher를 통해 failure가 Never인 publisher로 변경이 된다.

  • combineLatest를 통해 2개의 publisher를 묶어서 내보낸다.

 

  • 이렇게 처리를 해서 비밀 번호를 확인하는 publisher는 생성이 되었다

 

  • 이제 서버로 부터 username을 확인하는 것을 할 차례인데 사용자가 입력을 할때 마다 즉시 요청을 보내면 안된다.
  • 그렇기 때문에 debounce라는 개념을 사용해서 서버에 보내는 요청을 조절한다.

 

 

  • 사용자가 입력한 값의 마지막이 동일하다면 다시 요청을 보낼 필요가 없기 때문에 같은 요청은 제거를 해준다.
  • 예시) Merlin을 입력하고 n을 삭제하고 다시 n을 입력해서 Meriln을 동일하게 입력하는 행위

 

  • 그러면 이런 방식으로 작성이 된다. 하지만 아직 비동기 통신을 하지 않았기 때문에
  • usernameAvailable이라는 함수를 publisher와 합쳐주어야 한다.

 

 

  • promise는 성공과 실패 결과를 가지는 다른 클로저다

 

  • usernameAvailable 함수를 호출하고 해당 함수가 비동기 적으로 완료가 되고 값을 갖게 되면 성공으로 이행한다.

 

  • 값을 입력받고 debounce로 조절하고 remove Duplicates로 동일한 요청은 보내지 않으며 flatMap을 통해 오류가 있어도 연결이 끊기지 않게 하고 Future를 통해 비동기 네트워크 호출을 하고 eraseToAnypublisher로 값을 내보낸다

 

  • 결과로 동기적으로 수행해서 password의 결과를 가지고 오고
  • 비동기적 요소가 들어간 username을 가지고 온다.

 

  • 결합하여 1개의 값이 잘못된 값이면 nil을 가지고 오도록 설정

 

  • 결과 값에 따라서 버튼의 활성화 유무를 확인한다