ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [포케덱스] - TCA 관련 문제: Perceptible state was accessed but is not being tracked.
    프로젝트/포케덱스 2025. 9. 1. 20:20
    728x90
    반응형

     

    오늘은 런타임시에 발생하는 에러에 대한 내용에 대해 트러블 슈팅을 하겠다.

     

    포케덱스에서 이런 에러가 나기 시작했다.

    TCA를 사용하면서 생긴 문제다.

     

    문제 파악

     

    이런 에러는 다음과 같은 상황에 뜬다.

     

    1. 빌드 후 4개의 보라에러가 뜸

    2. 포켓몬 상세보기를 누르면 4개가 추가됨

    3. 나왔다 상세보기를 계속 들어갈 떄마다 3~4개씩 추가됨

     

    생기는 상황은 이렇고 에러 메세지는 다음과 같다.

    Perceptible state was accessed but is not being tracked. 
    Track changes to state by wrapping your view in a 'WithPerceptionTracking' view.
    This must also be done for any escaping, trailing closures, such as 'GeometryReader',
    LazyVStack (and all lazy views), navigation APIs ('sheet', 'popover', 'fullScreenCover',
    etc.), and others.

     

    해석과 요약을 해보자면

    상태를 읽었지만 추적되고 있지 않다.
    또한뷰를 WithPerceptionTracking으로 감싸서 상태 변화를 추적해야 한다.

     

    이렇다고는 하는데.. 이게 무슨 말인지 왜 필요한지 모르겠다.

     

    문서를 읽어보니 다음과 같았다.

    TCA 1.2+ 버전부터 도입된 Perception 시스템ViewStore의 상태 변경을 효율적으로 감지하기 위해 도입된 구조다.

    그런데 SwiftUI의 일부 뷰(특히 LazyVStack, sheet, GeometryReader 등)는 내부적으로 상태를 lazy 또는 비동기로 읽기 때문에, TCA가 상태 추적을 놓치게 된다.

     

    그럴 경우, Perception 시스템이 다음과 같은 경고를 띄운다.

     

    "상태는 읽었는데 트래킹 안하고 있음”

     

    그럴 경우 LazyView를 WithPerceptionTracking로 감싸면 된다고 한다. 

     

    하지만 그전에 Perception시스템이라는 말이 나왔는데, 이게 먼지 알아보자.

     

    Perception 시스템이란?

     

    사전적으로 Preception은 지각이라는 뜻이지만, TCA에서의 개념은

    어떤 View가 어떤 상태를 읽고 있는지 추적해서, 그 상태가 바뀌었을 때만 그 View를 다시 그리게 해주는 시스템”이다.

     

    도대체 이게 무슨 말인지 알아보기 위해 예시를 들어보겠다.

     

    SwiftUI에서는 상태를 아래처럼 선언하면, 

    @State var count = 0

    View자동으로 상태 변화를 감지하고 리렌더링 된다.

    왜냐면 @State, @ObservedObject, @EnvironmentObject 같은 Property Wrapper들이 SwiftUI 내부적으로 변화 감지를 등록하기 때문이다.

     

    하지만 TCA를 쓰게되면 프로퍼티래퍼를 쓰지 않고 상태 관리를 하게 되는데, 나같은 경우는 ViewStore<State, Action>을 통해 상태를 바인딩한다.

     

    이 코드를 봐보자

    /// 리스트 뷰
    @ViewBuilder
    private func pokemonListView(viewStore: MainStore) -> some View {
    
        let columns = Array(repeating: GridItem(.flexible()), count: 3)
    
        ScrollView(.vertical, showsIndicators: true) {
            LazyVGrid(columns: columns) {
                ForEach(
                    store.scope(
                        state: \.pokemonCellStates,
                        action: \.cell.pokemonCellFeature
                    )
                ) { cellStore in
                    WithViewStore(cellStore, observe: { $0 }) { cellViewStore in
                        PokemonCellView(store: cellStore)
                        if viewStore.isLastPokemonReached,
                            viewStore.pokemonCellStates.last?.id == cellViewStore.id {
                            ProgressView()
                                .onAppear {
                                    viewStore.send(.view(.scrollUpList))
                                }
                        }
                    }
                }
            }
            .padding()
        }
    }

     

    외부 API 요청을 통해 불러온 데이터를 상태로 저장하고, 해당 상태의 리스트(포켓몬 리스트)를 ForEach로 출력한다.

     

    여기서 중요한 점은 viewStore의 상태를 읽는 시점에 TCA는 내부적으로 이 뷰는 해당 리스트 값의 상태 구독하고 있다“고 등록해둔다. 이후 상태가 바뀌게 되면, TCA가 이걸 감지해서 해당 뷰만 리렌더링하게 유도한다.

     

    그럼 문제가 되는 시점은 뭘까?

     

    SwiftUI는 lazy키워드를 쓰거나 concurrency로 상태를 지연하거나, 비동기로 읽는 구조가 있다.

    이런 경우에는 상태를 읽는 시점이 바로 body내부에서 일어나는게 아니라 이후에 실행될 클로저 안에서 읽혀지게 된다.

     

    그러면 TCA입장에서는 "viewStore의 상태를 읽었는데, 언제 읽어야하는지 모르겠다." 

    즉, 추적 시스템이 이 상태를 감지하지 못한다는 뜻이다.

     

     

     

    그럼 해결책은?

     

    위에서 언급했던 대로 lazy나 비동기로 동작하는 데이터를 표시하는 컴포넌트에서 WithPerceptionTracking으로 해당 컴포넌트를 래핑하면 된다.

     

    이렇게 래핑하면 

    WithPerceptionTracking {
        // 이 안에서 일어나는 상태 접근은 모두 추적할게!
    }

     

    해당 스코프 안에 viewStore.state를 읽을수 있도록 추적할 준비를 하게 된다.

     

    나같은 경우는 LazyVGrid를 사용해서 포켓몬 리스트를 표현하고 있기 때문에 이곳에 삽입해줬다. 

     

    근데 없어지지 않았다..!

     

    문제를 찾아보니 이 부분에서 문제가 나타났다...

    /// 타이틀 라벨
        private func titleLabel(viewStore: MainStore) -> some View {
            Text(viewStore.regionTitle + "도감")
                .bold()
                .font(.title)
                .padding([.leading, .bottom])
        }

     

    여기서 두가지 의문이 생겼다.

    1. 왜 titleLabel은 lazy로 생성되지 않는데 이런 문제가 발생할까?

    2. 왜 내 코드에서는 LazyVGrid를 사용하는데 왜 WithPerceptionTracking로 감싸지 않아도 문제가 안될까?

     

    viewStore.regionTitle에 접근하고 있지만, SwiftUI는 이 뷰가 어떤 값에 의존하는지를 제대로 추론하지 못할 수 있다. 특히 VStack 안의 titleLabel 같은 작은 컴포넌트는 SwiftUI의 최적화 때문에 감시가 잘 안 되는 경우가 있다고 한다..

     

    그래서 다음과 같이 해당 뷰만 WithPerceptionTracking으로 감싸면 해결되는 것이 맞다고 하는데, 솔직히 이게 맞는 답인지 잘 모르겠다.

     

     

    그리고 LazyVGrid안에 있는 리스트는 감싸지 않아도 되는 이유는 이미 셀자체를 WithViewStore로 감싸고 있어서 그렇다고 한다. 즉, 셀 내부 뷰에서의 상태 접근도 안전하게 추적되고, SwiftUI는 해당 셀의 변화가 생길 떄만 리렌더링을 하게 된다고 한다.

     

     

    일단 이렇게 해서 해결하기는 했는데,,, 난 솔직히 이해가 가지는 않는다.

    이 부분은 조금 더 파보면서 내용을 보강할 필요가 있을 것 같다.

     

Designed by Tistory.