반응형

SwiftUI에서 많이 하는 실수 - List 셀 내부에서 상태 변경/로직을 수행해 무한 렌더링이 발생하는 실수

 

SwiftUI의 List/ForEach는 각 셀을 필요할 때마다 재사용/재생성합니다.
셀 View의 body에서 상태를 변경하거나 네트워크 호출을 수행하면,
렌더링 → 상태 변경 → 다시 렌더링 → … 무한 루프가 발생할 수 있습니다.


1. 문제 원인

  • 셀 View 내부에서 onAppear/onChange로 상태를 직접 바꿈
  • View 생성 자체가 부수효과를 일으키도록 설계
  • “셀 당 초기화가 한 번만 될 것”이라고 가정

2. 나타나는 증상

  • 스크롤만 살짝 해도 API가 계속 호출됨
  • 로그를 찍어보면 같은 셀에서 onAppear가 여러 번 실행
  • CPU 사용량이 높고, 스크롤이 심하게 버벅거림

3. 잘못된 코드 예시

struct CardRow: View {
    let card: Card
    @ObservedObject var viewModel: CardsViewModel

    var body: some View {
        HStack {
            Text(card.title)
        }
        .onAppear {
            // ❌ 셀이 보일 때마다 통계를 올리는 API 호출
            viewModel.trackImpression(for: card)
        }
    }
}

List 쪽:

List(viewModel.cards) { card in
    CardRow(card: card, viewModel: viewModel)
}

스크롤할 때마다 같은 셀에 대해 trackImpression이 여러 번 호출됩니다.


4. 올바른 코드 예시

4-1. 셀 외부에서 제어하거나, 한 번만 실행되도록 방어

final class CardsViewModel: ObservableObject {
    private var trackedIDs: Set<Card.ID> = []

    func trackImpressionIfNeeded(for card: Card) {
        guard !trackedIDs.contains(card.id) else { return }
        trackedIDs.insert(card.id)
        // 실제 트래킹 호출
    }
}

struct CardRow: View {
    let card: Card
    @ObservedObject var viewModel: CardsViewModel

    var body: some View {
        HStack {
            Text(card.title)
        }
        .onAppear {
            viewModel.trackImpressionIfNeeded(for: card) // ✅ id 기반 방어
        }
    }
}

4-2. 단순 UI 상태만 셀에 두고, 비즈니스 로직은 밖으로

셀 내부에서 네트워크 호출/DB 업데이트를 하지 말고,

ViewModel/UseCase에 이벤트만 보내도록 설계합니다.


5. 정리 및 팁

  • 셀 View는 최대한 순수 UI에 가깝게 유지하는 것이 좋습니다.
  • onAppear/onDisappear에서 비즈니스 로직을 호출해야 한다면,
    “같은 셀에 대해 여러 번 불려도 안전한지”를 항상 고려해야 합니다.
  • “한 번만 실행되어야 하는 로직”은 ViewModel 쪽에서 id 기반으로 방어하는 패턴이 필수입니다.
반응형
Posted by 까칠코더
,