AsyncSequence 란
기존의 Sequence는 한 번에 하나씩, 단계별(step)로 진행할 수 있는 값 목록(list of values)입니다.
여기에 비동기성을 추가한 것이 바로 AsyncSequence입니다.
내부 구현부
먼저 AsyncSequence 내부를 살펴봅시다.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@rethrows public protocol AsyncSequence {
// AsyncIterator는 반복을 위해 AsyncIteratorProtocol을 구현해줘야 하네요.
associatedtype AsyncIterator : AsyncIteratorProtocol
// 사용할 요소의 타입을 정해줄 수 있는 것 같네요.
associatedtype Element where Self.Element == Self.AsyncIterator.Element
// 반복동작을 위해 위에 만든 AsyncIterator를 리턴합니다.
func makeAsyncIterator() -> Self.AsyncIterator
}
구조는 Async(비동기)라는 키워드만 붙었지 Sequence와 완전히 동일한 것 같네요.
Sequence에 대한 자세한 정보는 [iOS/Swift] 프로토콜 Sequence 을 참고해 주세요.
개요
Sequence로 요소를 반복하는 가장 일반적인 방법은 for문(for-in loop)을 사용하는 것입니다.
대신 비동기 작업이 추가되므로 await
통하여 대기 지점을 만들어 줘야 되겠죠.
for await value in AsyncSequenceType {}
AsyncSequence가 종료되는 조건으로는
next()
작업에서 리턴 값으로nil
을 반환할 때- 에러가 발생할 때 (throwing an error)
비동기적으로 작업을 한다는 의미는 실패를 할 수 있다는 의미를 갖고 있습니다. (즉. 에러를 던질 수 있죠)
이제 아래에서 예제에서 반복을 만들고 종료하는 법을 같이 확인해 봅시다.
커스텀 타입에서 프로토콜 AsyncSequence 적용 방법
Sequence와 거의 동일한 방법으로 커스텀 타입을 만듭니다.
[iOS/Swift] 프로토콜 Sequence 의 커스텀 타입을 만드는 방법 및 공식문서 를 참고해 주세요.
struct Danny: AsyncSequence {
typealias Element = Int
func makeAsyncIterator() -> DannyAsyncIterator {
return DannyAsyncIterator(count: 5)
}
}
struct DannyAsyncIterator: AsyncIteratorProtocol {
var count: Int
mutating func next() async -> Int? {
if count < 0 { return nil }
defer { count -= 1 }
return count
}
}
let danny = Danny()
// 비동기 작업이므로 Task로 묶어서 사용
Task {
for await value in danny {
print(value)
}
}
// 5
// 4
// 3
// 2
// 1
// 0
AsyncIteratorProtocol(반복자)를 만들 때, next()
에서 count가 0이 될 때까지 반복하게 만들어 줬습니다.
(0이 되면 nil
을 만나게 되므로 반복이 종료됩니다.)
또한, 비동기 작업이므로 Task로 묶어서 사용해 줬습니다.
AsyncSequence와 AsyncIteratorProtocol 합쳐서 사용하기
사용자 타입을 Sequence로 만들 때는 Element
와 makeAsyncIterator()
가 생략이 가능했지만,
🤦 AsyncSequence에서는 기본 구현이 제공이 안되므로 꼭 구현을 해줘야 합니다.
struct Danny: AsyncSequence, AsyncIteratorProtocol {
typealias Element = Int
var count: Int
mutating func next() async -> Int? {
if count < 0 { return nil }
defer { count -= 1 }
return count
}
func makeAsyncIterator() -> Danny {
return self
}
}
let danny = Danny(count: 5)
Task {
for await value in danny {
print(value)
}
}
// 5
// 4
// 3
// 2
// 1
// 0
에러처리와 같이 사용하기
일단 에러를 만들기 위해 enum
생성을 해줬습니다.
enum NumberError: Error {
case lessThanZero
var printMessage: Void {
switch self {
case .lessThanZero:
print("Error: 음수를 감지함!")
}
}
}
크게 바뀐 점은 없고, 다만 반복 구현을 하는 next()
메서드를 만들어 줄 때,
에러 구현을 위해 async 뒤에 명시적으로 throws
키워드를 붙여주고, throw
를 통해 에러를 받아 줍시다.
마지막으로 실행 시 try
, do
, catch
로 에러 처리만 해 주면 됩니다.
struct Danny: AsyncSequence, AsyncIteratorProtocol {
typealias Element = Int
var count: Int
// throws, throw를 통해 에러를 정의
mutating func next() async throws -> Int? {
if count < 0 { throw NumberError.lessThanZero }
defer { count -= 1 }
return count
}
func makeAsyncIterator() -> Danny {
return self
}
}
let danny = Danny(count: 5)
// try, do, catch로 에러 처리
Task {
do {
for try await value in danny {
print(value)
}
}
catch {
NumberError.lessThanZero.printMessage
}
}
// 5
// 4
// 3
// 2
// 1
// 0
// Error: 음수를 감지함!
count가 0이 되는 순간 throw
로 에러를 반환하고 반복이 멈추는 것을 확인할 수 있습니다.
다음에는 AsyncSequence와 비슷하지만, 더욱 쉽게 사용 가능한 AsyncStream에 대하여 알아보겠습니다.
이렇게 오늘은 AsyncSequence에 대하여 알아보았습니다.
부족한 설명이지만, 조금은 이해 가셨나요?
틀린 내용이 있다면 언제든지 지적해 주시면 감사히 받겠습니다. 🫠
읽어주셔서 감사합니다 😃
참고
'Xcode > Swift 문법' 카테고리의 다른 글
[iOS/Swift] 형 변환 (Type Casting) (0) | 2023.01.31 |
---|---|
[iOS/Swift] AsyncStream / AsyncThrowingStream (0) | 2023.01.26 |
[iOS/Swift] 프로토콜 Sequence (0) | 2023.01.19 |
[iOS/Swift] AssociatedTypes (프로토콜을 위한 Generic문법) (0) | 2023.01.13 |
[iOS/Swift] 프로토콜 Equatable (타입 비교) (0) | 2023.01.12 |