Skip to content

Instantly share code, notes, and snippets.

@pilgwon
Last active March 28, 2024 01:53
Show Gist options
  • Star 169 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save pilgwon/ea05e2207ab68bdd1f49dff97b293b17 to your computer and use it in GitHub Desktop.
Save pilgwon/ea05e2207ab68bdd1f49dff97b293b17 to your computer and use it in GitHub Desktop.
TCA README in Korean

The Composable Architecture

The Composable Architecture(TCA)๋Š” ์ผ๊ด€๋˜๊ณ  ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์‹์œผ๋กœ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ํƒ„์ƒํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ํ•ฉ์„ฑ(Composition), ํ…Œ์ŠคํŒ…(Testing) ๊ทธ๋ฆฌ๊ณ  ์ธ์ฒด ๊ณตํ•™(Ergonomics)์„ ์—ผ๋‘์— ๋‘” TCA๋Š” SwiftUI, UIKit์„ ์ง€์›ํ•˜๋ฉฐ ๋ชจ๋“  ์• ํ”Œ ํ”Œ๋žซํผ(iOS, macOS, tvOS, watchOS)์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

The Composable Architecture๋ž€

์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋‹ค์–‘ํ•œ ๋ชฉ์ ๊ณผ ๋ณต์žก๋„๋ฅผ ๊ฐ€์ง„ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ํ•ต์‹ฌ ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋„๊ตฌ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ํฅ๋ฏธ๋กœ์šด ์Šคํ† ๋ฆฌ๋Š” ์šฐ๋ฆฌ๊ฐ€ ๋งค์ผ ๋งŒ๋‚˜๋Š” ์ˆ˜๋งŽ์€ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค์ค„ ๊ฒ๋‹ˆ๋‹ค.

  • ์ƒํƒœ(State) ๊ด€๋ฆฌ
    ๊ฐ„๋‹จํ•œ ๊ฐ’ ํƒ€์ž…๋“ค๋กœ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•, ์ƒํƒœ๋ฅผ ๊ณต์œ ๋ฅผ ํ†ตํ•ด ํ™”๋ฉด์—์„œ ์ผ์–ด๋‚˜๋Š” ๋ณ€ํ™”(Mutation)๋ฅผ ๋‹ค๋ฅธ ํ™”๋ฉด์—์„œ ์ฆ‰์‹œ ๊ด€์ธก(Observe)ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  • ํ•ฉ์„ฑ(Composition)
    ๊ธฐ๋Šฅ์„ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋…๋ฆฝ๋œ ๋ชจ๋“ˆ๋กœ ์ถ”์ถœํ•˜๋Š” ๋ฐฉ๋ฒ•, ์ด ๋ชจ๋“ˆ์„ ๋‹ค์‹œ ํ•ฉ์ณ์„œ ๊ฑฐ๋Œ€ํ•œ ๊ธฐ๋Šฅ์„ ์ž‘์€ ์ปดํฌ๋„ŒํŠธ์˜ ์ง‘ํ•ฉ์œผ๋กœ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  • ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ(Side Effects)
    ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐ”๊นฅ์„ธ์ƒ๊ณผ ์ ‘์ด‰ํ•˜๋Š” ์ž‘์—…์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ณ  ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  • ํ…Œ์ŠคํŒ…(Testing)
    ์•„ํ‚คํ…์ฒ˜ ๋‚ด๋ถ€์˜ ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์—ฌ๋Ÿฌ ํŒŒํŠธ๋กœ ๊ตฌ์„ฑ๋œ ๊ธฐ๋Šฅ์˜ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•, ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋ผ์น˜๋Š” ์˜ํ–ฅ์— ๋Œ€ํ•ด ์ „์ฒด ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ํ…Œ์ŠคํŠธ ๋ฐฉ์‹์€ ์—ฌ๋Ÿฌ๋ถ„์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์˜ˆ์ƒ๋Œ€๋กœ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ๊ฐ•ํ•œ ๋ณด์ฆ๋„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  • ์ธ์ฒด ๊ณตํ•™(Ergonomics)
    ์œ„์˜ ๋‚ด์šฉ์„ ๊ฐ€๋Šฅํ•œ ํ•œ ์ ์€ ๊ฐœ๋…์˜ ๊ฐ„๋‹จํ•œ API๋กœ ์ด๋ฃจ๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๊ฐ•์˜ ์˜์ƒ

The Composable Architecture๋Š” Point-Free์˜ Brandon Williams์™€ Stephen Celis๊ฐ€ ๋งŒ๋“  Swift์˜ ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๋Š” ์—ํ”ผ์†Œ๋“œ๋ฅผ ํ†ตํ•ด ํƒ„์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์ „์ฒด ์—ํ”ผ์†Œ๋“œ๋Š” ์—ฌ๊ธฐ์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด์™ธ์—๋„, ์ด ๋„ค ํŒŒํŠธ๋กœ ๊ตฌ์„ฑ๋œ ์˜์ƒ๋„ ์žˆ์Šต๋‹ˆ๋‹ค. (part 1, part 2, part 3 ๊ทธ๋ฆฌ๊ณ  part 4)

video poster image

์˜ˆ์ œ ์ฝ”๋“œ

Screen shots of example applications

์ด ์ €์žฅ์†Œ์—” ๊ธฐ๋ณธ์ ์ธ ๋ฌธ์ œ๋ถ€ํ„ฐ ๋ณต์žกํ•œ ๋ฌธ์ œ๊นŒ์ง€ TCA๋ฅผ ํ†ตํ•ด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์ฆ๋ช…ํ•˜๊ธฐ ์œ„ํ•œ ๋งŽ์€ ์˜ˆ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋‚ด์šฉ์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•

