iOS ๐ŸŽ/Architecture

[SwiftUI/TCA] TCA ๊ธฐ๋ณธ ๊ฐœ๋…์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ธฐ

fram 2023. 11. 21. 23:02

์ฐธ๊ณ  ์‚ฌ์ดํŠธ 

https://axiomatic-fuschia-666.notion.site/Chapter-1-Hello-TCA-70c56437681547d4b85cd1363a157356

 

Chapter 1. Hello, TCA

์•ˆ๋…•ํ•˜์„ธ์š”. Swift๋กœ Apple ์ƒํƒœ๊ณ„ ๊ฐœ๋ฐœ์— ์ผ์กฐํ•ด ์ฃผ์‹œ๋Š” ๊ฐœ๋ฐœ์ž ์—ฌ๋Ÿฌ๋ถ„, ๋ชจ๋‘ ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค!

axiomatic-fuschia-666.notion.site

https://www.pointfree.co/collections/tours/composable-architecture-1-0/ep243-tour-of-the-composable-architecture-1-0-the-basics

 

Episode #243: Tour of the Composable Architecture: The Basics

The Composable Architecture has reached a major milestone: version 1.0. To celebrate this release we are doing a fresh tour of the library so that folks can become comfortable building applications with it in its most modern form. We will start with a simp

www.pointfree.co

 

์ฐธ๊ณ  ์‚ฌ์ดํŠธ ๋ถ€ํ„ฐ ์ ๋Š” ์ด์œ ๋Š” TCA๋ฅผ ๋ฐฐ์šฐ๊ธฐ ๊ฐ€์žฅ ์ข‹์€ ์‚ฌ์ดํŠธ๋Š” ์•„๋ฌด๋ž˜๋„ TCA๋ฅผ ๋งŒ๋“  pointfree์™€ ์ด๋ฅผ ํ† ๋Œ€๋กœ ๋งŒ๋“  ์ „์ž์ฑ…์ด์ง€ ์•Š์„๊นŒ ์‹ถ์Šต๋‹ˆ๋‹ค! 

 

ViewModel

  • ๊ธฐ์กด UIKit์—์„œ์˜ ViewModel
    • RxSwift + ViewModel => ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ฑ…์ž„์ง. Reactive Programing
  • SwiftUI์—์„œ์˜ ViewModel 
    • SwiftUI์—์„œ๋Š” View ๋ ˆ๋ฒจ์—์„œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ๊ฐ€๋Šฅ
    • ViewModel์ด ์ˆ˜ํ–‰ํ•˜๋Š” ์ผ์„ View๊ฐ€ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋จ

 

TCA์˜ ํŠน์ง•

  • ๊ฐ’ ํƒ€์ž… ๊ธฐ๋ฐ˜
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด ์ƒํƒœ๋ฅผ ์ผ๊ด€์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํ‘œ์ค€ ์ œ์•ˆ
  • ์ตœ์†Œ ๊ธฐ๋Šฅ ๋‹จ์œ„ -> ๊ฐ์ฒด ๊ฐ„ ๊ฒฐํ•ฉ ๋ฐ ๋ถ„๋ฆฌ, ์žฌ์‚ฌ์šฉ์„ฑ ์‰ฌ์›Œ์ง
  • ํ…Œ์ŠคํŠธ ์œ ์—ฐ์„ฑ ํ™•๋ณด

 

SwiftUI์—์„œ์˜ MVVM

  • SwiftUI๋Š” ์–‘๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ง€์›ํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ
  • View๊ฐ€ ViewModel์˜ ์š”์ฒญ์„ ๋ฐ›์•„์˜จ ํ›„ ์ถ”๊ฐ€ ์ž‘์—…์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒ

 

Reducer ์ƒ์„ฑ

struct ProductDetailFeature: Reducer {
    struct State: Equatable {
    	// ์ƒ๋žต
    }
    
