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


UIKit에서는 Time Profiler로 백트레이스 찍어보면 viewDidLoad에서 뭘 너무 많이 했나, tableView(_:cellForRowAt:)에서 뭘 건드리고 있나 단계별로 명령들을 볼 수 있었어요
그런데 SwiftUI로 넘어오면서 상황이 달라졌어요. 스크롤이 버벅대는데, Time Profiler를 열어보면 뭔가 알기가 어려워요.
선언형 패러다임의 양날의 검일 수도 있는데 이전과 달리 무엇에 집중하다 보니 어떻게는 뛰어넘어 그 어떻게를 보기 힘든거죠!!!
그래서 이번 WWDC26에서는 Instruments를 업데이트 해줬습니다:)(사실 원래도 있긴 했어요)
먼저, SwiftUI의 렌더링 루프를 이해해야 해요

요즘 아이폰은 대부분 120fps이기에 초당 120번 화면을 갱신해요. 대량 8.xxms마다 새로운 프레임을 그려야한다는 거죠?
앱은 이 주기마다 깨어나서:
- 사용자 이벤트 처리하고
- UI 업데이트 준비해서
- 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에서 오래 걸리는 내부 작업 포착 | 레이아웃/애니메이션 등 다른 장시간 작업 확인 |

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

- 다음 프레임에서, SwiftUI는 대기 중이던 Transaction을 꺼내서 실행해요
- 바뀐 값에 종속된 모든 View Body가 "outdated" 플래그가 붙어요. 그런데 이때 바로 위에서 부터 AttributeGraph에 의해서 누가 isOn에 의존하고 있는가를 따라가 내려가는 거죠
- 여기서는 environment도 체크해요. ex) foregorund, background
- outdated 체인이 완성되면, 이제 실제로 재계산이 시작돼요 . 그리고 나서 그후에 GPU 렌더링을 통해 화면에 표시되는거죠
결국 바디는 가볍게, 업데이트도 꼭 필요할 때만 ‼️
이게 결국 hitch를 줄여 데드라인을 지키도록 하는 방법이겟네요