TCA๋ฅผ ํ†ตํ•ด ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„  ์—ฌ๋Ÿฌ๋ถ„์˜ ๋„๋ฉ”์ธ์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ช‡ ๊ฐ€์ง€ ํƒ€์ž…์„ ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  • ์ƒํƒœ(State): ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๊ฑฐ๋‚˜ UI๋ฅผ ๊ทธ๋ฆด ๋•Œ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์„ค๋ช…์„ ๋‚˜ํƒ€๋‚ด๋Š” ํƒ€์ž…์ž…๋‹ˆ๋‹ค.
  • ํ–‰๋™(Action): ์‚ฌ์šฉ์ž๊ฐ€ ํ•˜๋Š” ํ–‰๋™์ด๋‚˜ ๋…ธํ‹ฐํ”ผ์ผ€์ด์…˜ ๋“ฑ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ƒ๊ธธ ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ํ–‰๋™์„ ๋‚˜ํƒ€๋‚ด๋Š” ํƒ€์ž…์ž…๋‹ˆ๋‹ค.
  • ํ™˜๊ฒฝ(Environment): API ํด๋ผ์ด์–ธํŠธ๋‚˜ ์• ๋„๋ฆฌํ‹ฑ์Šค ํด๋ผ์ด์–ธํŠธ์™€ ๊ฐ™์ด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ํ•„์š”๋กœ ํ•˜๋Š” ์˜์กด์„ฑ(Dependency)์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ํƒ€์ž…์ž…๋‹ˆ๋‹ค.
  • ๋ฆฌ๋“€์„œ(Reducer): ์–ด๋–ค ํ–‰๋™(Action)์ด ์ฃผ์–ด์กŒ์„ ๋•Œ ์ง€๊ธˆ ์ƒํƒœ(State)๋ฅผ ๋‹ค์Œ ์ƒํƒœ๋กœ ๋ณ€ํ™”์‹œํ‚ค๋Š” ๋ฐฉ๋ฒ•์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ๋˜ํ•œ ๋ฆฌ๋“€์„œ๋Š” ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ์ดํŽ™ํŠธ(Effect, ์˜ˆ์‹œ: API ๋ฆฌํ€˜์ŠคํŠธ)๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜๋ฉฐ, ๋ณดํ†ต์€ Effect ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ์Šคํ† ์–ด(Store): ์‹ค์ œ๋กœ ๊ธฐ๋Šฅ์ด ์ž‘๋™ํ•˜๋Š” ๊ณต๊ฐ„์ž…๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์‚ฌ์šฉ์ž ํ–‰๋™(Action)์„ ๋ณด๋‚ด์„œ ์Šคํ† ์–ด(Store)๋Š” ๋ฆฌ๋“€์„œ(Reducer)์™€ ์ดํŽ™ํŠธ(Effect)๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ณ , ์Šคํ† ์–ด(Store)์—์„œ ์ผ์–ด๋‚˜๋Š” ์ƒํƒœ(State) ๋ณ€ํ™”๋ฅผ ๊ด€์ธก(observe)ํ•ด์„œ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

์œ„์˜ ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ๊ฒƒ์˜ ์ด์ ์€ ์ฆ‰์‹œ ์—ฌ๋Ÿฌ๋ถ„์˜ ๊ธฐ๋Šฅ์— ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๊ณ , ๊ฒŒ๋‹ค๊ฐ€ ํฌ๊ณ  ๋ณต์žกํ•œ ๊ธฐ๋Šฅ์„ ์„œ๋กœ ๊ฒฐํ•ฉ ๊ฐ€๋Šฅํ•œ ์ž‘๊ณ  ๋…๋ฆฝ๋œ ๋ชจ๋“ˆ๋กœ ์ชผ๊ฐค ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ๋กœ ์„ค๋ช…๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ํ™”๋ฉด์— ์ˆซ์ž์™€ ์ด ์ˆซ์ž๋ฅผ ์ฆ๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” + ๋ฒ„ํŠผ, ๊ฐ์†Œํ•  ์ˆ˜ ์žˆ๋Š” - ๋ฒ„ํŠผ์ด ์žˆ๋‹ค๊ณ  ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋” ๋‹ค์–‘ํ•œ ํ–‰๋™์„ ์œ„ํ•ด ํƒญ ํ•˜๋ฉด API ํ˜ธ์ถœ์„ ํ•ด์„œ ์ˆซ์ž์— ๊ด€ํ•œ ๋ฌด์ž‘์œ„ ์‚ฌ์‹ค์„ ์•Œ๋ฆผ์ฐฝ์œผ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๋ฒ„ํŠผ๋„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ํ™”๋ฉด์˜ ์ƒํƒœ(State)๋Š” ๋ฌด์—‡์ด ์žˆ์„๊นŒ์š”? ๋จผ์ € ํ™”๋ฉด์˜ ์ˆซ์ž๋ฅผ ์ •์ˆ˜๋กœ ๊ฐ€์ง€๊ณ  ์žˆ์„ ๊ฒƒ์ด๊ณ , ์•Œ๋ฆผ์ฐฝ์„ ๋ณด์—ฌ์ค„ ๋•Œ ํ•„์š”ํ•œ ์ˆซ์ž์— ๊ด€ํ•œ ์‚ฌ์‹ค๋„ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. (์•Œ๋ฆผ์ฐฝ์ด ๋œฐ ํ•„์š”๊ฐ€ ์—†๋Š” ์ƒํ™ฉ์—์„  nil ๊ฐ’์„ ๋„ฃ์–ด์•ผ ํ•˜๋‹ˆ ์˜ต์…”๋„๋กœ ๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค.)

struct AppState: Equatable {
  var count = 0
  var numberFactAlert: String?
}

ํ–‰๋™(Action)์—๋Š” ๋ฌด์—‡์ด ์žˆ์„๊นŒ์š”? ์ฆ๊ฐ€ ๋ฒ„ํŠผ์ด๋‚˜ ๊ฐ์†Œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋Š” ํ–‰๋™์€ ๋ˆ„๊ตฌ๋‚˜ ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ์„ ๋งŒํผ ๋ช…ํ™•ํ•œ ํ–‰๋™๋„ ์žˆ๊ณ , ๋ฐ˜๋Œ€๋กœ ์•Œ๋ฆผ์ฐฝ์„ ๋‹ซ๊ฑฐ๋‚˜ ๋ฌด์ž‘์œ„ ์‚ฌ์‹ค API ๋ฆฌํ€˜์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์•˜์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ํ–‰๋™๊ฐ™์ด ์•ฝ๊ฐ„์€ ์ƒ๊ฐํ•˜๊ธฐ ์–ด๋ ค์šด ํ–‰๋™๋„ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

