Notice
Recent Posts
Recent Comments
Link
ยซ   2025/03   ยป
์ผ ์›” ํ™” ์ˆ˜ ๋ชฉ ๊ธˆ ํ† 
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
๊ด€๋ฆฌ ๋ฉ”๋‰ด

J

[SwiftUI] SwiftUI + TCA: ์ตœ์‹  ๋ฒ„์ „ TCA์˜ ๋™์ž‘ ๊ณผ์ • (์˜ˆ์ œ ํฌํ•จ) ๋ณธ๋ฌธ

SwiftUI

[SwiftUI] SwiftUI + TCA: ์ตœ์‹  ๋ฒ„์ „ TCA์˜ ๋™์ž‘ ๊ณผ์ • (์˜ˆ์ œ ํฌํ•จ)

yujaehui 2024. 7. 5. 16:50

๐Ÿง˜ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์—…

๊ธฐ์กด ๊ธ€์—์„œ TCA๋ฅผ ์ด๋ฃจ๋Š” ๊ธฐ๋ณธ ์š”์†Œ(?)์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ณ  ์™”๋Š”๋ฐ์š”.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” TCA์˜ ํ•ต์‹ฌ ๊ฐœ๋…๊ณผ ์ตœ์‹  ๋ฒ„์ „์˜ ์˜ˆ์ œ๋ฅผ ๋‹ค๋ค„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

ํ•™์Šตํ•˜๋ฉด์„œ ๊ฐ€์žฅ ํฌ๊ฒŒ ๋Š๋‚€ ์ ์€, ๋งŽ์€ ๋ธ”๋กœ๊ทธ ๊ธ€๊ณผ ChatGPT ๋‹ต๋ณ€์ด ๊ณผ๊ฑฐ ๋ฒ„์ „์„ ๊ธฐ์ค€์œผ๋กœ ํ•˜๊ณ  ์žˆ์–ด ์ตœ์‹  ๋ฒ„์ „์„ ์„ค์น˜ํ–ˆ์„ ๋•Œ ์˜ค๋ฅ˜๊ฐ€…

์ด์— ํ•™์Šต ๊ณผ์ •์ด ํ›จ์”ฌ ์–ด๋ ค์› ๋Š”๋ฐ, ์ด๋ฒˆ ๊ธ€์„ ํ†ตํ•ด ์ƒˆ๋กญ๊ฒŒ ํ•™์Šตํ•˜๋Š” ๋ถ„๋“ค์ด ๊ฐ™์€ ์–ด๋ ค์›€์„ ๊ฒช์ง€ ์•Š๋„๋ก ์ตœ์‹  ๋ฒ„์ „์„ ๊ธฐ์ค€์œผ๋กœ ์ •๋ฆฌํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

๐Ÿ” TCA๊ฐ€ ํ•ด๊ฒฐํ•ด์ฃผ๋Š” ํ•ต์‹ฌ ๊ฐœ๋…๋“ค

(1) ์ƒํƒœ ๊ด€๋ฆฌ ๐Ÿ› 

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณต.

  • ์ƒํƒœ๋ฅผ ๋‹จ์ˆœํ•œ ๊ฐ’ ํƒ€์ž…(struct)์œผ๋กœ ์ •์˜ํ•˜๊ณ  ๊ด€๋ฆฌ.
  • ์—ฌ๋Ÿฌ ํ™”๋ฉด์—์„œ ๋™์ผํ•œ ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๊ณ , ํ•œ ๊ณณ์—์„œ ๋ณ€๊ฒฝ๋œ ์ƒํƒœ๋ฅผ ๋‹ค๋ฅธ ๊ณณ์—์„œ๋„ ์ฆ‰์‹œ ๋ฐ˜์˜ ๊ฐ€๋Šฅ.

์˜ˆ๋ฅผ ๋“ค์–ด, ๋กœ๊ทธ์ธํ•œ ์œ ์ € ์ •๋ณด๋ฅผ ์—ฌ๋Ÿฌ ํ™”๋ฉด์—์„œ ๊ณต์œ ํ•ด์•ผ ํ•  ๋•Œ, ํ•˜๋‚˜์˜ State์—์„œ ๊ด€๋ฆฌํ•˜๊ณ  ๋ชจ๋“  ํ™”๋ฉด์ด ๊ฐ™์€ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ.


