꿈돌이랜드

TCA 아키텍처에서 IdentifiedArray를 사용해야 하는 이유 본문

Programming/iOS

TCA 아키텍처에서 IdentifiedArray를 사용해야 하는 이유

loinsir 2025. 3. 21. 01:52
반응형

Modern SwiftUI: Identified arrays

이 포스팅은 위 링크를 기반으로 작성했습니다.

IdentifiedArray가 필요한 이유

  • 보통 SwiftUI에서 리스트 뷰를 구현할 때 ForEach 구문으로 리스트 데이터를 순차적으로 표시하는 케이스가 많습니다.

  • 그런 경우 보통 다음과 같이 구현하게 됩니다.

      struct StandupsList: View {
        @State var standups: [Standup] = […]
    
        var body: some View {
          List {
            ForEach(standups) { standup in
              StandupRow(standup: standup)
            }
          }
        }
      }
  • 그런데 보통 뷰 레이어에서 다시 데이터를 조작해서 상태를 반영시켜주어야 하는 경우가 많은데, 그런 경우 다음과 같은 경우가 필연적으로 발생하게 됩니다.

      func deleteStandup(id: Standup.ID) {
        guard let index = standups.firstIndex(where: { $0.id == id })
        else { return }
    
        standups.remove(at: index)
      }
  • TCA의 경우, Reducer 쪽에 다음처럼 같이 작성될 수 있습니다.

      struct StandupsList: Reducer {
          struct State: Equatable {
              var standups: [Standup] = []
          }
    
          enum Action {
              case deleteStandup(id: Standup.ID)
          }
    
          func reduce(into state: inout State, action: Action) -> Effect<Action> {
              switch action {
              case let .deleteStandup(id):
                  if let index = state.standups.firstIndex(where: { $0.id == id }) {
                      state.standups.remove(at: index)
                  }
                  return .none
              }
          }
      }
  • 문제는 firstIndex에 있습니다, firstIndex는 해당하는 요소를 찾기 위해 배열을 처음부터 스캔해야 하기 때문에(시간복잡도 O(n)) 잠재적인 성능 문제가 발생하는 지점이 되게 됩니다.

  • 문서에는 다음과 같은 예시도 들어서, api 클라이언트를 통해 배열을 수정하게 되면 크래시가 나는 경우도 보여줍니다.

      func deleteStandup(id: Standup.ID) async throws {
        guard let index = standups.firstIndex(where: { $0.id == id })
        else { return }
    
        try await apiClient.delete(id: id)
        standups.remove(at: index)
      }

IdentifiedArray의 특징과 사용법

  • IdentifiedArray는 기존의 원시 배열을 대체하는 컬렉션 타입입니다.

  • ID로 요소를 안전하고 효율적으로 읽고 수정할 수 있는 메서드를 제공합니다.

  • IdentifiedArray에 들어갈 요소 타입의 id는 마치 identifiable 프로토콜 처럼 hashable을 만족해야합니다.

  • 다음과 같이 선언하여 사용할 수 있습니다.

      import IdentifiedCollections
    
      var standups: IdentifiedArrayOf<Standup> = []
  • 가장 큰 장점은 특정 요소를 찾기 위해 firstIndex가 아닌 id를 사용해 시간복잡도 O(1)에 접근이 가능합니다.

      standups[id: standup.id] = standup
  • Reducer도 다음과 같이 수정될 수 있습니다.

      struct StandupsList: Reducer {
          struct State: Equatable {
              var standups: IdentifiedArrayOf<Standup> = []
          }
    
          enum Action {
              case deleteStandup(id: Standup.ID)
          }
    
          func reduce(into state: inout State, action: Action) -> Effect<Action> {
              switch action {
              case let .deleteStandup(id):
                  state.standups.remove(id: id)
                  return .none
              }
          }
      }

성능?

  • Github README에서는 성능이 Swift의 OrderedDictionary 성능과 일치한다고 주장합니다😃

어떻게 구현되어 있나?

  • 내부적으로 Swift의 OrderedDictionary를 사용하여 래핑한 것을 살펴볼수 있습니다.
    • OrderedDictionary는 순서를 보장하는 dictionary를 구현한 것으로 swift 오픈소스인 OrderedCollections에 내장되어 있습니다.
  • Github(Link)
public struct IdentifiedArray<ID: Hashable, Element> {
  public let id: KeyPath<Element, ID>

  // NB: Captures identity access. Direct access to `Identifiable`'s `.id` property is faster than
  //     key path access.
  @usableFromInline
  var _id: (Element) -> ID

  @usableFromInline
  var _dictionary: OrderedDictionary<ID, Element>

  /// A read-only collection view for the elements contained in this array, as an `Array`.
  ///
  /// - Complexity: O(1)
  @inlinable
  @inline(__always)
  public var elements: [Element] { self._dictionary.values.elements }

 ...
반응형