enum AppAction: Equatable {
  case factAlertDismissed
  case decrementButtonTapped
  case incrementButtonTapped
  case numberFactButtonTapped
  case numberFactResponse(Result<String, ApiError>)
}

struct ApiError: Error, Equatable {}

๋‹ค์Œ์€ ํ™”๋ฉด์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ์˜์กด์„ฑ(Dependency)์„ ๊ด€๋ฆฌํ•˜๋Š” ํ™˜๊ฒฝ(Environment) ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค. ์ˆซ์ž์— ๊ด€ํ•œ ์‚ฌ์‹ค์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒฝ์šฐ ๋„คํŠธ์›Œํฌ ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ์š”์•ฝํ•ด์„œ Effect ๊ฐ’์œผ๋กœ ๋งŒ๋“œ๋Š” ์ž‘์—…์ด ์žˆ๊ฒ ๋„ค์š”. ์ด ์ž‘์—…์˜ ์˜์กด์„ฑ์€ Int๋ฅผ ๋ฐ›์•„์„œ Effect<String, ApiError>๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๊ฐ€ ๋˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ String์€ ๋ฆฌํ€˜์ŠคํŠธ์˜ ๋ฆฌ์Šคํฐ์Šค๋ฅผ ์š”์•ฝํ•œ ๊ฐ’์ž…๋‹ˆ๋‹ค. ์ดํŽ™ํŠธ๋Š” ํ†ต์ƒ์ ์œผ๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค(URLSession์ด ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ์š”). ์ €ํฌ๋Š” ์ดํŽ™ํŠธ์˜ ๊ฐ’์„ ๋ฉ”์ธ ํ์—์„œ ๋ฐ›์„ ๋ฐฉ๋ฒ•์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”์ธ ํ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. AnyScheduler๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ”„๋กœ๋•์…˜์—์„  DispatchQueue๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ํ…Œ์ŠคํŠธ ์‹œ์—” ํ…Œ์ŠคํŠธ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•ด๋ด…์‹œ๋‹ค.

struct AppEnvironment {
  var mainQueue: AnySchedulerOf<DispatchQueue>
  var numberFact: (Int) -> Effect<String, ApiError>
}

์ด์ œ ๋ฆฌ๋“€์„œ(Reducer)๋ฅผ ๊ตฌํ˜„ํ•ด๋ด…์‹œ๋‹ค. ๊ทธ๋Ÿฌ๋ ค๋ฉด ํ˜„์žฌ ์ƒํƒœ(State)๋ฅผ ๋ณ€ํ™”์‹œ์ผœ์„œ ๋‹ค์Œ ์ƒํƒœ๋กœ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์„ค๋ช…๊ณผ ์–ด๋–ค ์ดํŽ™ํŠธ(Effect)๊ฐ€ ์‹คํ–‰๋ผ์•ผํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์„ค๋ช…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์–ด๋– ํ•œ ์ดํŽ™ํŠธ๋„ ์‹คํ–‰์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—” .none์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
  switch action {
  case .factAlertDismissed:
    state.numberFactAlert = nil
    return .none

  case .decrementButtonTapped:
    state.count -= 1
    return .none

  case .incrementButtonTapped:
    state.count += 1
    return .none

  case .numberFactButtonTapped:
    return environment.numberFact(state.count)
      .receive(on: environment.mainQueue)
      .catchToEffect()
      .map(AppAction.numberFactResponse)

  case let .numberFactResponse(.success(fact)):
    state.numberFactAlert = fact
    return .none

  case .numberFactResponse(.failure):
    state.numberFactAlert = "Could not load a number fact :("
    return .none
  }
}

๋งˆ์ง€๋ง‰์œผ๋กœ ์ด ๊ธฐ๋Šฅ์ด ์ž‘๋™๋  ๋ทฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. Store<AppState, AppAction>๊ฐ€ ์žˆ์œผ๋ฉด ๋ชจ๋“  ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ๊ด€์ธกํ•˜๊ณ  UI๋ฅผ ๋‹ค์‹œ ๊ทธ๋ฆด ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์‚ฌ์šฉ์ž ํ–‰๋™์„ ๋ณด๋‚ด์„œ ์ƒํƒœ๋ฅผ ๋ณ€ํ™”ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. .alert View Modifier๊ฐ€ ์š”๊ตฌํ•˜๋Š” ๋Œ€๋กœ ์ˆซ์ž์— ๊ด€ํ•œ ์‚ฌ์‹ค์„ ๊ตฌ์กฐ์ฒด๋กœ ํ•œ ๋ฒˆ ๊ฐ์‹ธ์„œ Identifiable์„ ๋”ฐ๋ฅด๊ฒŒ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค.

struct AppView: View {
  let store: Store<AppState, AppAction>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      VStack {
        HStack {
          Button("โˆ’") { viewStore.send(.decrementButtonTapped) }
          Text("\(viewStore.count)")
          Button("+") { viewStore.send(.incrementButtonTapped) }
        }

        Button("Number fact") { viewStore.send(.numberFactButtonTapped) }
      }
      .alert(
        item: viewStore.binding(
          get: { $0.numberFactAlert.map(FactAlert.init(title:)) },
          send: .factAlertDismissed
        ),
        content: { Alert(title: Text($0.title)) }
      )
    }
  }
}

struct FactAlert: Identifiable {
  var title: String
  var id: String { self.title }
}

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

์ด ๋ง์ธ์ฆ‰์Šจ, ๋™์ผํ•œ ์Šคํ† ์–ด์— UIKit์„ ๋ถ™์ด๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์˜๋ฏธ์ž…๋‹ˆ๋‹ค. UI ์—…๋ฐ์ดํŠธ๋‚˜ ์•Œ๋ฆผ์ฐฝ์„ ๋ณด์—ฌ์ฃผ๋Š” ์ž‘์—…์„ ์œ„ํ•ด viewDidLoad์—์„œ ์Šคํ† ์–ด๋กœ ๊ตฌ๋…(Subscribe)ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ฝ”๋“œ ์ž์ฒด๋Š” SwiftUI ๋ฒ„์ „๋ณด๋‹ค ์กฐ๊ธˆ ๋” ๊น๋‹ˆ๋‹ค.