    enum Action: Equatable {
	    // ์ƒ๋žต
    }
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
                // ์ƒ๋žต
            }
        }
    }
}
  • State, Action, body property(ํ˜น์€ reduce(into:action:) ํ•จ์ˆ˜)๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•จ
  • ์ฃผ์–ด์ง„ Action์„ ๋ฐ”ํƒ•์œผ๋กœ ํ˜„์žฌ State๋ฅผ ์–ด๋–ป๊ฒŒ ๋‹ค์Œ ์ƒํƒœ๋กœ ๋ฐ”๊ฟ€ ๊ฒƒ์ธ์ง€ ์ •์˜

 

View์™€ Reducer ์—ฐ๊ฒฐ

struct ProductDetailView: View {
    let store: StoreOf<ProductDetailFeature>
    
    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            // ์ƒ๋žต
        }
    }
}
  • StoreOf<ProductDetailFeature>์˜ ์ด์ „ ๋ฐฉ์‹์€ Store<ProductDetailFeature.State, ProductDetailFeature.Action>

State

    struct State: Equatable {
        let productId = 4
        var product: Product?
        var isLike: Bool = false
    }
  • View์— ํ•„์š”ํ•œ ์ƒํƒœ๊ฐ’์ด๋ฉฐ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๊ฐ€์ง€๋Š” ๊ตฌ์กฐ์ฒด
  • ์ด ์ƒํƒœ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์ด๋ฅผ View์— ๋ฐ˜์˜
  • ๊ฐ’ํƒ€์ž…
  • ๋ณ€๋™์„ ์ถ”์ ํ•˜๊ณ  ์˜๋„ํ•˜์ง€ ์•Š์€ ๋ณ€๊ฒฝ์—์„œ ์ง€์ผœ๋‚ด์•ผ ํ•  ์ƒํƒœ
  • Single Source Of Truth

Action

    enum Action: Equatable {
        case likeButtonTapped
        case productResponse(Product)
        case getProductDetail
    }
  • ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ ‰์…˜์„ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•œ ํƒ€์ž…
  • ์‚ฌ์šฉ์ž๊ฐ€ View๋ฅผ ํ†ตํ•ด ์–ด๋–ค ์ž‘์—…์ด๋‚˜ ์•Œ๋ฆผ, ์ด๋ฒคํŠธ๋ฅผ ์ •์˜
  • Action์„ ๋„ค์ด๋ฐ ํ•  ๋•Œ์—๋Š” ์‚ฌ์šฉ์ž๊ฐ€ UI์—์„œ ์ˆ˜ํ–‰ํ•œ ์ž‘์—…์˜ ์ด๋ฆ„์„ ๋”ฐ์„œ ์ง“๋Š” ๊ฒƒ์ด ์ถ”์ฒœ๋จ