(2) ๊ตฌ์„ฑ ์š”์†Œ ๋ถ„๋ฆฌ(Composition) ๐Ÿงฉ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ปค์ง€๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์–ด๋ ค์›Œ์ง€๋Š”๋ฐ, TCA๋Š” ๊ธฐ๋Šฅ์„ ์ž‘์€ ๋‹จ์œ„๋กœ ๋‚˜๋ˆ„๊ณ , ๋‚˜์ค‘์— ๋‹ค์‹œ ํ•ฉ์น˜๋Š” ๊ตฌ์กฐ๋ฅผ ์ง€์›.

  • ํ•˜๋‚˜์˜ ํฐ ๊ธฐ๋Šฅ์„ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ž‘์€ ๊ธฐ๋Šฅ์œผ๋กœ ์ชผ๊ฐค ์ˆ˜ ์žˆ์Œ.
  • ๋‚˜๋ˆ ์ง„ ์ž‘์€ ๊ธฐ๋Šฅ๋“ค์€ ๋…๋ฆฝ์ ์ธ ๋ชจ๋“ˆ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ณ , ํ•„์š”ํ•  ๋•Œ ์‰ฝ๊ฒŒ ์กฐํ•ฉ ๊ฐ€๋Šฅ.

์˜ˆ๋ฅผ ๋“ค์–ด, Todo ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ ๋‹ค๊ณ  ํ•˜๋ฉด ๊ฐœ๋ณ„ Todo๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๋ชจ๋“ˆ, Todo ๋ฆฌ์ŠคํŠธ ์ „์ฒด๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๋ชจ๋“ˆ, ๋ฆฌ์ŠคํŠธ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๋Š” ๋ชจ๋“ˆ๋กœ ๋‚˜๋ˆ  ๋งŒ๋“ค๊ณ , ํ•„์š”ํ•  ๋•Œ ํ•ฉ์ณ์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ.


(3) ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ๊ด€๋ฆฌ ๐ŸŒ

๋„คํŠธ์›Œํฌ ์š”์ฒญ, ๋ฐ์ดํ„ฐ ์ €์žฅ, ์•Œ๋ฆผ ์ „์†ก ๊ฐ™์€ ์™ธ๋ถ€ ์ž‘์—…์„ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌ.

  • ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋Š” Reducer(๋ฆฌ๋“€์„œ) ๋‚ด๋ถ€์—์„œ Effect๋ผ๋Š” ๊ฐœ๋…์œผ๋กœ ๊ด€๋ฆฌ.
  • Effect๋Š” ํŠน์ • ์•ก์…˜์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์‹คํ–‰๋˜๊ณ , ์ดํ›„ ์ƒˆ๋กœ์šด ์•ก์…˜์„ ๋ฐฉ์ถœํ•  ์ˆ˜๋„ ์žˆ์Œ.
  • ์ด ๊ณผ์ •์ด ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์–ด์„œ, ์–ด๋–ค ์ฝ”๋“œ๊ฐ€ ์–ด๋–ค ์ž‘์—…์„ ํ•˜๋Š”์ง€ ์‰ฝ๊ฒŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์Œ.

์˜ˆ๋ฅผ ๋“ค์–ด, ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ API๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ํ™”๋ฉด์„ ์—…๋ฐ์ดํŠธํ•œ๋‹ค๋“ ์ง€, ์ƒˆ๋กœ์šด ์ผ์ • ์ถ”๊ฐ€ ์‹œ ์•Œ๋ฆผ์„ ์˜ˆ์•ฝํ•œ๋‹ค๋“ ์ง€ ์ด๋Ÿฐ ๊ฒƒ๋“ค์ด ๊น”๋”ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋  ์ˆ˜ ์žˆ์Œ.


(4) ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ โœ…

TCA์˜ ํ•ต์‹ฌ ์žฅ์  ์ค‘ ํ•˜๋‚˜๋Š” ํ…Œ์ŠคํŠธ ์šฉ์ด.

  • ์ƒํƒœ ๋ณ€ํ™”์™€ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋ฏ€๋กœ, ๊ฐ ๊ธฐ๋Šฅ์„ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ(Unit Test) ๊ฐ€๋Šฅ.
  • ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ธฐ๋Šฅ์ด ํ•ฉ์ณ์ง„ ์ƒํƒœ์—์„œ๋„ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ.
  • ์‹ค์ œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์—†์ด ๋ชจ์˜ ๋ฐ์ดํ„ฐ(Mock) ๋ฅผ ํ™œ์šฉํ•ด API ์—ฐ๋™ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅ.

