카테고리 없음

SwiftUI Instruments 하기

2료일 2025. 10. 30. 10:13

왜 SwiftUI 성능 디버깅이 어려울까?

UIKit에서는 Time Profiler로 백트레이스 찍어보면 viewDidLoad에서 뭘 너무 많이 했나, tableView(_:cellForRowAt:)에서 뭘 건드리고 있나 단계별로 명령들을 볼 수 있었어요

 

그런데 SwiftUI로 넘어오면서 상황이 달라졌어요. 스크롤이 버벅대는데, Time Profiler를 열어보면 뭔가 알기가 어려워요. 

선언형 패러다임의 양날의 검일 수도 있는데 이전과 달리 무엇에 집중하다 보니 어떻게는 뛰어넘어 그 어떻게를 보기 힘든거죠!!! 

그래서 이번 WWDC26에서는 Instruments를 업데이트 해줬습니다:)(사실 원래도 있긴 했어요)

먼저, SwiftUI의 렌더링 루프를 이해해야 해요

요즘 아이폰은 대부분 120fps이기에 초당 120번 화면을 갱신해요.  대량 8.xxms마다 새로운 프레임을 그려야한다는 거죠?

앱은 이 주기마다 깨어나서:

  1. 사용자 이벤트 처리하고
  2. UI 업데이트 준비해서
  3. Frame Deadline 직후에 렌더 요청을 해요

여기서 말하는 "렌더"는 GPU가 처리할 수 있는 형태로 변환하고 준비하는 작업이라고 이해하면 돼요 👍

그 후 다음 Frame Deadline에 화면에 실제로 출력되는 거고요.

그럼 프레임 드랍(Hitch)은 언제 생길까?

1. 긴 View Body 업데이트

만약 View의 body가 엄청 복잡해서 연산이 오래 걸리면요? Frame Deadline을 못 맞추게 돼요.

그러면 렌더 요청이 다음 Frame Deadline 이후로 밀리고, 화면에 보이는 건 또 그 다음이 되는 거죠. 전체적으로 한 칸씩 밀리는 거예요.

 

2. 너무 많은 업데이트

개별 업데이트는 짧아도 한 프레임에 너무 많은 바디가 모이면 총합이 데드라인을 초과하여 hitch가 생길 수 있어요

Instruments SwiftUI

트랙명 기능 요약 진단 예시
Update Groups SwiftUI가 실제 작업 중인지를 확인하는 용도 CPU spike인데 UpdateGroups가 비었다면 SwiftUI 외부 문제!
Long View Body Updates View의 body 연산이 오래 걸리는 경우를 추적 스크롤이나 리스트에서 특정 뷰가 느린 이유가 뭘까?
Long Representable Updates UIViewControllerRepresentable이나 UIViewRepresentable 같은 UIKit 브릿지 레이어의 성능 문제를 검출 커스텀 UIView/VC 연계 성능 이슈 분리 
Other Long Updates 기타 SwiftUI에서 오래 걸리는 내부 작업 포착 레이아웃/애니메이션 등 다른 장시간 작업 확인 

 

SwiftUI는 죄가 없다

자 이 경우만 예시를 들어서 좀 더 설명해볼게요

struct ImageRow: View {
    let imageURL: URL
    
    var body: some View {
        AsyncImage(url: imageURL) { image in
            image
        } placeholder: {
            ProgressView()
        }
        .onAppear {
            ImageCache.shared.prefetch(imageURL)
        }
    }
}

이런 경우가 Body의 연산은 없는데 onAppear에서 호출한 외부 캐시 로직 때문에 CPU사용률이 올라가는 경우에요

 

AttributeGraph

(이건 영상에서의 예시인데 좋은 자료라서 가져왔어요 간단한 토글 뷰입니다!)

struct ToggleView: View {
    @State private var isOn = false
    
    var body: some View {
        VStack {
            Toggle("설정", isOn: $isOn)
            if isOn {
                Text("켜졌어요!")
                    .foregroundStyle(isOn ? .green : .red)
            }
        }
    }
}

버튼을 탭하는 순간, isOn이 true로 바뀌죠. 이때 SwiftUI는 즉시 화면을 다시 그리지 않아요.

  1. 대신 "이 값이 바뀌었다"는 신호를 만들어요(위에서 보면 State Change Signal 보이시죠??!!!)
  2. SwiftUI System이 트랜잭션을 생성해 "이 값이 바꼈어!!"라는 것을 표시해요. 중요한 건, 이 Transaction은 즉시 실행되지 않아요. 다음 프레임 업데이트 때까지 대기하고 있죠.
    왜냐고요?
    만약 여러 State가 연달아 바뀌면, 그걸 하나의 Transaction으로 모아서 한 번에 처리하는 게 효율적이니까요.

  1. 다음 프레임에서, SwiftUI는 대기 중이던 Transaction을 꺼내서 실행해요
  2. 바뀐 값에 종속된 모든 View Body가 "outdated" 플래그가 붙어요. 그런데 이때 바로 위에서 부터 AttributeGraph에 의해서 누가 isOn에 의존하고 있는가를 따라가 내려가는 거죠
  3. 여기서는 environment도 체크해요. ex) foregorund, background
  4. outdated 체인이 완성되면, 이제 실제로 재계산이 시작돼요 . 그리고 나서 그후에 GPU 렌더링을 통해 화면에 표시되는거죠

결국 바디는 가볍게, 업데이트도 꼭 필요할 때만 ‼️

이게 결국 hitch를 줄여 데드라인을 지키도록 하는 방법이겟네요