Effect

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .likeButtonTapped:
                state.isLike.toggle()
                return .none
            case .productResponse(let product):
                state.product = product
                return .none
            case .getProductDetail:
                return .run { [id = state.productId] send in
                    let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api/v1/products/\(id)")!)
                    do {
                        let product = try JSONDecoder().decode(Product.self, from: data)
                        print(product)
                        await send(.productResponse(product))
                    } catch {
                        // TODO: error handling
                    }
                }
            }
        }
    }
  • Action์ด State๋ฅผ ๋ณ€ํ™”์‹œํ‚ค๋Š” Effect๋ฅผ ๋ฐ˜ํ™˜
  • ์ƒํƒœ๋ฅผ ๋ณ€ํ™”์‹œํ‚ค๊ฑฐ๋‚˜ ๋„คํŠธ์›Œํฌ ํ†ต์‹  ๊ฐ™์€ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•จ
  • body ์†์„ฑ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ง์ ‘ ์ƒํƒœ ๋ณ€๊ฒฝ ๋˜๋Š” ํšจ๊ณผ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์Œ
    • body ์†์„ฑ์˜ ๋ฐ˜ํ™˜ ๊ฐ’์ด some Reducer์ธ opaque type์ด๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋ฆฌ๋“€์„œ์™€ ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ์Œ
    • ์•ฑ์˜ ๋ณต์žก๋„๊ฐ€ ์ฆ๊ฐ€ํ–ˆ์„ ๋•Œ ์šฉ์ด
  • ์ž‘์€ ๋‹จ์œ„์˜ ๋ฆฌ๋“€์„œ๋กœ ๊ตฌํ˜„ํ•  ๋•Œ ์šฉ์ด
    func reduce(into state: inout State, action: Action) -> Effect<Action> {
        switch action {
            // ์ƒ๋žต
        }
    }
  • ๊ธฐ๋ณธ์ ์ธ ๋ฐฉ๋ฒ•
  • ๋ฆฌ๋“€์„œ์˜ ๋กœ์ง์„ ์ง์ ‘ ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์— ๊ตฌํ˜„
  • ๋‹ค๋ฅธ ๋ฆฌ๋“€์„œ์™€์˜ ๊ฒฐํ•ฉ์ด ํ•„์š” ์—†๋Š” ๊ฒฝ์šฐ์— ์‚ฌ์šฉ

Equatable

  • WithViewStore์˜ ์ด๋‹ˆ์…œ๋ผ์ด์ €์—์„œ State๊ฐ€ Equatable ํ”„๋กœํ† ์ฝœ์„ ์ค€์ˆ˜ํ•˜๋„๋ก ์š”๊ตฌ
  • ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ๊ฐ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ด์ „ ์ƒํƒœ์™€ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋น„๊ตํ•˜๊ฒŒ ๋จ. ์ด ๊ณผ์ •์„ ํ†ตํ•ด ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Œ
  • ๋งŒ์•ฝ ์ฐธ์กฐ ํƒ€์ž…์˜ State์˜ ๊ฒฝ์šฐ ๊ฐ์ฒด ๋‚ด๋ถ€์˜ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์ด ๋ณ€๊ฒฝ์„ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ๋ณ€ํ™”๋ฅผ ์ธ์ง€ํ•˜์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ์Œ
struct Product: Codable, Identifiable, Equatable {
    let id: Int
    let title: String
    let price: Int
    let description: String
    let images: [String]
    let creationAt, updatedAt: String
    let category: Category
    
    static func == (lhs: Product, rhs: Product) -> Bool {
        lhs.id == rhs.id
    }
}
  • State์˜ ๋ณ€ํ™”๋ฅผ ์•Œ๊ธฐ ์œ„ํ•ด Equatable์„ ์ค€์ˆ˜ํ•ด์•ผ ํ•œ๋‹ค.
  • ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๊ฐ„์†Œํ™”ํ•˜๊ณ  ๋ฒ„๊ทธ๋ฅผ ์ค„์ด๋Š” ๋ฐฉ์‹

 

Effect & Side Effect

  • ์ž‘์—…์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์˜๋ฏธ
    • ์ž‘์—…์— ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ Effect
    • ์ž‘์—…์ด ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์‹คํŒจ๋ฅผ ํ•˜๋Š” ๊ฒฝ์šฐ Side Effect 
  • effect์™€ side effect์— ๋”ฐ๋ผ ๋‹ค์‹œ Action์„ ์„ ํƒํ•ด Sate๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ์ด State๋Š” ๋‹ค์‹œ ๋ทฐ์— ์˜ํ–ฅ์„ ์ฃผ๊ฒŒ ๋˜๋Š” ๋‹จ๋ฐฉํ–ฅ ํ๋ฆ„์„ ๊ฐ€์ง

https://pixabay.com/ko/photos/%EB%B9%B5-%ED%86%A0%EC%8A%A4%ED%84%B0%EC%97%90-%EC%9D%8C%EC%8B%9D-%ED%86%A0%EC%8A%A4%ED%8A%B8-1077984/