์˜ˆ๋ฅผ ๋“ค์–ด, ํŠน์ • ์•ก์…˜์„ ๋ณด๋ƒˆ์„ ๋•Œ ์ƒํƒœ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฐ”๋€Œ๋Š”์ง€, API ์š”์ฒญ์ด ๋๋‚œ ํ›„ ์˜ฌ๋ฐ”๋ฅธ ๋ฐ์ดํ„ฐ๊ฐ€ ํ™”๋ฉด์— ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ.


(5) ์‚ฌ์šฉํ•˜๊ธฐ ํŽธ๋ฆฌํ•œ API ์ œ๊ณต ๐ŸŽฏ

TCA๋Š” ์œ„์˜ ๊ธฐ๋Šฅ๋“ค์„ ์‰ฝ๊ณ  ์ง๊ด€์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ผ๊ด€๋œ ๊ตฌ์กฐ๋ฅผ ์ œ๊ณต.

  • State, Reducer, Effect, Store ๊ฐ™์€ ๊ฐœ๋…์„ ์ดํ•ดํ•˜๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „๋ฐ˜์—์„œ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ.
  • ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ๋•Œ ๊ธฐ์กด ๊ตฌ์กฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๋ฉด์„œ ํ™•์žฅ ๊ฐ€๋Šฅํ•ด์„œ ์œ ์ง€๋ณด์ˆ˜ ์šฉ์ด.

๐Ÿ” TCA์˜ ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ

๊ตฌ์„ฑ ์š”์†Œ  ์„ค๋ช…
State ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ๊ตฌ์กฐ์ฒด
Action ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ ๋˜๋Š” ๋‚ด๋ถ€ ์ด๋ฒคํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•
Reducer Action์„ ๋ฐ›์•„ State๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜
Effect ๋„คํŠธ์›Œํฌ ์š”์ฒญ, ํƒ€์ด๋จธ ๋“ฑ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜
Store State, Reducer, Effect๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ค‘์•™ ์ €์žฅ์†Œ

๐Ÿ” TCA์˜ ๋™์ž‘ ๊ณผ์ • (๋ฐ์ดํ„ฐ ํ๋ฆ„)

 

TCA๋Š” ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„(Unidirectional Data Flow)์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.

[์‚ฌ์šฉ์ž ์ž…๋ ฅ] → [Action ๋ฐœ์ƒ] → [Reducer ์‹คํ–‰] → [State ๋ณ€๊ฒฝ] → [View ์—…๋ฐ์ดํŠธ]
  1. ์‚ฌ์šฉ์ž๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฆ„ (View → Action)
  2. Action์ด Store๋กœ ์ „๋‹ฌ๋จ (Store.send(action))
  3. Reducer๊ฐ€ Action์„ ๋ฐ›์•„ State๋ฅผ ๋ณ€๊ฒฝํ•จ
  4. Store๊ฐ€ State์˜ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜๊ณ  View๋ฅผ ๋‹ค์‹œ ๊ทธ๋ฆผ

โžก TCA์—์„œ๋Š” State๋ฅผ ์ง์ ‘ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†๊ณ , ๋ฐ˜๋“œ์‹œ Reducer๋ฅผ ํ†ตํ•ด ๋ณ€๊ฒฝํ•ด์•ผ ํ•จ!


๐Ÿ” TCA์˜ ์ฝ”๋“œ ์˜ˆ์ œ (Counter ์•ฑ)

(1) State (์•ฑ์˜ ์ƒํƒœ)

import ComposableArchitecture

struct CounterFeature: Reducer {
    struct State: Equatable {
        var count: Int = 0
    }
}
  • State๋Š” ์•ฑ์˜ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ตฌ์กฐ์ฒด
  • Equatable์„ ์ค€์ˆ˜ํ•˜์—ฌ ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ๊ฐ์ง€ ๊ฐ€๋Šฅ

(2) Action (์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ)

enum Action: Equatable {
    case increment
    case decrement
}
  • Action์€ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ํŠธ๋ฆฌ๊ฑฐํ•˜๋Š” ์—ด๊ฑฐํ˜•
  • increment, decrement ๊ฐ™์€ ์ด๋ฒคํŠธ๋ฅผ ์ •์˜

(3) Reducer (์ƒํƒœ ๋ณ€๊ฒฝ ๋กœ์ง)

var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .increment:
            state.count += 1  // ์ƒํƒœ ๋ณ€๊ฒฝ
            return .none

        case .decrement:
            state.count -= 1  // ์ƒํƒœ ๋ณ€๊ฒฝ
            return .none
        }
    }
}
  • Reducer๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜(Pure Function) ์ด๋ฉฐ, State๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์œ ์ผํ•œ ๊ณณ
  • return .none์€ ๋ถ€์ˆ˜ํšจ๊ณผ(Side Effect)๊ฐ€ ์—†์Œ์„ ์˜๋ฏธ