ํŽผ์ณ๋ณด๊ธฐ
class AppViewController: UIViewController {
  let viewStore: ViewStore<AppState, AppAction>
  var cancellables: Set<AnyCancellable> = []

  init(store: Store<AppState, AppAction>) {
    self.viewStore = ViewStore(store)
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    let countLabel = UILabel()
    let incrementButton = UIButton()
    let decrementButton = UIButton()
    let factButton = UIButton()

    // addSubview๋‚˜ constraint ์„ค์ •ํ•˜๋Š” ์ฝ”๋“œ๋Š” ์ƒ๋žตํ–ˆ์Šต๋‹ˆ๋‹ค

    self.viewStore.publisher
      .map { "\($0.count)" }
      .assign(to: \.text, on: countLabel)
      .store(in: &self.cancellables)

    self.viewStore.publisher.numberFactAlert
      .sink { [weak self] numberFactAlert in
        let alertController = UIAlertController(
          title: numberFactAlert, message: nil, preferredStyle: .alert
        )
        alertController.addAction(
          UIAlertAction(
            title: "Ok",
            style: .default,
            handler: { _ in self?.viewStore.send(.factAlertDismissed) }
          )
        )
        self?.present(alertController, animated: true, completion: nil)
      }
      .store(in: &self.cancellables)
  }

  @objc private func incrementButtonTapped() {
    self.viewStore.send(.incrementButtonTapped)
  }
  @objc private func decrementButtonTapped() {
    self.viewStore.send(.decrementButtonTapped)
  }
  @objc private func factButtonTapped() {
    self.viewStore.send(.numberFactButtonTapped)
  }
}

์ด์ œ ๋ทฐ๋Š” ์ค€๋น„๋˜์—ˆ์œผ๋‹ˆ ์ž‘๋™์„ ์œ„ํ•œ ์Šคํ† ์–ด๋ฅผ ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค. ์—ฌ๊ธฐ์„œ ๋””ํŽœ๋˜์‹œ๋ฅผ ์ œ๊ณตํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  API ๋ฆฌํ€˜์ŠคํŠธ๋ฅผ ์ƒ๋žตํ•˜๊ธฐ ์œ„ํ•ด ๋ฌธ์ž์—ด์„ mock ํ•ด์„œ ๋ฐ”๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ดํŽ™ํŠธ๋ฅผ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.

let appView = AppView(
  store: Store(
    initialState: AppState(),
    reducer: appReducer,
    environment: AppEnvironment(
      mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
      numberFact: { number in Effect(value: "\(number) is a good number Brent") }
    )
  )
)

๋“œ๋””์–ด ํ™”๋ฉด์„ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•œ ์ž‘์—…์ด ๋ชจ๋‘ ๋๋‚ฌ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์—ฌ๋Ÿฌ ๋‹จ๊ณ„๋ฅผ ํ†ตํ•ด ๊ธฐ๋Šฅ์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์€ ์ˆœ์ˆ˜ํ•˜๊ฒŒ SwiftUI๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ๋ณด๋‹จ ํ™•์‹คํžˆ ๋ช‡ ๋‹จ๊ณ„ ๋” ์žˆ๊ธด ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ๋งŒํผ ๋” ์ด์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋‹จ๊ณ„๋Š” ๋‹จ์ˆœํžˆ ๋กœ์ง์„ ๊ด€์ธก ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด๋‚˜ ๋‹ค์–‘ํ•œ UI ์ปดํฌ๋„ŒํŠธ์˜ ํด๋กœ์ €์— ํฉ๋ฟŒ๋ฆฌ๋Š” ๊ฒƒ๋ณด๋‹ค, ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์ ์šฉํ•˜๋Š” ๊ฒƒ์— ์ผ๊ด€๋œ ํƒœ๋„๋ฅผ ๊ฐ€์ง€๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค. ๋˜ํ•œ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ํ‘œํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ถ”๊ฐ€์ ์ธ ์ž‘์—… ์—†์ด ์ดํŽ™ํŠธ๊ฐ€ ํฌํ•จ๋œ ๋กœ์ง์„ ๋ฐ”๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŒ…

ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด์„  TestStore๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. TestStore๋Š” ์•ž์—์„œ ๋งŒ๋“  ์Šคํ† ์–ด์™€ ๊ฐ™์€ ๋‚ด์šฉ์˜ ๋””ํŽœ๋˜์‹œ๋กœ ๋งŒ๋“ค์–ด๋„ ๋˜์ง€๋งŒ, ์ด๋ฒˆ์—” ์กฐ๊ธˆ ๋” ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์ข‹์€ ๋””ํŽœ๋˜์‹œ๋ฅผ ์ฃผ์ž…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ, ๋ผ์ด๋ธŒ DispatchQueue.main ๋Œ€์‹  ํ…Œ์ŠคํŠธ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์–ด๋–ค ์ž‘์—…์ด ์ง„ํ–‰๋˜๋Š” ๊ฒƒ์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์–ด์„œ ํ๋ฅผ ๊ตณ์ด ๊ธฐ๋‹ค๋ฆด ํ•„์š”๊ฐ€ ์—†๊ฒŒ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค.

let scheduler = DispatchQueue.testScheduler

let store = TestStore(
  initialState: AppState(),
  reducer: appReducer,
  environment: AppEnvironment(
    mainQueue: scheduler.eraseToAnyScheduler(),
    numberFact: { number in Effect(value: "\(number) is a good number Brent") }
  )
)

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

