본문 바로가기

약간은 더 현실적인 Combine 예제들

기술적인 이야기/애플 플랫폼 개발 2020. 1. 27.
반응형

컴바인에 대한 여러 개념적인 이야기들을 써오긴 했는데 역시 개념 설명 글은 와 닿는 것이 없는 것 같습니다. 아무래도 현실적으로 현업에서 쓸 만한 예제는 아니니깐요. 그래서 이번에는 조금은 더 실용성이 있어 보이는 컴바인 예제 투성이라는 주제로 글을 써봅니다.

기본 자료구조의 퍼블리셔

스위프트(Swift)는 다채로운 자료구조를 제공합니다. 예를 들어 기본 타입도 있겠지만 배열(array)이나 사전형(dictionary) 같은 복잡한 자료구조도 있지요.

대표적으로 Sequence.publisher를 살펴보겠습니다. 이 타입은 보통 배열(array) 혹은 리스트(list)라고도 불리며 굉장히 자주 쓰이는 타입이지요?

let publisher = [1, 2, 3, 4, 5].publisher
publisher
    .filter { $0 % 2 == 0 }
    .map { $0 * $0 }
    .sink { (value) in
            print(value)
    }

위 예제의 첫 줄에서 .publisher라는 프로퍼티를 통해 간단하게 해당 리스트의 퍼블리셔를 구해와서 체인을 구성하는 모습을 볼 수 있습니다. 결과는 다들 파악하시겠지만 4와 16이 표시됩니다. filter를 통해 짝수만 다음 체인으로 넘어가며, map을 통해 제곱 값을 체인으로 밀어 넣습니다. 그래서 그 결과가 sink를 통해 출력되지요.

.assign 컨슈머

서브스크라이버로 sink 컨슈머를 주로 써 왔습니다만 이번에는 다른 컨슈머인 assign을 살펴봅시다. 이 컨슈머는 키 패스(Key Path)를 통해 최종 결과를 전달하는 데 최적화된 서브스크라이버입니다.

아래 예제는 위 예제에서 sink 대신 assign을 쓰기 위해 좀 수정한 코드입니다.

class Dumper {
    var value = 0 {
        didSet {
            print("value was updated to \(value)")
        }
    }
}

let dumper = Dumper()
let publisher = [1, 2, 3, 4, 5].publisher
publisher
    .filter { $0 % 2 == 0 }
    .map { $0 * $0 }
    .assign(to: \.value, on: dumper)

Dumper라는 클래스를 만들었는데, 이 클래스의 value 프로퍼티에 값을 지정하면 그 값을 콘솔에 표시하는 기능을 가지고 있습니다.

그리고 앞의 예제와 동일한 리스트의 퍼블리셔 체인 끝에는 assign을 이용해 dumper 오브젝트의 value 프로퍼티에 결과를 전달하도록 구현했습니다. 이 코드가 실행되면 콘솔에는 "value was updated to 4", "value was updated to 16"라는 두 줄의 문자열이 찍힙니다. Dumper 클래스를 통해 출력된 결과지요.

살짝 정리하자면 sink가 '이벤트 체인'의 끝을 장식한다면 assign은 '데이터 체인'의 끝을 장식한다고 볼 수도 있겠네요.

 

@Published

스위프트의 기본 타입을 사용한다면 @Published라는 Property Wrapper를 이용해 퍼블리셔를 구성할 수도 있습니다. 제약 사항으로 클래스(class)의 프로퍼티에만 이 형식을 사용할 수 있다는 점을 우선 알아둡시다.

class SomeModel {
    @Published var name = "Test"
}

위의 SomeModel 클래스는 name이라는 프로퍼티를 가지고 있는데 이걸 퍼블리셔로 쓸 수 있습니다. 아래와 같은 식이지요.

let model = SomeModel()
let publisher = model.$name
publisher
    .filter { $0.count > 0 }
    .sink(receiveValue: { value in
        print("name is \(value)")
    })

두 번째 라인에 .$name라는 것을 볼 수 있는데 이것이 바로 퍼블리셔입니다. 당연히 달러마크($)는 오타가 아닙니다. 이렇게 해서 name 프로퍼티를 퍼블리셔로 써서 체인을 만들었습니다.

이 코드가 실행되고 난 후 name의 값이 변경되면 이 퍼블리셔의 체인에 데이터가 들어가게 됩니다. 그런데 초기값도 이 체인을 타기 때문에 최초 한번 아래와 같은 내용이 콘솔에 표시됩니다.

"name is Test"

위 코드 이후에 연달아서 아래와 같은 코드를 추가해 봅시다. 그리고 이벤트에 따른 동작이 발생하는지 파악해 봅시다.

model.name = ""
model.name = "John"

위 코드가 실행되면 아래 한 줄이 콘솔에 표시됩니다.

"name is John"

filter를 통해 name이 빈 문자열이면 걸러내도록 했기 때문에 당연히 결과는 하나만 표시되지요.

이렇게 클래스 프로퍼티를 퍼블리셔로 간결하게 사용할 수 있는 방법이 있습니다.

 

파운데이션에서 찾아보기

Swift 5.1에서는 파운데이션(Foundation)이나 UIKit, AppKit 등 기본 프레임워크의 다양한 기능들이 컴바인에 대응하고 있습니다. 이번에는 그 예로 URLSession.dataTaskPublisher를 살펴보겠습니다. 이름에서 알 수 있겠지만 인터넷에서 자료를 다운로드하는 기능을 제공하는 URLSession의 Data Task 퍼블리셔입니다.

let p = URLSession
    .shared
    .dataTaskPublisher(for: URL(string: "https://seorenn.tistory.com")!)
    .filter({ (data, response) -> Bool in
        guard let r = response as? HTTPURLResponse,
            200..<300 ~= r.statusCode else {
                return false
        }
        return true
    })
    .sink(receiveCompletion: { (completion) in
        // do nothing... :)
    }) { (data, response) in
        guard let html = String(data: data, encoding: .utf8) 
            else { return }
        if let s = html.range(of: "<title>"),
            let e = html.range(of: "</title>") {
            let range = s.upperBound..<e.lowerBound
            print(html[range])
        }
    }

위 코드가 실행되면 제 블로그의 인덱스 페이지를 로딩해서 filter를 통해 리퀘스트가 성공하면 최종 sink로 데이터가 전달됩니다. 그리고 sink에서는 <title>...</title> 사이의 문자열을 부분적으로 추출(substring)해서 콘솔에 표시합니다. 즉 현재 시점(2020년 1월)에는 아래의 내용이 콘솔에 표시됩니다.

"SEORENN NOTEBOOK"

궁금하다면 사용하는 기본 프레임워크의 레퍼런스 문서에서 publisher와 관련된 것이 있는지 찾아봅시다. 예를 들어 NotificationCenter.publisher 등등 굉장히 많습니다.

관련 글

728x90
반응형

댓글