Skip to content

Meet Swift Async Algorithms โ€ WWDC 2022

๊น€๊ทœ์ฒ  edited this page Dec 2, 2025 · 1 revision

Swift Async Algorithms ์†Œ๊ฐœ (WWDC 2022)

AsyncSequence ๋ณต์Šต

Swift๋Š” ์˜คํ”ˆ ์†Œ์Šค ํŒจํ‚ค์ง€ ์นดํƒˆ๋กœ๊ทธ๊ฐ€ ์ ์  ํ™•์žฅ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Swift Async Algorithms ํŒจํ‚ค์ง€๋Š” Swift Collections, Swift Algorithms์™€ ํ•จ๊ป˜ํ•˜๋Š” ์ƒˆ๋กœ์šด ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค. ์ด ํŒจํ‚ค์ง€๋Š” AsyncSequence๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ๊ฐ„์— ๋”ฐ๋ผ ๊ฐ’์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์„ธํŠธ์ž…๋‹ˆ๋‹ค.

AsyncSequence๋Š” ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” ๊ฐ’์„ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋Š” ํ”„๋กœํ† ์ฝœ์ž…๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ Sequence์™€ ๋™์ผํ•˜์ง€๋งŒ ๋‘ ๊ฐ€์ง€ ํ•ต์‹ฌ ์ฐจ์ด์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค:

  1. Iterator์˜ next ํ•จ์ˆ˜๊ฐ€ ๋น„๋™๊ธฐ์ ์ด์–ด์„œ Swift ๋™์‹œ์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ’์„ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
  2. Swift์˜ throw ํšจ๊ณผ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž ์žฌ์ ์ธ ์‹คํŒจ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

"In short, if you know how to use Sequence, you already know how to use AsyncSequence." (์š”์•ฝํ•˜๋ฉด, Sequence ์‚ฌ์šฉ๋ฒ•์„ ์•Œ๋ฉด ์ด๋ฏธ AsyncSequence ์‚ฌ์šฉ๋ฒ•๋„ ์•„๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.)

for-await-in ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ˜๋ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. AsyncSequence๊ฐ€ ๋„์ž…๋˜์—ˆ์„ ๋•Œ, map, filter, reduce ๋“ฑ Sequence์—์„œ ๊ธฐ๋Œ€ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฑฐ์˜ ๋ชจ๋“  ๋„๊ตฌ์˜ async ๋ฒ„์ „์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Swift Async Algorithms ํŒจํ‚ค์ง€๋Š” ๋” ๊ณ ๊ธ‰ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ํ†ตํ•ฉํ•˜๊ณ , Clock๊ณผ์˜ ์ƒํ˜ธ ์šด์šฉ์„ ํ†ตํ•ด ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.


๋ฉ”์‹œ์ง• ์•ฑ ์˜ˆ์ œ

์ž‘๋…„์— Swift Algorithms ํŒจํ‚ค์ง€๋ฅผ ์†Œ๊ฐœํ•˜๋ฉด์„œ ๋ฉ”์‹œ์ง• ์•ฑ์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ์•ฑ์„ Swift ๋™์‹œ์„ฑ์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•  ์ข‹์€ ๊ธฐํšŒ๊ฐ€ ๋งŽ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

struct Account {
  var messages: AsyncStream
}  

actor AccountManager {
  var primaryAccount: Account
  var secondaryAccount: Account? 
}

protocol MessagePreview {
  func displayPreviews(_ manager: AccountManager) async
}

์—ฌ๋Ÿฌ ์ž…๋ ฅ AsyncSequence ๊ฒฐํ•ฉ ์•Œ๊ณ ๋ฆฌ์ฆ˜

Zip

์—ฌ๋Ÿฌ ์ž…๋ ฅ AsyncSequence๋ฅผ ํ•จ๊ป˜ ์ž‘์—…ํ•˜๊ธฐ ์œ„ํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํŒจ๋ฐ€๋ฆฌ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋“ค์€ ๋ชจ๋‘ ํ•˜๋‚˜์˜ ํŠน์„ฑ์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค: ์—ฌ๋Ÿฌ ์ž…๋ ฅ AsyncSequence๋ฅผ ๋ฐ›์•„ ํ•˜๋‚˜์˜ ์ถœ๋ ฅ AsyncSequence๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

Zip ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ์—ฌ๋Ÿฌ ์ž…๋ ฅ์„ ๋ฐ›์•„ ๊ฐ ๊ธฐ๋ฐ˜์—์„œ ๊ฒฐ๊ณผ์˜ ํŠœํ”Œ์„ ์ƒ์„ฑํ•˜๋„๋ก ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ Zip ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ Zip ์•Œ๊ณ ๋ฆฌ์ฆ˜๊ณผ ๋™์ผํ•˜๊ฒŒ ์ž‘๋™ํ•˜์ง€๋งŒ, ๊ฐ ๊ธฐ๋ฐ˜์„ ๋™์‹œ์— ๋ฐ˜๋ณตํ•˜๊ณ  ๋ฐ˜๋ณต ์ค‘ ์‹คํŒจ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์˜ค๋ฅ˜๋ฅผ rethrowํ•ฉ๋‹ˆ๋‹ค.

"The asynchronous Zip algorithm works just like the Zip algorithm in the standard library, but it iterates each of the bases concurrently and rethrows errors if a failure occurs on iterating any of them." (๋น„๋™๊ธฐ Zip ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ Zip๊ณผ ๋™์ผํ•˜๊ฒŒ ์ž‘๋™ํ•˜์ง€๋งŒ, ๊ฐ ๊ธฐ๋ฐ˜์„ ๋™์‹œ์— ๋ฐ˜๋ณตํ•˜๊ณ  ๋ฐ˜๋ณต ์ค‘ ์‹คํŒจ ์‹œ ์˜ค๋ฅ˜๋ฅผ rethrowํ•ฉ๋‹ˆ๋‹ค.)

์˜ค๋ฅ˜ rethrow์™€ ํ•จ๊ป˜ ๋™์‹œ ๋ฐ˜๋ณต์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์€ ์ƒ๋‹นํžˆ ๋ณต์žกํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, Swift Async Algorithms ํŒจํ‚ค์ง€๊ฐ€ ์ด ๋ชจ๋“  ๊ฒƒ์„ ์ฒ˜๋ฆฌํ•ด์ค๋‹ˆ๋‹ค.