์•„๋ž˜์˜ ํ…Œ์ŠคํŠธ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ฆ๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๊ณ  ๊ฐ์†Œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ ํ›„ ์ˆซ์ž์— ๊ด€ํ•œ ์‚ฌ์‹ค์„ ์š”์ฒญํ•œ ๋’ค ์ดํŽ™ํŠธ์˜ ๋ฆฌ์Šคํฐ์Šค๊ฐ€ ์•Œ๋ฆผ์ฐฝ์„ ๋„์šฐ๊ฒŒ ๋งŒ๋“ค๊ณ  ๋งˆ์ง€๋ง‰์œผ๋กœ ์•Œ๋ฆผ์ฐฝ์„ ๋‹ซ๋Š” ๊ฒƒ๊นŒ์ง€์˜ ๋‚ด์šฉ์„ ๋‹ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

store.assert(
  // ์ฆ๊ฐ€/๊ฐ์†Œ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๊ฒฝ์šฐ ์นด์šดํŠธ๋ฅผ ๋ฐ”๊พธ๋Š” ๊ฒƒ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ
  .send(.incrementButtonTapped) {
    $0.count = 1
  },
  .send(.decrementButtonTapped) {
    $0.count = 0
  },

  // ์ˆซ์ž์— ๊ด€ํ•œ ์žฌ๋ฐŒ๋Š” ์‚ฌ์‹ค ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๊ณ  ์ดํŽ™ํŠธ์—์„œ ๋ฆฌ์Šคํฐ์Šค๋ฅผ ๋ฐ›๋Š” ๊ฒƒ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ
  // reducer์—์„œ `.receive(on:)`์„ ์‚ฌ์šฉํ–ˆ์œผ๋‹ˆ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ `advance()`ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  .send(.numberFactButtonTapped),
  .do { scheduler.advance() },
  .receive(.numberFactResponse(.success("0 is a good number Brent"))) {
    $0.numberFactAlert = "0 is a good number Brent"
  },

  // ์•Œ๋ฆผ์ฐฝ ๋‹ซ๊ธฐ
  .send(.factAlertDismissed) {
    $0.numberFactAlert = nil
  }
)

์—ฌ๊ธฐ๊นŒ์ง€ The Composable Architecture์—์„œ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค๊ณ  ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•œ ๊ธฐ๋ณธ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์•ž์œผ๋กœ ํ•ฉ์„ฑ(Composition), ๋ชจ๋“ˆํ™”(Modularity), ์ ์‘์„ฑ(Adaptability), ๋ณต์žกํ•œ ์ดํŽ™ํŠธ๋ฅผ ๋‹ค๋ฃจ๋Š” ๋ฒ•๊ณผ ๊ฐ™์ด ์—ฌ๋Ÿฌ๋ถ„์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๊ฐœ๋…๋“ค์ด ์ •๋ง ๋งŽ์Šต๋‹ˆ๋‹ค. ์˜ˆ์ œ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ๋” ์ž์„ธํ•œ ์‚ฌ์šฉ๋ฒ•์ด ์†Œ๊ฐœ๋ผ ์žˆ์œผ๋‹ˆ ์‚ดํŽด๋ณด์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

๋””๋ฒ„๊น…

The Composable Architecture๋Š” ์—ฌ๋Ÿฌ ๋””๋ฒ„๊น… ๋„๊ตฌ๋ฅผ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  • reducer.debug()๋Š” ๋ฆฌ๋“€์„œ๊ฐ€ ๋ฐ›๋Š” ๋ชจ๋“  ํ–‰๋™๊ณผ ์ƒํƒœ ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ์„ค๋ช…์„ ๋””๋ฒ„๊ทธ ์ฐฝ์— ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.

    received action:
      AppAction.todoCheckboxTapped(
        index: 0
      )
    โ€‡ AppState(
    โ€‡   todos: [
    โ€‡     Todo(
    -       isComplete: false,
    +       isComplete: true,
    โ€‡       description: "Milk",
    โ€‡       id: 5834811A-83B4-4E5E-BCD3-8A38F6BDCA90
    โ€‡     ),
    โ€‡     Todo(
    โ€‡       isComplete: false,
    โ€‡       description: "Eggs",
    โ€‡       id: AB3C7921-8262-4412-AA93-9DC5575C1107
    โ€‡     ),
    โ€‡     Todo(
    โ€‡       isComplete: true,
    โ€‡       description: "Hand Soap",
    โ€‡       id: 06E94D88-D726-42EF-BA8B-7B4478179D19
    โ€‡     ),
    โ€‡   ]
    โ€‡ )
  • reducer.signpost()๋Š” ํ–‰๋™์ด ์‹คํ–‰๋˜๋Š” ๋ฐ์— ๊ฑธ๋ฆฐ ์‹œ๊ฐ„๊ณผ ์–ธ์ œ ์‹คํ–‰๋˜๋Š”์ง€ ๋“ฑ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋„๋ก Instrument์— ํ‘œ์‹œ๋ฅผ ์ƒ์„ฑํ•ด์ค๋‹ˆ๋‹ค.

์ถ”๊ฐ€์ ์ธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