(4) Store (์ค‘์•™ ์ƒํƒœ ๊ด€๋ฆฌ)

import SwiftUI

struct CounterView: View {
    let store: StoreOf<CounterFeature>

    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            VStack {
                Text("\\(viewStore.count)")
                HStack {
                    Button("-") { viewStore.send(.decrement) }
                    Button("+") { viewStore.send(.increment) }
                }
            }
        }
    }
}
  • Store๋Š” ์ƒํƒœ(State)๋ฅผ ์ €์žฅํ•˜๊ณ , Reducer๋ฅผ ์‹คํ–‰ํ•˜๋Š” ์ค‘์•™ ์ €์žฅ์†Œ ์—ญํ• 
  • viewStore.send(.increment) → Action์„ Reducer๋กœ ์ „๋‹ฌํ•˜์—ฌ State๋ฅผ ๋ณ€๊ฒฝ

(5) ์•ฑ ์‹คํ–‰ ์‹œ Store ์ฃผ์ž… (์•ฑ์˜ ์ง„์ž…์ )

import SwiftUI
import ComposableArchitecture

@main
struct TCAExampleApp: App {
    var body: some Scene {
        WindowGroup {
            CounterView(
                store: Store(
                    initialState: CounterFeature.State(),  // ์ดˆ๊ธฐ ์ƒํƒœ
                    reducer: { CounterFeature() }  // Reducer ์ฃผ์ž…
                )
            )
        }
    }
}
  • ์•ฑ์ด ์‹คํ–‰๋  ๋•Œ Store๋ฅผ ์ƒ์„ฑํ•˜์—ฌ CounterView์— ์ฃผ์ž…

๐Ÿ”ฅ ์ „์ฒด ์ฝ”๋“œ

import SwiftUI
import ComposableArchitecture

// MARK: - Feature ์ •์˜ (Reducer)
struct CounterFeature: Reducer {
    struct State: Equatable {
        var count = 0 // ํ˜„์žฌ ์นด์šดํŠธ ๊ฐ’(count)์„ ์ €์žฅ
    }
    
    enum Action: Equatable {
        case increment // ์ˆซ์ž ์ฆ๊ฐ€
        case decrement // ์ˆซ์ž ๊ฐ์†Œ
    }

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .increment:
                state.count += 1
                return .none
            case .decrement:
                state.count -= 1
                return .none
            }
        }
    }
}
import SwiftUI
import ComposableArchitecture

// MARK: - SwiftUI View
struct CounterView: View {
    let store: StoreOf<CounterFeature>
    
    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            VStack(spacing: 20) {
                Text("์นด์šดํŠธ: \\(viewStore.count)")
                    .font(.largeTitle)
                
                HStack(spacing: 40) {
                    Button(action: { viewStore.send(.decrement) }) {
                        Text("–")
                            .font(.largeTitle)
                            .frame(width: 60, height: 60)
                            .background(Color.red.opacity(0.7))
                            .foregroundColor(.white)
                            .clipShape(Circle())
                    }
                    
                    Button(action: { viewStore.send(.increment) }) {
                        Text("+")
                            .font(.largeTitle)
                            .frame(width: 60, height: 60)
                            .background(Color.green.opacity(0.7))
                            .foregroundColor(.white)
                            .clipShape(Circle())
                    }
                }
            }
            .padding()
        }
    }
}
import SwiftUI
import ComposableArchitecture

@main
struct TCAExampleApp: App {
    var body: some Scene {
        WindowGroup {
            CounterView(
                store: Store(
                    initialState: CounterFeature.State(),  // ์ดˆ๊ธฐ ์ƒํƒœ
                    reducer: { CounterFeature() }  // Reducer ์ฃผ์ž…
                )
            )
        }
    }
}

๐Ÿง˜ ๋งˆ์น˜๋ฉฐ…

๋‹ค์Œ์—๋Š” ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์„ค๋ช…ํ•˜์ง€ ์•Š์€ Effect์— ๋Œ€ํ•ด ๋‹ค๋ค„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์ถ”๊ฐ€์ ์œผ๋กœ DependencyValues์™€ API ํ†ต์‹  ์˜ˆ์ œ๋„ ํฌํ•จํ•ด์„œ ์ž‘์„ฑํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.