๋ฉ”์‹œ์ง• ์•ฑ์—์„œ ๋น„๋””์˜ค ๋…นํ™”์˜ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ , ํšจ์œจ์ ์ธ ์ €์žฅ ๋ฐ ์ „์†ก์„ ์œ„ํ•ด ๋น„๋””์˜ค๋ฅผ ์—ฌ๋Ÿฌ ํฌ๊ธฐ๋กœ ํŠธ๋žœ์Šค์ฝ”๋”ฉํ•˜๋Š” ๋งŽ์€ ์ฝ”๋“œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. Zip์„ ์‚ฌ์šฉํ•˜๋ฉด ํŠธ๋žœ์Šค์ฝ”๋”ฉ๋œ ๋น„๋””์˜ค๊ฐ€ ์„œ๋ฒ„๋กœ ์ „์†ก๋  ๋•Œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ํ•จ๊ป˜ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Zip์€ ๋™์‹œ์— ์‹คํ–‰๋˜๋ฏ€๋กœ ํŠธ๋žœ์Šค์ฝ”๋”ฉ์ด๋‚˜ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ค‘ ์–ด๋А ์ชฝ๋„ ์„œ๋กœ๋ฅผ ์ง€์—ฐ์‹œํ‚ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

// upload attachments of videos and previews such that every video has a preview 
// that are created concurrently so that neither blocks each other. 

for try await (vid, preview) in zip(videos, previews) {
  try await upload(vid, preview)
}

Zip ์ž์ฒด๋Š” ์–ด๋А ์ชฝ์ด ๋จผ์ € ๊ฐ’์„ ์ƒ์„ฑํ–ˆ๋Š”์ง€์— ๋Œ€ํ•œ ์„ ํ˜ธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋น„๋””์˜ค๊ฐ€ ๋จผ์ € ์ƒ์„ฑ๋  ์ˆ˜๋„ ์žˆ๊ณ  ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ๋จผ์ € ์ƒ์„ฑ๋  ์ˆ˜๋„ ์žˆ์œผ๋ฉฐ, ์–ด๋А ์ชฝ์ด๋“  ์™„์ „ํ•œ ํŠœํ”Œ์„ ๋ณด๋‚ด๊ธฐ ์œ„ํ•ด ๋‹ค๋ฅธ ์ชฝ์„ awaitํ•ฉ๋‹ˆ๋‹ค. Zip์€ ๊ฐ ์ธก๋ฉด์„ ๋™์‹œ์— awaitํ•˜์—ฌ ๊ฐ’์˜ ํŠœํ”Œ์„ ๊ตฌ์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์Œ์„ ํ•จ๊ป˜ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


Merge

๋“ค์–ด์˜ค๋Š” ๋ฉ”์‹œ์ง€๋ฅผ AsyncSequence๋กœ ๋ชจ๋ธ๋งํ•˜๋Š” ๊ฒƒ์ด ํ•ฉ๋ฆฌ์ ์ด๋ผ๊ณ  ๊ฒฐ๋ก ์ง€์—ˆ์Šต๋‹ˆ๋‹ค. AsyncStream์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ๋กœ ํ–ˆ๋Š”๋ฐ, ์ด๋Š” ์ˆœ์„œ๋ฅผ ์œ ์ง€ํ•˜๊ณ  ์ฝœ๋ฐฑ์„ ๋ฉ”์‹œ์ง€์˜ AsyncSequence๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์—ฌ๋Ÿฌ ๊ณ„์ •์„ ์ง€์›ํ•ด์•ผ ํ•˜๋Š” ์š”์ฒญ๋œ ๊ธฐ๋Šฅ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ๊ณ„์ •์€ ๋“ค์–ด์˜ค๋Š” ๋ฉ”์‹œ์ง€์˜ AsyncStream์„ ์ƒ์„ฑํ•˜์ง€๋งŒ, ์ด๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ ๋ชจ๋“  ๊ฒƒ์„ ํ•˜๋‚˜์˜ ๋‹จ์ผ AsyncSequence๋กœ ํ•จ๊ป˜ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด AsyncSequence๋ฅผ ๋ณ‘ํ•ฉํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

Swift Async Algorithms ํŒจํ‚ค์ง€์—๋Š” ์ •ํ™•ํžˆ ์ด๋ฅผ ์œ„ํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด ์žˆ์Šต๋‹ˆ๋‹ค: "Merge"

"It works similarly to Zip in the regards that it concurrently iterates multiple AsyncSequences. But instead of creating paired tuples, it requires the bases to share the same element type and merges the base AsyncSequences into one singular AsyncSequence of those elements." (Merge๋Š” ์—ฌ๋Ÿฌ AsyncSequence๋ฅผ ๋™์‹œ์— ๋ฐ˜๋ณตํ•œ๋‹ค๋Š” ์ ์—์„œ Zip๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์Œ์œผ๋กœ ๋œ ํŠœํ”Œ์„ ๋งŒ๋“œ๋Š” ๋Œ€์‹ , ๊ธฐ๋ฐ˜๋“ค์ด ๋™์ผํ•œ ์š”์†Œ ํƒ€์ž…์„ ๊ณต์œ ํ•ด์•ผ ํ•˜๋ฉฐ ๊ธฐ๋ฐ˜ AsyncSequence๋“ค์„ ํ•ด๋‹น ์š”์†Œ์˜ ๋‹จ์ผ AsyncSequence๋กœ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค.)

// Display previews of messages from either the primary or secondary account

for try await message in merge(primaryAccount.messages, secondaryAccount.messages) {
  displayPreview(message)
}

Merge๋Š” ๋ฐ˜๋ณต๋  ๋•Œ ์–ด๋А ์ชฝ์—์„œ๋“  ๋จผ์ € ์ƒ์„ฑ๋œ ์š”์†Œ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ๋” ์ด์ƒ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ๋Š” ๊ฐ’์ด ์—†์„ ๋•Œ๊นŒ์ง€, ์ฆ‰ ๋ชจ๋“  ๊ธฐ๋ฐ˜ AsyncSequence๊ฐ€ iterator์—์„œ nil์„ ๋ฐ˜ํ™˜ํ•  ๋•Œ๊นŒ์ง€ ๋ฐ˜๋ณต์„ ๊ณ„์†ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ฐ˜ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๋‹ค๋ฅธ ๋ฐ˜๋ณต๋“ค์€ ์ทจ์†Œ๋ฉ๋‹ˆ๋‹ค.