Composable Architecture์˜ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์›์น™ ์ค‘ ํ•˜๋‚˜๋Š” ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋Š” ์ ˆ๋Œ€ ์ง์ ‘์ ์œผ๋กœ ์‹คํ–‰๋˜์ง€ ์•Š๊ณ , ๋Œ€์‹  Effect ํƒ€์ž…์— ๊ฐ์‹ผ ํ›„์— ๋ฆฌ๋“€์„œ์—์„œ ๋ฐ˜ํ™˜๋˜๊ณ  ๋‚˜์ค‘์— ์Šคํ† ์–ด์—์„œ ์‹คํ–‰๋œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ฐ์ดํ„ฐ ํ”Œ๋กœ์šฐ๋ฅผ ๊ฐ„๊ฒฐํ™”ํ•˜๋Š” ๋ฐ ์žˆ์–ด์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋‚ด์šฉ์œผ๋กœ ์ด ์›์น™์„ ๋”ฐ๋ผ์•ผ ์‚ฌ์šฉ์ž์˜ ํ–‰๋™๊ณผ ์ดํŽ™ํŠธ ์‹คํ–‰ ์‚ฌ์ด์˜ ์‚ฌ์ดํด์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์„ ๋ณด์žฅ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด ๋ง์€ ์šฐ๋ฆฌ๊ฐ€ ๋งค์ผ ๋งŒ๋‚˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ SDK๊ฐ€ Composable Architecture ์Šคํƒ€์ผ๋กœ ๋ฐ”๋€Œ์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•˜๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ์ €ํฌ๋Š” ์ด ๊ณ ํ†ต์„ ์กฐ๊ธˆ์ด๋‚˜๋งˆ ๋œ์–ด๋“œ๋ฆฌ๊ธฐ ์œ„ํ•ด ์• ํ”Œ์˜ ์œ ๋ช…ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ Composable Architecture์—์„œ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ ์ž˜ ์ž‘๋™ํ•˜๋„๋ก ๋ž˜ํผ(wrapper) ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ง€์›ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ชฉ๋ก์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • ComposableCoreLocation: CLLocationManager์˜ ๋ž˜ํผ๋กœ, ๋ฆฌ๋“€์„œ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฝ๊ณ  CLLocationManager์˜ ๊ธฐ๋Šฅ์„ ๋กœ์ง์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ์—๋„ ์šฉ์ดํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.
  • ComposableCoreMotion: CMMotionManager์˜ ๋ž˜ํผ๋กœ, ๋ฆฌ๋“€์„œ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฝ๊ณ  CMMotionManager์˜ ๊ธฐ๋Šฅ์„ ๋กœ์ง์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ์—๋„ ์šฉ์ดํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

