-
Chapter 10: DebuggingRaywenderlich/Combine: Asynchronous Programming 2020. 8. 13. 01:37
Version
Swift 5.3, iOS 14, Xcode 12
비동기(asynchronous) 코드의 이벤트(event) 흐름(flow)을 이해하는 것은 항상 어려운 일이었다. publisher의 연산자(operators) 체인(chains)이 이벤트(events)를 즉시 내보내지(emit) 않을 수 있기 때문에 Combine의 컨텍스트(context)에서 특히 그러하다. 예를 들어
throttle(for:scheduler:latest:)
와 같은 연산자(operators)는 수신하는 모든 이벤트(events)를 내보내지(emit) 않으므로, 무슨 일이 일어나고 있는지(what’s going on) 이해해야 한다. Combine은 반응형(reactive) 흐름(flows)을 디버깅(debugging)하는 데 도움이 되는 몇 가지 연산자(operators)를 제공한다. 이를 알면 곤혹스러운(puzzling) 상황을 해결하는(troubleshoot) 데 도움이 될 것이다.Printing events
print(_:to:)
연산자(operator)는 publishers에서 어떤 일이 일어나고 있는지 확실하지 않을 때(unsure) 가장 먼저 사용해야 하는 연산자(operator)이다. 이는 무슨 일이 일어나고 있는지에 대한 많은 정보를 출력(prints)하는 passthrough publisher이다.다음과 같은 간단한(simple) 경우(cases)에도:
let subscription = (1...3).publisher .print("publisher") .sink { _ in }
매우 상세한 출력(output)을 보여준다:
publisher: receive subscription: (1...3)
publisher: request unlimited
publisher: receive value: (1)
publisher: receive value: (2)
publisher: receive value: (3)
publisher: receive finished여기에서
print(_:to:)
연산자(operators)는 다음과 같이 많은 정보(information)를 보여준다:- 구독(subscription)을 받으면 출력(prints)하고, 업 스트림(upstream) publisher에 대한 설명(description)을 표시한다.
- 요청(requested) 중인 항목(items) 수를 확인할 수 있도록 subscriber의 수요(demand) 요청(requests)을 출력(prints)한다.
- 업 스트림(upstream) publisher가 내보낸(emits) 모든 값(value)을 출력(prints)한다.
- 마지막으로, 완료(completion) 이벤트(event)를 출력(prints)한다.
TextOutputStream
객체를 받는 추가(additional) 매개 변수(parameter)가 있다. 이를 사용하여 로거(logger)에 출력할 문자열(strings)을 리디렉션(redirect)할 수 있다. 현재 날짜(date) 및 시간(time) 등과 같은 정보(information)를 로그에 추가할 수도 있다. 가능성은 무한하다(The possibilities are endless).예를 들어, 각 문자열(string) 사이의 시간 간격(interval)을 표시하는 간단한 로거(logger)를 만들어, publisher가 값(values)을 얼마나 빨리 내보내(emits)는지 파악할 수 있다:
class TimeLogger: TextOutputStream { private var previous = Date() private let formatter = NumberFormatter() init() { formatter.maximumFractionDigits = 5 formatter.minimumFractionDigits = 5 } func write(_ string: String) { //TextOutputStream 프로토콜은 해당 함수를 구현해야 한다. let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return } let now = Date() print("+\(formatter.string(for: now.timeIntervalSince(previous))!)s: \(string)") previous = now } }
코드에서 사용하는 것은 매우 간단하다:
let subscription = (1...3).publisher .print("publisher", to: TimeLogger()) .sink { _ in }
그리고 결과는 출력 줄(line) 사이의 시간을 표시한다:
+0.00111s: publisher: receive subscription: (1...3)
+0.03485s: publisher: request unlimited
+0.00035s: publisher: receive value: (1)
+0.00025s: publisher: receive value: (2)
+0.00027s: publisher: receive value: (3)
+0.00024s: publisher: receive finished위에서 언급했듯이, 가능성은 무궁무진하다(the possibilities are quite endless here).
Acting on events — performing side effects
정보(information)를 출력(printing out)하는 것 외에도, 특정 이벤트(events)에 대해 해당 연산자 작업을 수행(perform)하는 것이 유용한 경우가 많다. 이를 performing side effects이라고 부르는데, 다른(further) publishers에게 직접적으로(directly) 영향(impact)을 미치지는 않지만 외부(external) 변수(variable)를 수정(modifying)하는 것과 같은 영향을 미칠(effect) 수 있기 때문이다.
handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:)
를 사용하면 publisher의 생명 주기(lifecycle)에서 모든 이벤트(events)에서(intercept) 각 단계(step)의 조치를 취할 수 있다.publisher가 네트워크(network) 요청(request)을 수행(perform)한 다음, 일부 데이터를 내보내는(emit) 작업(issue)을 추적(tracking)한다고 가정해 본다. 실행(run)시에 데이터가 수신(receives)되지 않는다면 무슨 일이 일어났는지 확인해 보기 위해 다음과 같은 코드를 고려(consider)해 볼 수 있다:
let request = URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.raywenderlich.com/")!) request .sink(receiveCompletion: { completion in print("Sink received completion: \(completion)") }) { (data, _) in print("Sink received data: \(data)") }
이를 실행(run)해도 아무것도 출력(print)되지 않는다면, 코드만 보고 문제(issue)를 해결하기는 쉽지 않다.
handleEvents
를 사용하여 상황(happening)을 추적한다. publisher와sink
사이에 다음 연산자(operator)를 삽입(insert)할 수 있다:.handleEvents(receiveSubscription: { _ in print("Network request will start") }, receiveOutput: { _ in print("Network request data received") }, receiveCancel: { print("Network request cancelled") })
그런 다음 코드를 다시 실행(run)한다. 이번에는 몇 가지 디버깅(debugging) 출력(output)이 표시된다:
Network request will start
Network request cancelled이를 확인하면,
Cancellable
을 유지(keep)하는 것을 잊었다는 것을 알 수 있다. 따라서 구독(subscription)이 시작(starts)되지만, 즉시(immediately) 취소(canceled)된다. 이제Cancellable
을 유지하도록(retaining) 코드를 수정한다:let subscription = request .handleEvents...
코드를 다시 실행(running)하면, 올바르게 작동하는 것을 확인할 수 있다:
Network request will start
Network request data received
Sink received data: 153253 bytes
Sink received completion: finishedUsing the debugger as a last resort
다른 어떤 방법도 무엇이 잘못되었는지 파악(figure out)하는 데 도움이 되지 않아서, 디버거(debugger)의 실제 항목을 특정 시간에서(at certain times) 조사할 필요가 있는 경우에 최후의 조치(last resort)로 사용할 수 있는 연산자(operator)가 있다.
첫 번째 연산자(operator)는
breakpointOnError()
이다. 이름에서 알 수 있듯이, 이 연산자(operator)를 사용할 때 업 스트림(upstream) publishers 중 하나에서 오류(error)가 발생(emits)하면 Xcode가 디버거(debugger)를 중단(break)시켜 스택(stack)을 살펴 볼 수 있게 되고, publisher가 오류(errors)를 발생한 이유(why)와 위치(where)를 찾을 수 있게 된다.보다 완전한 형식(variant)은
breakpoint(receiveSubscription:receiveOutput:receiveCompletion:)
이다. 다양한 이벤트(events)에서, 디버거(debugger)의 일시 중지(pause) 여부를 사례별로(case-by-case) 결정할 수 있다.예를 들어, 다음과 같이 특정(certain) 값(values)이 publisher를 통과하는 경우에만 중단하도록 할 수 있다:
.breakpoint(receiveOutput: { value in return value > 10 && value < 15 })
업 스트림(upstream) publisher가 정수(integer) 값(values)을 내보내(emits)지만, 값이
11
~14
이어서는 안 된다고 가정하면, 이 경우에만 중단(break)되도록breakpoint
을 구성(configure)하고 조사(investigate)할 수 있다.조건부(conditionally)로 구독(subscription) 및 완료(completion) 시에 중단(break)할 수도 있지만,
handleEvents
연산자(operator)처럼 취소(cancelations)에는 사용할 수 없다.Note: 중단점(breakpoint) publishers는 playgrounds에서 작동하지 않는다. 실행(execution)이 중단되었다(interrupted)는 오류(error)가 표시되지만, 디버거(debugger)에 표시되지는 않는다.
Key points
print
연산자(operator)를 사용해 publisher의 생명 주기(lifecycle)를 추적할 수 있다.- 자신만(own)의
TextOutputStream
을 생성하여, 사용자 지정(customize) 출력(output) 문자열(strings)을 정의할 수 있다. handleEvents
연산자(operator)를 사용하여 수명 주기(lifecycle) 이벤트(events)를 가져오고(intercept) 작업을 수행(perform)한다.breakpointOnError
및breakpoint
연산자(operators)를 사용하여 특정(specific) 이벤트(events)를 중단(break)한다.
'Raywenderlich > Combine: Asynchronous Programming' 카테고리의 다른 글
Chapter 12: Key-Value Observing (0) 2020.08.14 Chapter 11: Timers (0) 2020.08.13 Chapter 9: Networking (0) 2020.08.13 Chapter 8: In Practice: Project "Collage" (0) 2020.08.11 Chapter 7: Sequence Operators (0) 2020.08.09