Clock API๋ฅผ ํ™œ์šฉํ•œ ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ์•Œ๊ณ ๋ฆฌ์ฆ˜

์ด๋Ÿฌํ•œ ๊ฒฐํ•ฉ ์•Œ๊ณ ๋ฆฌ์ฆ˜๋“ค์€ ๊ฐ’์ด ์ƒ์„ฑ๋  ๋•Œ ๋™์‹œ์— ์ž‘๋™ํ•˜์ง€๋งŒ, ๋•Œ๋กœ๋Š” ์‹œ๊ฐ„ ์ž์ฒด์™€ ์‹ค์ œ๋กœ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๊ฒƒ์ด ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. Swift Async Algorithms ํŒจํ‚ค์ง€๋Š” Swift์˜ ์ƒˆ๋กœ์šด Clock API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์‹œ๊ฐ„๊ณผ ํ•จ๊ป˜ ์ž‘๋™ํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํŒจ๋ฐ€๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์‹œ๊ฐ„ ์ž์ฒด๋Š” ์ •๋ง ๋ณต์žกํ•œ ์ฃผ์ œ๊ฐ€ ๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ, Swift 5.7์—์„œ ์ด๋ฅผ ์•ˆ์ „ํ•˜๊ณ  ์ผ๊ด€๋˜๊ฒŒ ๋งŒ๋“œ๋Š” API ์„ธํŠธ๊ฐ€ ์ƒˆ๋กœ ๋„์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค: Clock, Instant, Duration.

"The Clock protocol defines two primitives, a way to wake up after a given instant and a way to produce a concept of now." (Clock ํ”„๋กœํ† ์ฝœ์€ ๋‘ ๊ฐ€์ง€ ๊ธฐ๋ณธ ์š”์†Œ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค: ์ฃผ์–ด์ง„ ์‹œ์  ์ดํ›„์— ๊นจ์–ด๋‚˜๋Š” ๋ฐฉ๋ฒ•๊ณผ '์ง€๊ธˆ'์˜ ๊ฐœ๋…์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•.)

๋ช‡ ๊ฐ€์ง€ ๋‚ด์žฅ Clock์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋” ์ผ๋ฐ˜์ ์ธ ๋‘ ๊ฐ€์ง€๋Š” ContinuousClock๊ณผ SuspendingClock์ž…๋‹ˆ๋‹ค.

SuspendingClock

// Sleep until a given deadline

let clock = SuspendingClock()
var deadline = clock.now + .seconds(3)
try await clock.sleep(until: deadline)

์•ฑ์—์„œ ์ƒˆ๋กœ์šด Clock API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ธฐ์กด ์ฝœ๋ฐฑ ์ด๋ฒคํŠธ๋ฅผ clock sleep ํ•จ์ˆ˜๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜์—ฌ ๋งˆ๊ฐ ์‹œ๊ฐ„ ํ›„ ์•Œ๋ฆผ์„ ํ•ด์ œํ•˜๋Š” ๊ฒƒ์„ ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์ง€์—ฐํ•˜๋ ค๋Š” ์ดˆ ์ˆ˜๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ๋‚˜ํƒ€๋‚ด๋Š” duration ๊ฐ’์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

SuspendingClock vs ContinuousClock

ContinuousClock์€ ์Šคํ†ฑ์›Œ์น˜์ฒ˜๋Ÿผ ์‹œ๊ฐ„์„ ์ธก์ •ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ธก์ • ๋Œ€์ƒ์˜ ์ƒํƒœ์™€ ๊ด€๊ณ„์—†์ด ์‹œ๊ฐ„์ด ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด SuspendingClock์€ ์ด๋ฆ„์ด ์•”์‹œํ•˜๋“ฏ์ด ๊ธฐ๊ธฐ๊ฐ€ ์ ˆ์ „ ๋ชจ๋“œ์— ๋“ค์–ด๊ฐ€๋ฉด ์ผ์‹œ ์ค‘์ง€๋ฉ๋‹ˆ๋‹ค.

Clock์—๋Š” ์ž‘์—… ์‹คํ–‰์˜ ๊ฒฝ๊ณผ ์‹œ๊ฐ„์„ ์ธก์ •ํ•˜๋Š” ํŽธ๋ฆฌํ•œ ๋ฉ”์„œ๋“œ๋„ ์žˆ์Šต๋‹ˆ๋‹ค:

let clock = SuspendingClock()
let elapsed = await clock.measure {
  await someLongRunningWork()
}
//Elapsed time reads 00:05.40

let clock = ContinuousClock()
let elapsed = await clock.measure {
  await someLongRunningWork()
}
//Elapsed time reads 00:19.54

์ด ๋‘ Clock์˜ ํ•ต์‹ฌ ์ฐจ์ด์ ์€ ๊ธฐ๊ธฐ๊ฐ€ ์ ˆ์ „ ๋ชจ๋“œ์ผ ๋•Œ์˜ ๋™์ž‘์—์„œ ๋‚˜์˜ต๋‹ˆ๋‹ค. ์˜ค๋ž˜ ์‹คํ–‰๋˜๋Š” ์ž‘์—…์˜ ๊ฒฝ์šฐ, ์ž‘์—…์ด ์ผ์‹œ ์ค‘์ง€๋  ์ˆ˜ ์žˆ์ง€๋งŒ, ์‹คํ–‰์„ ์žฌ๊ฐœํ•˜๋ฉด ContinuousClock์€ ๊ธฐ๊ธฐ๊ฐ€ ์ž ์ž๋Š” ๋™์•ˆ์—๋„ ์ง„ํ–‰๋˜์—ˆ์ง€๋งŒ, SuspendingClock์€ ๊ทธ๋ ‡์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

"If you need to interact with time in relation to the machine, like for animations, use the SuspendingClock. Measuring tasks in relation to the human in front of the device is better suited for the ContinuousClock." (์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ๊ฐ™์ด ๊ธฐ๊ธฐ์™€ ๊ด€๋ จํ•˜์—ฌ ์‹œ๊ฐ„๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ SuspendingClock์„ ์‚ฌ์šฉํ•˜์„ธ์š”. ๊ธฐ๊ธฐ ์•ž์˜ ์‚ฌ๋žŒ๊ณผ ๊ด€๋ จํ•˜์—ฌ ์ž‘์—…์„ ์ธก์ •ํ•˜๋Š” ๊ฒƒ์€ ContinuousClock์ด ๋” ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.)

์ผ๋ฐ˜์ ์œผ๋กœ ์ด ์ฐจ์ด์ ์€ ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ๊ฐ™์€ ๊ฒƒ์ด ์‹คํ–‰ ํƒ€์ด๋ฐ์„ ์ผ์‹œ ์ค‘์ง€ํ•˜์—ฌ ์˜ˆ์ƒ๋Œ€๋กœ ์ž‘๋™ํ•˜๋„๋ก ํ•˜๋Š” ํ•ต์‹ฌ ์„ธ๋ถ€ ์‚ฌํ•ญ์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ ˆ๋Œ€ ์ง€์† ์‹œ๊ฐ„์œผ๋กœ ์ง€์—ฐํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ, ์ฆ‰ ์ธ๊ฐ„์— ์ƒ๋Œ€์ ์ธ ๊ฒƒ์€ ContinuousClock์„ ์‚ฌ์šฉํ•˜์„ธ์š”.


Debounce - ๊ฒ€์ƒ‰ ๋ฉ”์‹œ์ง€ ์ œ์–ด

Swift Async Algorithms ํŒจํ‚ค์ง€๋Š” ์ด๋Ÿฌํ•œ ์ƒˆ๋กœ์šด Clock, Instant, Duration ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ๊ฐ„๊ณผ ๊ด€๋ จํ•˜์—ฌ ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๋˜๋Š” ๋ฐฉ์‹์— ๋Œ€ํ•œ ๋งŽ์€ ๊ฐœ๋…์„ ๋‹ค๋ฃจ๋Š” ์ œ๋„ค๋ฆญ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค.

๋ฉ”์‹œ์ง• ์•ฑ์—์„œ ์ด๋ฒคํŠธ์— ๋Œ€ํ•œ ์ •๋ฐ€ํ•œ ์ œ์–ด๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ฐ ์ •๋ง ์œ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ์ƒํ˜ธ์ž‘์šฉ์„ ์†๋„ ์ œํ•œํ•˜๊ณ  ๋ฉ”์‹œ์ง€๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๋ฒ„ํผ๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ์•„๋งˆ๋„ ์‹œ๊ฐ„์„ ๊ฐ€์žฅ ๋งŽ์ด ํ™œ์šฉํ•œ ์˜์—ญ์€ ๋ฉ”์‹œ์ง€ ๊ฒ€์ƒ‰์ด์—ˆ์Šต๋‹ˆ๋‹ค.

// Control searching messages

class SearchController {
  let searchResults = AsyncChannel()

  func search(_ searchValues: SearchValues) 
    where SearchValues.Element == String 
}

๊ฒฐ๊ณผ ์ฑ„๋„์„ ๊ด€๋ฆฌํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์ฑ„๋„์€ ๊ฒ€์ƒ‰ ์ž‘์—…์—์„œ UI๋กœ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์ž‘์—… ์ž์ฒด๋Š” ์‹œ๊ฐ„๊ณผ ๊ด€๋ จํ•˜์—ฌ ๋ช‡ ๊ฐ€์ง€ ํŠน์ • ํŠน์„ฑ์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ ์ „์†ก๋œ ๋ฉ”์‹œ์ง€ ๊ฒ€์ƒ‰์„ ์†๋„ ์ œํ•œํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค.

Debounce ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ๋ฐ˜๋ณต๋  ๋•Œ ๋‹ค์Œ ๊ฐ’์„ ๋ฐฉ์ถœํ•˜๊ธฐ ์ „์— ์ •์  ๊ธฐ๊ฐ„(quiescence period)์„ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ๊ฐ€ ๋น ๋ฅด๊ฒŒ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ฐ’์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์ „์— ์กฐ์šฉํ•œ ๊ธฐ๊ฐ„์„ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

"When user input from a search field is changed rapidly, we don't want the search controller to fire off a search request for each change. Instead, we want to make sure to wait for a quiet period when we're certain typing was likely to be done." (๊ฒ€์ƒ‰ ํ•„๋“œ์˜ ์‚ฌ์šฉ์ž ์ž…๋ ฅ์ด ๋น ๋ฅด๊ฒŒ ๋ณ€๊ฒฝ๋  ๋•Œ, ๊ฐ ๋ณ€๊ฒฝ๋งˆ๋‹ค ๊ฒ€์ƒ‰ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ๊ฒ€์ƒ‰ ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๊ฒƒ์„ ์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹ , ํƒ€์ดํ•‘์ด ์™„๋ฃŒ๋˜์—ˆ์„ ๊ฒƒ์ด ํ™•์‹คํ•œ ์กฐ์šฉํ•œ ๊ธฐ๊ฐ„์„ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.)

let queries = searchValues
   .debounce(for: .milliseconds(300))

for await query in queries {
  let results = try await performSearch(query)
  await channel.send(results)
}

๊ธฐ๋ณธ์ ์œผ๋กœ Debounce ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ContinuousClock์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ, ์•„๋ฌด๊ฒƒ๋„ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š” ๋™์•ˆ ์ง€์ •๋œ ๊ธฐ๊ฐ„์„ ๊ธฐ๋‹ค๋ฆฌ๋„๋ก ์ž…๋ ฅ์„ debounceํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


Chunked - ๋ฉ”์‹œ์ง€ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ

Clock๊ณผ Duration์€ debounce๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋‹ค๋ฅธ ์•Œ๊ณ ๋ฆฌ์ฆ˜์—๋„ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ •๋ง ์œ ์šฉํ•œ ์˜์—ญ ์ค‘ ํ•˜๋‚˜๋Š” ์„œ๋ฒ„๋กœ ๋ฉ”์‹œ์ง€ ๋ฐฐ์น˜๋ฅผ ์ „์†กํ•˜๋Š” ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

Swift Algorithms ํŒจํ‚ค์ง€์—๋Š” ๊ฐ’์„ ์ฒญํฌ๋กœ ๋‚˜๋ˆ„๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์„ธํŠธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. Swift Async Algorithms ํŒจํ‚ค์ง€๋Š” ์ด๋Ÿฌํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์ œ๊ณตํ•˜๊ณ , Clock ๋ฐ Duration๊ณผ ์ƒํ˜ธ ์šด์šฉํ•˜๋Š” ๋ฒ„์ „ ์„ธํŠธ๋„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

"The family of chunking algorithms allow for control over chunks by count, by time, or by content. If an error occurs in any of these, that error is rethrown, so our code is safe when it comes to failures." (์ฒญํ‚น ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํŒจ๋ฐ€๋ฆฌ๋Š” ๊ฐœ์ˆ˜, ์‹œ๊ฐ„ ๋˜๋Š” ๋‚ด์šฉ๋ณ„๋กœ ์ฒญํฌ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ค‘ ์–ด๋А ๊ณณ์—์„œ๋“  ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ํ•ด๋‹น ์˜ค๋ฅ˜๊ฐ€ rethrow๋˜๋ฏ€๋กœ ์‹คํŒจ์— ๋Œ€ํ•ด ์ฝ”๋“œ๊ฐ€ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.)

let batches = outboundMessages.chunked(
  by: .repeating(every: .milliseconds(500))
)

let encoder = JSONEncoder() 
for await batch in batches {
  let data = try encoder.encode(batch)
  try await postToServer(data) 
}

chunked(by:) API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€ ์ฒญํฌ๊ฐ€ ํŠน์ • ๊ฒฝ๊ณผ ๊ธฐ๊ฐ„์— ๋”ฐ๋ผ ์ง๋ ฌํ™”๋˜์–ด ์ „์†ก๋˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ํšจ์œจ์ ์ธ ํŒจํ‚ท์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ด API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ 500๋ฐ€๋ฆฌ์ดˆ๋งˆ๋‹ค ๋ฉ”์‹œ์ง€ ๋ฐฐ์น˜๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋ˆ„๊ตฐ๊ฐ€๊ฐ€ ์ •๋ง ํฅ๋ถ„ํ•ด์„œ ์ •๋ง ๋น ๋ฅด๊ฒŒ ํƒ€์ดํ•‘ํ•˜๋ฉด ์„œ๋ฒ„๋กœ ์ „์†ก๋˜๋Š” ์š”์ฒญ์ด ๊ทธ๋ฃนํ™”๋ฉ๋‹ˆ๋‹ค.


Collection ์ดˆ๊ธฐํ™” - AsyncSequence์—์„œ ์ปฌ๋ ‰์…˜์œผ๋กœ

์ปฌ๋ ‰์…˜๊ณผ ์‹œํ€€์Šค๋ฅผ ๋‹ค๋ฃฐ ๋•Œ, ์š”์†Œ๋ฅผ ์ง€์—ฐ ์ฒ˜๋ฆฌ(lazily process)ํ•˜๋Š” ๊ฒƒ์ด ์œ ์šฉํ•˜๊ณ  ์„ฑ๋Šฅ์ด ์ข‹์€ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. AsyncSequence๋Š” Swift ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ lazy ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด ์ž‘๋™ํ•˜๋Š” ๋ฐฉ์‹๊ณผ ๋งค์šฐ ์œ ์‚ฌํ•˜๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ๋Ÿฌํ•œ lazy ์•Œ๊ณ ๋ฆฌ์ฆ˜์ฒ˜๋Ÿผ ์ปฌ๋ ‰์…˜ ์„ธ๊ณ„๋กœ ๋‹ค์‹œ ์ด๋™ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ข…์ข… ์žˆ์Šต๋‹ˆ๋‹ค.

"The Swift Async Algorithms package offers a set of initializers for constructing collections using AsyncSequence. These let you build up dictionaries, sets, or arrays with input AsyncSequences that are known to be finite." (Swift Async Algorithms ํŒจํ‚ค์ง€๋Š” AsyncSequence๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ปฌ๋ ‰์…˜์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์ดˆ๊ธฐํ™”๊ธฐ ์„ธํŠธ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์œ ํ•œํ•œ ๊ฒƒ์œผ๋กœ ์•Œ๋ ค์ง„ ์ž…๋ ฅ AsyncSequence๋กœ ๋”•์…”๋„ˆ๋ฆฌ, ์…‹ ๋˜๋Š” ๋ฐฐ์—ด์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.)

// Create a message with awaiting attachments to be encoded
init(_ attachments: Attachments) async rethrows {
  self.attachments = try await Array(attachments)
}

์ปฌ๋ ‰์…˜ ์ดˆ๊ธฐํ™”๊ธฐ๋ฅผ ํ†ตํ•ด ๋ฉ”์‹œ์ง€ ์ดˆ๊ธฐํ™”์— ๋ณ€ํ™˜์„ ๋ฐ”๋กœ ๊ตฌ์ถ•ํ•˜๊ณ  ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ Array๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. Swift ๋™์‹œ์„ฑ์„ ์‚ฌ์šฉํ•˜๋„๋ก ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋Š” ๋งŽ์€ ๊ธฐ๋Šฅ์ด ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ •๋ง ์œ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•จ์œผ๋กœ์จ ์•ฑ์˜ ์ผ๋ถ€๋ฅผ ์ ์ง„์ ์œผ๋กœ, ํ•ฉ๋ฆฌ์ ์ธ ๊ณณ์—์„œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


Swift Async Algorithms ํŒจํ‚ค์ง€ ์š”์•ฝ

์ง€๊ธˆ๊นŒ์ง€ Swift Async Algorithms ํŒจํ‚ค์ง€์˜ ๋ช‡ ๊ฐ€์ง€ ํ•˜์ด๋ผ์ดํŠธ๋งŒ ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์˜ค๋Š˜ ๋‹ค๋ฃฌ ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ๋” ๋งŽ์€ ๊ฒƒ์ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์—ฌ๋Ÿฌ AsyncSequence ๊ฒฐํ•ฉ (Zip, Merge)
  • ์‹œ๊ฐ„์— ์˜ํ•œ ์†๋„ ์ œํ•œ (Debounce)
  • ์ฒญํฌ๋กœ ๋‚˜๋ˆ„๊ธฐ (Chunked)

ํ•˜์ง€๋งŒ ์ด๊ฒƒ๋“ค์€ ์•ฑ์—์„œ ๊ด‘๋ฒ”์œ„ํ•˜๊ฒŒ ์‚ฌ์šฉํ•œ ํ•˜์ด๋ผ์ดํŠธ์ผ ๋ฟ์ž…๋‹ˆ๋‹ค. ์ด ํŒจํ‚ค์ง€์—๋Š” ํ›จ์”ฌ ๋” ๋งŽ์€ ๊ฒƒ์ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋ฒ„ํผ๋ง
  • ๋ฆฌ๋“€์‹ฑ
  • ์กฐ์ธ
  • ๊ฐ„ํ—์ ์œผ๋กœ ๊ฐ’ ์ฃผ์ž…
  • ๊ทธ๋ฆฌ๊ณ  ๋” ๋งŽ์€ ๊ฒƒ๋“ค

Swift Async Algorithms ํŒจํ‚ค์ง€๋Š” ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ๊ฒƒ๋“ค์„ ๋‹ค๋ฃจ๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์„ธํŠธ๋ฅผ ๊ฐ€์ ธ์™€ ์•ฑ์—์„œ ๋„์›€์ด ๋  ์ˆ˜ ์žˆ๋Š” ๊ด‘๋ฒ”์œ„ํ•œ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์œผ๋กœ ํ™•์žฅํ•ฉ๋‹ˆ๋‹ค.

์ด ํŒจํ‚ค์ง€๋Š” ์˜คํ”ˆ ์†Œ์Šค๋กœ ๊ฐœ๋ฐœ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ’ก ํ•ต์‹ฌ ์š”์•ฝ

Q: Zip๊ณผ Merge์˜ ์ฐจ์ด์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?

๊ตฌ๋ถ„ Zip Merge
์ถœ๋ ฅ ํŠœํ”Œ (A, B) ๋‹จ์ผ ์š”์†Œ
์š”์†Œ ํƒ€์ž… ๋‹ค๋ฅธ ํƒ€์ž… ๊ฐ€๋Šฅ ๋™์ผ ํƒ€์ž… ํ•„์ˆ˜
์™„๋ฃŒ ์กฐ๊ฑด ์–ด๋А ํ•œ์ชฝ์ด ๋๋‚˜๋ฉด ์™„๋ฃŒ ๋ชจ๋“  ์ชฝ์ด ๋๋‚˜์•ผ ์™„๋ฃŒ
์‚ฌ์šฉ ์‚ฌ๋ก€ ์ง์ง€์–ด์•ผ ํ•˜๋Š” ๋ฐ์ดํ„ฐ ์—ฌ๋Ÿฌ ์†Œ์Šค ํ†ตํ•ฉ

ํ•ต์‹ฌ ์š”์•ฝ:

  • ๊ฐœ๋… ์š”์•ฝ: Zip์€ ์—ฌ๋Ÿฌ AsyncSequence์—์„œ ๊ฐ’์„ ๋™์‹œ์— ๊ฐ€์ ธ์™€ ํŠœํ”Œ๋กœ ์ง์ง“๊ณ , Merge๋Š” ์—ฌ๋Ÿฌ AsyncSequence๋ฅผ ํ•˜๋‚˜์˜ ์ŠคํŠธ๋ฆผ์œผ๋กœ ํ•ฉ์นฉ๋‹ˆ๋‹ค
  • ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ: concurrent iteration / tuple / same element type / rethrows
  • ์˜ˆ์‹œ ์ฝ”๋“œ:
// Zip - ๋น„๋””์˜ค์™€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์ง์ง€์–ด ์—…๋กœ๋“œ
for try await (vid, preview) in zip(videos, previews) {
  try await upload(vid, preview)
}

// Merge - ์—ฌ๋Ÿฌ ๊ณ„์ •์˜ ๋ฉ”์‹œ์ง€๋ฅผ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉ
for try await message in merge(account1.messages, account2.messages) {
  displayPreview(message)
}
  • ๋ฉด์ ‘ ํฌ์ธํŠธ: Zip์€ "๋ชจ๋“  ์†Œ์Šค์—์„œ ๊ฐ’์ด ์ค€๋น„๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ"ํ•˜๊ณ , Merge๋Š” "๋จผ์ € ์ค€๋น„๋œ ๊ฐ’์„ ์ฆ‰์‹œ ์ „๋‹ฌ"ํ•œ๋‹ค๋Š” ์ฐจ์ด๋ฅผ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ

Q: SuspendingClock๊ณผ ContinuousClock์˜ ์ฐจ์ด์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?

ํ•ต์‹ฌ ์š”์•ฝ:

  • ๊ฐœ๋… ์š”์•ฝ: SuspendingClock์€ ๊ธฐ๊ธฐ ์ ˆ์ „ ์‹œ ์ผ์‹œ ์ค‘์ง€๋˜๊ณ , ContinuousClock์€ ์ ˆ์ „๊ณผ ๊ด€๊ณ„์—†์ด ๊ณ„์† ์ง„ํ–‰
  • ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ: machine sleep / animation / absolute duration / human-relative time
  • ์˜ˆ์‹œ ์ฝ”๋“œ:
// ์• ๋‹ˆ๋ฉ”์ด์…˜์šฉ - ๊ธฐ๊ธฐ ์ ˆ์ „ ์‹œ ์ผ์‹œ ์ค‘์ง€
let suspending = SuspendingClock()
try await suspending.sleep(until: suspending.now + .seconds(3))

// ์‚ฌ์šฉ์ž ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์ธก์ •์šฉ - ์ ˆ๋Œ€ ์‹œ๊ฐ„
let continuous = ContinuousClock()
let elapsed = await continuous.measure { await longTask() }
  • ๋ฉด์ ‘ ํฌ์ธํŠธ: ์• ๋‹ˆ๋ฉ”์ด์…˜์—๋Š” SuspendingClock, ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ธก์ •์—๋Š” ContinuousClock ์‚ฌ์šฉ ์ด์œ ๋ฅผ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ

Q: Debounce์™€ Throttle์˜ ์ฐจ์ด์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?

ํ•ต์‹ฌ ์š”์•ฝ:

  • ๊ฐœ๋… ์š”์•ฝ: Debounce๋Š” "์กฐ์šฉํ•œ ๊ธฐ๊ฐ„" ํ›„์— ๊ฐ’์„ ๋ฐฉ์ถœํ•˜๊ณ , Throttle์€ ์ผ์ • ๊ฐ„๊ฒฉ์œผ๋กœ ์ตœ์‹  ๊ฐ’์„ ๋ฐฉ์ถœ
  • ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ: quiescence period / rate limiting / search input / ContinuousClock
  • ์˜ˆ์‹œ ์ฝ”๋“œ:
// 300ms ๋™์•ˆ ์ž…๋ ฅ์ด ์—†์œผ๋ฉด ๊ฒ€์ƒ‰ ์‹คํ–‰
let queries = searchValues.debounce(for: .milliseconds(300))
for await query in queries {
  let results = try await performSearch(query)
}
  • ๋ฉด์ ‘ ํฌ์ธํŠธ: ๊ฒ€์ƒ‰ ์ž…๋ ฅ์— Debounce๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ (ํƒ€์ดํ•‘ ์™„๋ฃŒ ๋Œ€๊ธฐ)์™€ ์Šคํฌ๋กค ์ด๋ฒคํŠธ์— Throttle์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ (์ผ์ • ๊ฐ„๊ฒฉ ์ฒ˜๋ฆฌ)๋ฅผ ๊ตฌ๋ถ„ํ•ด์„œ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ

Q: Chunked ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ์–ธ์ œ ์‚ฌ์šฉํ•˜๋‚˜์š”?

ํ•ต์‹ฌ ์š”์•ฝ:

  • ๊ฐœ๋… ์š”์•ฝ: ์—ฐ์†์ ์ธ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์„ ๋ฐฐ์น˜๋กœ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌ
  • ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ: batching / time-based chunking / count-based chunking / efficient network requests
  • ์˜ˆ์‹œ ์ฝ”๋“œ:
// 500ms๋งˆ๋‹ค ๋ฉ”์‹œ์ง€ ๋ฐฐ์น˜ ์ „์†ก
let batches = outboundMessages.chunked(
  by: .repeating(every: .milliseconds(500))
)
for await batch in batches {
  let data = try encoder.encode(batch)
  try await postToServer(data)
}
  • ๋ฉด์ ‘ ํฌ์ธํŠธ: ๊ฐœ๋ณ„ ์š”์ฒญ ๋Œ€์‹  ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋กœ ๋„คํŠธ์›Œํฌ ํšจ์œจ์„ฑ์„ ๋†’์ด๋Š” ๋ฐฉ๋ฒ•๊ณผ, ์‹œ๊ฐ„/๊ฐœ์ˆ˜/๋‚ด์šฉ ๊ธฐ์ค€ ์ฒญํ‚น์˜ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ

Q: AsyncSequence๋ฅผ Collection์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์€?

ํ•ต์‹ฌ ์š”์•ฝ:

  • ๊ฐœ๋… ์š”์•ฝ: Swift Async Algorithms ํŒจํ‚ค์ง€์˜ Collection ์ดˆ๊ธฐํ™”๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์œ ํ•œํ•œ AsyncSequence๋ฅผ Array, Set, Dictionary๋กœ ๋ณ€ํ™˜
  • ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ: finite AsyncSequence / incremental migration / lazy to eager / rethrows
  • ์˜ˆ์‹œ ์ฝ”๋“œ:
init(_ attachments: Attachments) async rethrows {
  self.attachments = try await Array(attachments)
}
  • ๋ฉด์ ‘ ํฌ์ธํŠธ: AsyncSequence๊ฐ€ ์œ ํ•œํ•ด์•ผ ํ•˜๋Š” ์ด์œ (๋ฌดํ•œ ์‹œํ€€์Šค๋Š” ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ ๋ถˆ๊ฐ€)์™€ ์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต์—์„œ์˜ ํ™œ์šฉ์„ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ

๐Ÿ“ ์ƒˆ๋กญ๊ฒŒ ์•Œ๊ฒŒ ๋œ ์ •๋ณด

1. AsyncSequence โ†’ Collection ๋ณ€ํ™˜ ์‹œ "์œ ํ•œ์„ฑ" ํ•„์ˆ˜

Array(asyncSequence)๋Š” ๋ชจ๋“  ์š”์†Œ๊ฐ€ ๋๋‚  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ค์•ผ ๋ฐฐ์—ด์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// Array ์ดˆ๊ธฐํ™”๊ธฐ์˜ ๋‚ด๋ถ€ ๋™์ž‘ (๊ฐœ๋…์ )
extension Array {
    init(_ sequence: S) async rethrows where S.Element == Element {
        var result: [Element] = []
        for try await element in sequence {  // โ† ๋ชจ๋“  ์š”์†Œ๋ฅผ ํ•˜๋‚˜์”ฉ ๊ธฐ๋‹ค๋ฆผ
            result.append(element)
        }
        self = result  // โ† ๋ชจ๋“  ์š”์†Œ๊ฐ€ ๋๋‚˜์•ผ ์—ฌ๊ธฐ์— ๋„๋‹ฌ
    }
}

๋ฌดํ•œ ์‹œํ€€์Šค์˜ ๋ฌธ์ œ:

// โŒ ๋ฌดํ•œ AsyncSequence ์˜ˆ์‹œ
let infiniteStream = AsyncStream { continuation in
    var n = 0
    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
        continuation.yield(n)  // 1์ดˆ๋งˆ๋‹ค ๊ณ„์† ๊ฐ’ ์ƒ์„ฑ
        n += 1
    }
    // continuation.finish()๊ฐ€ ์ ˆ๋Œ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์Œ!
}

// ์ด ์ฝ”๋“œ๋Š” ์˜์›ํžˆ ์™„๋ฃŒ๋˜์ง€ ์•Š์Œ
let array = try await Array(infiniteStream)  // ๐Ÿ”ด ๋ฌดํ•œ ๋Œ€๊ธฐ + ๋ฉ”๋ชจ๋ฆฌ ๋ฌดํ•œ ์ฆ๊ฐ€
print(array)  // ์ ˆ๋Œ€ ์‹คํ–‰ ์•ˆ๋จ

์œ ํ•œ ์‹œํ€€์Šค์˜ ์ •์ƒ ๋™์ž‘:

// โœ… ์œ ํ•œ AsyncSequence ์˜ˆ์‹œ
let finiteStream = AsyncStream { continuation in
    for i in 0..<5 {
        continuation.yield(i)
    }
    continuation.finish()  // ์ข…๋ฃŒ ์‹ ํ˜ธ!
}

let array = try await Array(finiteStream)  // โœ… ์ •์ƒ ์™„๋ฃŒ
print(array)  // [0, 1, 2, 3, 4]

ํ•ต์‹ฌ: continuation.finish() ํ˜ธ์ถœ๋กœ ์ข…๋ฃŒ๋ฅผ ๋ช…์‹œํ•˜๋Š” ์œ ํ•œํ•œ AsyncSequence๋งŒ Collection์œผ๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋ฌดํ•œ AsyncSequence๋Š” ์ ˆ๋Œ€ ๋๋‚˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋ณ€ํ™˜์ด ์™„๋ฃŒ๋˜์ง€ ์•Š๊ณ , ๋ฉ”๋ชจ๋ฆฌ๋„ ๋ฌดํ•œํžˆ ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.