์ €ํฌ๊ฐ€ ์•„์ง ์ž‘์—…ํ•˜์ง€ ๋ชปํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์œ„ํ•œ ๋ž˜ํผ๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ์œผ์‹œ๋ฉด ์–ธ์ œ๋“  ์ด์Šˆ๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”! ๋‚˜์•„๊ฐˆ ๋ฐฉํ–ฅ์— ๋Œ€ํ•ด ํ•จ๊ป˜ ํ† ๋ก ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ

  • Composable Architecture๊ฐ€ Elm์ด๋‚˜ Redux๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ๋‹ค๋ฅธ ์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?

    ํŽผ์ณ์„œ ๋‹ต๋ณ€๋ณด๊ธฐ the Composable Architecture(TCA)๋Š” the Elm Architecture(TEA)์™€ Redux๊ฐ€ ๋Œ€์ค‘ํ™”ํ•œ ์•„์ด๋””์–ด์— ๊ธฐ๋ฐ˜ํ•˜๊ณ  ์žˆ์ง€๋งŒ ์• ํ”Œ์˜ ํ”Œ๋žซํผ์—์„œ Swift ์–ธ์–ด์— ๋งž๊ฒŒ ๋งŒ๋“ค์–ด์กŒ์Šต๋‹ˆ๋‹ค.

    TCA์˜ ๋ช‡๋ช‡ ๋ถ€๋ถ„์€ ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์— ๋น„ํ•ด ์ข€ ๋” ๊ณ ์ง‘์ด ์žˆ๋Š” ํŽธ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, Redux๋Š” ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋ฅผ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•œ ๊ทœ์น™์ด ์—†๋Š” ๋ฐ˜๋ฉด, TCA๋Š” ๋ชจ๋“  ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋ฅผ Effect ํƒ€์ž…์œผ๋กœ ๋ชจ๋ธ๋งํ•˜๊ณ  ๋ฆฌ๋“€์„œ๊ฐ€ ๋ฐ˜ํ™˜ํ•ด์•ผํ•˜๋Š” ๊ฒƒ์ด ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.

    ์–ด๋–ค ๋ถ€๋ถ„์—์„  TCA๋Š” ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์— ๋น„ํ•ด ๋Š์Šจํ•œ ํŽธ์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, Elm์—์„œ Cmd ํƒ€์ž…์œผ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” effect์˜ ์ข…๋ฅ˜๋ฅผ ์ปจํŠธ๋กคํ•˜๋Š” ๋ฐ˜๋ฉด์— TCA๋Š” Effect๊ฐ€ Combine์˜ Publisher ํ”„๋กœํ† ์ฝœ์„ ๋”ฐ๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ์–ด๋–ค ์ข…๋ฅ˜์˜ ์ดํŽ™ํŠธ๋„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ๋˜, TCA๋Š” Redux๋‚˜ Elm ๋“ฑ ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š๋Š” ๋ถ€๋ถ„์— ๋†’์€ ์šฐ์„  ์ˆœ์œ„๋ฅผ ์ฃผ๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ฑฐ๋Œ€ํ•œ ๊ธฐ๋Šฅ์„ ์ž‘์€ ๋‹จ์œ„์˜ ์ชผ๊ฐœ๊ณ  ๋‹ค์‹œ ๊ฒฐํ•ฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ•ฉ์„ฑ(Composition)์€ TCA์—์„œ ์•„์ฃผ ์ค‘์š”ํ•œ ์ธก๋ฉด ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค. ํ•ฉ์„ฑ์€ ๋ฆฌ๋“€์„œ์˜ pullback๊ณผ combine ์—ฐ์‚ฐ์ž ๋•๋ถ„์— ์™„์„ฑํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ , ๊ฒฐ๊ณผ์ ์œผ๋กœ ๋ณต์žกํ•œ ๊ธฐ๋Šฅ์„ ๋ชจ๋“ˆํ™”ํ•ด์„œ ๋” ๋…๋ฆฝ๋œ ์ฝ”๋“œ๋กœ ๋งŒ๋“ค๊ณ  ๋” ๋‚˜์€ ์ปดํŒŒ์ผ ์‹œ๊ฐ„์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

  • Store๊ฐ€ thread-safe ํ•œ ์ด์œ ๋Š” ๋ฌด์—‡์ธ๊ฐ€์š”?
    ์™œ send๋Š” ํ์— ์Œ“์ด์ง€ ์•Š๋‚˜์š”?
    ์™œ send๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋˜์ง€ ์•Š๋‚˜์š”?

    ํŽผ์ณ์„œ ๋‹ต๋ณ€๋ณด๊ธฐ action์ด `Store`๋กœ ๋ณด๋‚ด์ง€๋ฉด ๋ฆฌ๋“€์„œ๋Š” ์ง€๊ธˆ ์ƒํƒœ์—์„œ ์‹คํ–‰๋˜๊ณ , ์ด ์ž‘์—… ์ž์ฒด๋Š” ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. `send`์˜ ๊ตฌํ˜„๋ถ€์—์„œ ํ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๊ฒ ์ง€๋งŒ, ์ด๋Š” ์ƒˆ๋กœ์šด ๋ฌธ์ œ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
    1. ๊ฐ„ํŽธํ•˜๊ฒŒ DispatchQueue.main.async๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์Šค๋ ˆ๋“œ๋ฅผ ๋›ฐ์–ด๋„˜์œผ๋ ค๋Š” ์ผ์ด ์ผ์–ด๋‚  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋•Œ๋กœ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ธ”๋ฝ์ฒ˜๋Ÿผ ๋™๊ธฐ์ ์œผ๋กœ ์ผ์–ด๋‚˜์•ผ ํ•˜๋Š” ์ž‘์—…์ด ์žˆ์„ํ…๋ฐ, ์ด๋Ÿด ๊ฒฝ์šฐ UIKit๊ณผ SwiftUI์˜ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    2. DispatchQueue.main.async๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์Œ“์ธ ์ž‘์—…์„ ๋ฐ”๋กœ ์‹คํ–‰ํ•˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. (์˜ˆ์‹œ: ReactiveSwift์˜ UIScheduler) ์ด๋Š” ์˜คํžˆ๋ ค ์ƒํ™ฉ์„ ๋” ๋ณต์žกํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ ๋•Œ๋ฌธ์— ์—„์ฒญ ๊ดœ์ฐฎ์€ ์ด์œ ๊ฐ€ ์—†๋‹ค๋ฉด ์•„๋งˆ ์ฑ„ํƒ๋˜์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    ๊ฒฐ๊ตญ ์ €ํฌ๋Š” Store๊ฐ€ ์• ํ”Œ์˜ API์ฒ˜๋Ÿผ ์ƒํ˜ธ์ž‘์šฉํ•˜๋„๋ก ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. URLSession์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ๋กœ ๊ฒฐ๊ณผ๋ฅผ ์ „๋‹ฌํ•˜๊ณ  ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋กœ ๋„˜๊ธฐ๋Š” ์ž‘์—…์€ ์šฐ๋ฆฌ์—๊ฒŒ ๋งก๊ธฐ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ TCA๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ํ–‰๋™์„ ๋ณด๋‚ด๋Š” ๊ฒƒ์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋งก๊น๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์—ฌ๋Ÿฌ๋ถ„์ด ์ถœ๋ ฅ์„ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ์•„๋‹Œ ๊ณณ์œผ๋กœ ์ „๋‹ฌํ•˜๋Š” ์ดํŽ™ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์‹ ๋‹ค๋ฉด .receive(on:)์„ ์ด์šฉํ•ด์„œ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋กœ ๋„˜๊ธฐ๋„๋ก ๋งŒ๋“œ์…”์•ผ ํ•ฉ๋‹ˆ๋‹ค.

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

    ์ €ํฌ๊ฐ€ ์„ ํƒํ•œ ๋ฐฉ์‹์ด ๋งˆ์Œ์— ๋“ค์ง€ ์•Š์œผ์‹œ๋‚˜์š”? ๊ฑฑ์ •๋งˆ์„ธ์š”! The Composable Architecture๋Š” ์ด ๋ถ€๋ถ„์„ ์—ฌ๋Ÿฌ๋ถ„์ด ์›ํ•˜๋Š”๋Œ€๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋„๋ก ์œ ์—ฐํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์กŒ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด ๋ชจ๋“  ์ดํŽ™ํŠธ์— ๋Œ€ํ•ด์„œ ๊ฒฐ๊ณผ๋ฅผ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์— ์ „๋‹ฌํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ณ ๊ณ„ ๋ฆฌ๋“€์„œ๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ฃผ์ž…ํ•˜๋ฉด ์Šค๋ ˆ๋“œ์— ๋Œ€ํ•œ ์ฑ…์ž„์„ ๊ฑฑ์ •ํ•˜์ง€ ์•Š์œผ์…”๋„ ๋ฉ๋‹ˆ๋‹ค.

    extension Reducer {
      func receive<S: Scheduler>(on scheduler: S) -> Self {
        Self { state, action, environment in
          self(&state, action, environment)
            .receive(on: scheduler)
            .eraseToEffect()
        }
      }
    }

    ๊ทธ๋ž˜๋„ ์—ฌ์ „ํžˆ ๋ถˆํ•„์š”ํ•œ ์Šค๋ ˆ๋“œ ๊ฑด๋„ˆ๋›ฐ๊ธฐ๊ฐ€ ์ƒ๊ธฐ์ง€ ์•Š๋„๋ก ํ•ด์ฃผ๋Š” UIScheduler๋Š” ๋‹ˆ์ฆˆ๊ฐ€ ์žˆ๊ฒ ๋„ค์š”.

์ตœ์†Œ ๊ฐœ๋ฐœ ํƒ€๊ฒŸ

The Composable Architecture๋Š” Combine ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋””ํŽœ๋˜์‹œ๋กœ ๊ฐ–๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ตœ์†Œ ๊ฐœ๋ฐœ ํƒ€๊ฒŸ์ด iOS 13, macOS 10.15, Mac Catalyst 13, tvOS 13 ๊ทธ๋ฆฌ๊ณ  watchOS 6 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋” ์ด์ „ OS๋ฅผ ์ง€์›ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๋Š” ReactiveSwift ๋ฒ„์ „์ด๋‚˜ RxSwift๋ฒ„์ „์„ ์‚ฌ์šฉํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค!

์„ค์น˜ ๋ฐฉ๋ฒ•