2. Collection ์ดˆ๊ธฐํ™”๊ธฐ๋ฅผ ํ†ตํ•œ ์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

Swift Async Algorithms ํŒจํ‚ค์ง€๋Š” Array, Set, Dictionary ์ดˆ๊ธฐํ™”๊ธฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ๋ถ€๋ถ„๋งŒ AsyncSequence๋กœ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์˜ˆ์‹œ:

// ๊ธฐ์กด ์ฝ”๋“œ - ๋™๊ธฐ ๋ฐฉ์‹
class MessageStore {
    var messages: [Message] = []
    
    func loadMessages() {
        // ๋™๊ธฐ์ ์œผ๋กœ ๋ฉ”์‹œ์ง€ ๋กœ๋“œ
    }
}

// ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ›„ - ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ์†Œ์Šค, ๋™๊ธฐ ์ €์žฅ
class MessageStore {
    var messages: [Message] = []  // โ† ๊ธฐ์กด Array ํƒ€์ž… ์œ ์ง€!
    
    func loadMessages() async throws {
        // ๋น„๋™๊ธฐ ์ŠคํŠธ๋ฆผ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ Array๋กœ ๋ณ€ํ™˜
        let messageStream = fetchMessagesFromServer()  // AsyncSequence ๋ฐ˜ํ™˜
        self.messages = try await Array(messageStream)  // โ† Array๋กœ ๋ณ€ํ™˜
    }
}

๋‹ค์–‘ํ•œ Collection ๋ณ€ํ™˜:

// Set ๋ณ€ํ™˜ (์ค‘๋ณต ์ œ๊ฑฐ)
func collectUniqueTags(_ tagStream: S) async throws -> Set
    where S.Element == String {
    return try await Set(tagStream)
}

// Dictionary ๋ณ€ํ™˜
func buildUserCache(_ userStream: S) async throws -> [UserID: User]
    where S.Element == User {
    return try await Dictionary(
        uniqueKeysWithValues: userStream.map { ($0.id, $0) }
    )
}

ํ•ต์‹ฌ: UI ๋ ˆ์ด์–ด๋‚˜ ๋‹ค๋ฅธ ๋ถ€๋ถ„์€ ๋ณ€๊ฒฝ ์—†์ด ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ๋ถ€๋ถ„๋งŒ ๋น„๋™๊ธฐ๋กœ ์ „ํ™˜ํ•˜์—ฌ ์•ฑ ์ „์ฒด๋ฅผ ํ•œ ๋ฒˆ์— ๋ฐ”๊พธ์ง€ ์•Š๊ณ  ์ ์ง„์ ์œผ๋กœ Swift Concurrency๋ฅผ ๋„์ž…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


3. Chunked ์•Œ๊ณ ๋ฆฌ์ฆ˜์˜ ์„ธ ๊ฐ€์ง€ ๊ทธ๋ฃนํ™” ๋ฐฉ์‹

Chunked๋Š” ์—ฐ์†์ ์ธ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์„ **๋ฌถ์Œ(batch)**์œผ๋กœ ๊ทธ๋ฃนํ™”ํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์ž…๋‹ˆ๋‹ค. ์„ธ ๊ฐ€์ง€ ์ฒญํ‚น ๋ฐฉ์‹์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค:

๋ฐฉ์‹ API ์„ค๋ช…
์‹œ๊ฐ„ ๊ธฐ์ค€ chunked(by: .repeating(every:)) ์ผ์ • ์‹œ๊ฐ„๋งˆ๋‹ค ๋ฌถ์Œ ์ƒ์„ฑ
๊ฐœ์ˆ˜ ๊ธฐ์ค€ chunked(by: .count(_:)) ํŠน์ • ๊ฐœ์ˆ˜๋งŒํผ ๋ฌถ๊ธฐ
๋‚ด์šฉ ๊ธฐ์ค€ chunked(on:) ์กฐ๊ฑด ๋ณ€๊ฒฝ ์‹œ ๋ฌถ์Œ ๋ถ„๋ฆฌ

์‹œ๊ฐ„ ๊ธฐ์ค€ ์ฒญํ‚น ์˜ˆ์‹œ:

// 500ms๋งˆ๋‹ค ๋ฉ”์‹œ์ง€๋ฅผ ๋ชจ์•„์„œ ์„œ๋ฒ„๋กœ ์ „์†ก
let batches = outboundMessages.chunked(
    by: .repeating(every: .milliseconds(500))
)

for await batch in batches {
    // batch: [Message] - 500ms ๋™์•ˆ ๋ชจ์ธ ๋ฉ”์‹œ์ง€๋“ค
    let data = try encoder.encode(batch)
    try await postToServer(data)
}
์‹œ๊ฐ„ ํ๋ฆ„ โ†’
|------ 500ms ------|------ 500ms ------|
   msg1, msg2, msg3      msg4, msg5
        โ†“                    โ†“
   [msg1, msg2, msg3]   [msg4, msg5]

์™œ Chunking์ด ํ•„์š”ํ•œ๊ฐ€?

// โŒ ๋น„ํšจ์œจ์ : ๋ฉ”์‹œ์ง€๋งˆ๋‹ค ๋„คํŠธ์›Œํฌ ์š”์ฒญ
for await message in messages {
    try await postToServer(message)  // 100๊ฐœ ๋ฉ”์‹œ์ง€ = 100๋ฒˆ ์š”์ฒญ
}

// โœ… ํšจ์œจ์ : ๋ฐฐ์น˜๋กœ ๋ฌถ์–ด์„œ ์š”์ฒญ
for await batch in messages.chunked(by: .repeating(every: .milliseconds(500))) {
    try await postToServer(batch)  // 100๊ฐœ ๋ฉ”์‹œ์ง€ = ์ˆ˜ ๋ฒˆ์˜ ์š”์ฒญ
}

ํ•ต์‹ฌ: ๋„คํŠธ์›Œํฌ ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ ์ค„์ด๊ฑฐ๋‚˜ ํšจ์œจ์ ์ธ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์— ํ™œ์šฉ๋˜๋ฉฐ, ์‹œ๊ฐ„/๊ฐœ์ˆ˜/๋‚ด์šฉ ๊ธฐ์ค€ ์„ธ ๊ฐ€์ง€ ๋ฐฉ์‹ ์ค‘ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


์ฐธ๊ณ  ์ž๋ฃŒ

Clone this wiki locally