Composable Architecture๋Š” Xcode ํ”„๋กœ์ ํŠธ์— ํŒจํ‚ค์ง€ ๋””ํŽœ๋˜์‹œ๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. File ๋ฉ”๋‰ด์—์„œ Swift Packages โ€บ Add Package Dependency๋ฅผ ์„ ํƒํ•˜์„ธ์š”.
  2. ์ €์žฅ์†Œ URL ํ…์ŠคํŠธ ํ•„๋“œ์— "https://github.com/pointfreeco/swift-composable-architecture"๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.
  3. ์—ฌ๋Ÿฌ๋ถ„์˜ ํ”„๋กœ์ ํŠธ๊ฐ€ ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑ๋ผ์žˆ๋Š”๊ฐ€์— ๋”ฐ๋ผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์—…ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
    • ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ ‘๊ทผํ•ด์•ผ ํ•˜๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ํƒ€๊ฒŸ์ด ํ•˜๋‚˜์ผ ๊ฒฝ์šฐ, ComposableArchitecture๋ฅผ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋ฐ”๋กœ ์ถ”๊ฐ€ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
    • ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ํƒ€๊ฒŸ์ด ์—ฌ๋Ÿฌ ๊ฐœ์ผ ๊ฒฝ์šฐ, ๊ณต์œ  ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋งŒ๋“ค์–ด์„œ ComposableArchitecture๋ฅผ ๋””ํŽœ๋˜์‹œ๋กœ ๊ฐ€์ง€๊ฒŒ ๋งŒ๋“  ํ›„ ๊ทธ ๊ณต์œ  ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๊ฐ ํƒ€๊ฒŸ์—์„œ ๋””ํŽœ๋˜์‹œ๋กœ ๊ฐ€์ง€๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋ฐ๋ชจ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ธ Tic-Tac-Toe์—์„œ ๊ธฐ๋Šฅ์„ ์—ฌ๋Ÿฌ ๋ชจ๋“ˆ๋กœ ์ชผ๊ฐ  ํ›„ ์ •์  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ TicTacToeCommon ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์œผ๋‹ˆ ์ด ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์„ ์ž์„ธํžˆ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์„ ๊ฒ๋‹ˆ๋‹ค.

์ตœ์‹  ๊ฐœ๋ฐœ ๋ฌธ์„œ

Composable Architecture์˜ ๊ฐ€์žฅ ์ตœ์‹  ๋ฒ„์ „ ๊ฐœ๋ฐœ ๋ฌธ์„œ๋Š” ์—ฌ๊ธฐ์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋„์›€๋ฐ›๊ธฐ

Composable Architecture์— ๋Œ€ํ•ด ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ์ €ํฌ์™€ ํ† ๋ก ์„ ํ•˜๊ณ  ์‹ถ์œผ์‹  ๊ฒฝ์šฐ์—” discussions ํƒญ์—์„œ ํ† ํ”ฝ์„ ๋งŒ๋“œ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ํ˜น์€ Swift ํฌ๋Ÿผ์—์„œ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๊ฐ์‚ฌ ์ธ์‚ฌ

์•„๋ž˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ดˆ๊ธฐ ๊ฐœ๋ฐœ ๋‹จ๊ณ„๋ถ€ํ„ฐ ํ”ผ๋“œ๋ฐฑ์„ ์ฃผ์‹œ๊ณ  ์ง€๊ธˆ์˜ Composable Architecture๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ์‹  ๊ณ ๋งˆ์šด ๋ถ„๋“ค์˜ ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค.

Paul Colton, Kaan Dedeoglu, Matt Diephouse, Josef Doleลพal, Eimantas, Matthew Johnson, George Kaimakas, Nikita Leonov, Christopher Liscio, Jeffrey Macko, Alejandro Martinez, Shai Mishali, Willis Plummer, Simon-Pierre Roy, Justin Price, Sven A. Schmidt, Kyle Sherman, Petr ล รญma, Jasdev Singh, Maxim Smirnov, Ryan Stone, Daniel Hollis Tavares, ๊ทธ๋ฆฌ๊ณ  ๋ชจ๋“  Point-Free ๊ตฌ๋…์ž๋ถ„๋“ค๊นŒ์ง€ ๋ชจ๋‘ ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ๐Ÿ˜

ํŠนํžˆ, SwiftUI์˜ ๊ธฐ์ดํ•œ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ณ  ์ตœ์ข… API๋ฅผ ๊ฐœ์„ ํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ค€ Chris Liscio๊ป˜๋Š” ํŠน๋ณ„ํ•œ ๊ฐ์‚ฌ๋ฅผ ๋“œ๋ฆฌ๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  Shai Mishali์™€ CombineCommunity ํ”„๋กœ์ ํŠธ์˜ Publishers.Create๊ฐ€ ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— Effect์—์„œ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ์™€ ์ฝœ๋ฐฑ ๊ธฐ๋ฐ˜ API๋ฅผ ์—ฐ๊ฒฐํ•˜์—ฌ ํƒ€์‚ฌ ํ”„๋ ˆ์ž„์›Œํฌ์™€ ํ†ต์‹ ํ•  ๋•Œ ๋” ๋‚˜์€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

๋‹ค๋ฅธ ๋Œ€์•ˆ

Composable Architecture๋Š” Elm์ด๋‚˜ Redux๊ฐ™์€ ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์˜ ์•„์ด๋””์–ด์— ๊ธฐ๋ฐ˜ํ•ด์„œ ๋งŒ๋“ค์–ด์กŒ์Šต๋‹ˆ๋‹ค.

iOS ์ปค๋ฎค๋‹ˆํ‹ฐ์—๋Š” Composable Architecture ์ด์™ธ์—๋„ ๋‹ค๋ฅธ ์•„ํ‚คํ…์ฒ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ์ž ๋‹ค๋ฅธ ํŠน์ง•์„ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋‹ˆ ์‚ดํŽด๋ณด์‹œ๋Š” ๊ฒƒ๋„ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค.

๋ผ์ด์„ผ์Šค

์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” MIT ๋ผ์ด์„ผ์Šค๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ์ž์„ธํ•œ ์‚ฌํ•ญ์€ LICENSE๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment