-
SwiftUI View hierarchy DebuggingSwiftUI 2025. 12. 13. 07:42
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 SwiftUI View hiearchy Debugging에 대해 알아보겠습니다 🙋🏻

Xcode View Debugging
우선, Xcode에는 ViewDebug를 할 수 있게 View Hiearchy를 해볼 수 있어요.

많이 사용하고 계실 이 기능이죠.
물론, Reveal, Lookin, Rocketsim 등 요새는 다양하게 뷰 디버깅을 위한 툴이 나와서 더 잘 쓸 수 있지만 핵심 본질을 같긴합니다.
여기서 뭐가 중요하냐하면, 이 계층 구조가 어떤 컴포넌트로 이뤄졌는지를 볼때에요.

좌측 계층 구조 스택을 보면 여기서 SwiftUI로 그려진 ContentView가 호스팅되어서 나타나죠.
제가 코드에서 사실 ContentView에 ChildView라는 컴포넌트로 만들어서 코드를 넣었는데 여기서 찾을 수 있나요?
없습니다.
왜냐하면, 이 Xcode에서 계층을 표현하는 기준이 SwiftUI 뷰는 그대로 노출되지 않습니다.
결과적으로 UIKit 기반의 UIView 트리 형태로만 보이죠.
SwiftUI 뷰들은 플랫화 시켜서 지금 보이는것처럼 기본 아토믹한 컴포넌트 이름으로만 보이게 되는것이죠.
왜 그럼 SwiftUI View가 안보일까요?
Why can't we see the SwiftUI view?
SwiftUI View는 실제 렌더링 단계에서 아래처럼 동작해요.
1️⃣ SwiftUI VIew -> 내부 렌더링 엔진에서 layout + diff + render 수행
2️⃣ 결국 UIKit 기반으로 변환된 UIHostingView, UIHostingController 내부에서 UIView 트리로 그려짐
3️⃣ Xcode View Hiearchy Debugger는 UIKit의 계층 구조만 읽기에 SwiftUI VIew (Struct 기반 DSL)가 런타임에는 존재하지 않는 개념
그렇기에 보이지 않고 최종 결과물인 UIView만 노출되는것이죠.
그런데 이게 이런 상황에서는 막막할 수 있습니다.
"여기 화면에서 어디어디가 이상해요!"
복잡한 프로젝트에서 이런 QA가 들어온다면 일일히 코드를 찾아 그 화면의 이상한 어디어디를 찾아 해매는것보다 우리는 빠르게 그 특정 구현된 컴포넌트 파일을 찾아야 합니다.
이때, 지금까지 Xcode의 뷰 디버깅 기능이나 외부 뷰 디버깅 툴들이 도움이 되었어요.
런타임에서 그 화면에서 디버깅을 따서 원하는 컴포넌트만 누르면 되었으니까요.
물론, 최상위는 호스팅되고 있어서 거기서부터 SwiftUI로 코드가 그려졌으니 탐색해도 됩니다.
그렇지만 그것 또한 복잡한 뷰 구조를 가지고 있고 히스토리를 모른다면 시간이 걸리죠.
그래서 우리는 간단히 빠르게 컴포넌트 이름을 찾아야 합니다.
그럼 이제 왜 구조적으로 보이지 않는지를 알았으니 어떻게하면 계층 구조에서 빠르게 해당 문제가 되는 컴포넌트와 파일을 찾을 수 있을지 몇가지 방법을 살펴보겠습니다.
How can we handle it?
사실, 프리뷰로 본다, 브레이크 포인트를 찍어서 본다~ 이런것들은 여기에서의 해결책이 아닙니다.
다시 한번 상기시켜보면 여기서 우리의 문제는 하나 입니다.
런타임 도중에 잘못된 부분이 어떤 컴포넌트인지 역으로 코드를 찾아야 되는것!
그렇기에, 프리뷰나 브레이크 포인트나 그런 방식들은 파일과 컴포넌트 코드를 찾은 후에 액션이라 다른 상황입니다.
그리고 또 하나 많이들 사용하는 _printChanges() 메서드 활용이 있습니다.
예시를 볼까요?
import SwiftUI struct ContentView: View { var body: some View { #if DEBUG let _ = Self._printChanges() #endif VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) ChildView() Text("Hello, world!") } .padding() } } private struct ChildView: View { var body: some View { #if DEBUG let _ = Self._printChanges() #endif VStack { Text("ChildView") } } }이렇게 해당 컴포넌트들 구현 시 디버그 모드에서 뷰 변경사항을 찍도록 넣어두는것입니다.
그럼 이제 해당 화면에 진입하고 해당 뷰가 사용될때 이렇게 콘솔에 나타납니다.
ContentView: @self changed. ChildView: @self changed.그런데, 이것도 좋은 방법이 아니에요.
왜냐하면, 이것 조차 뷰 계층 구조를 확인할 수 있는것이 아니고 또한 LLDB 콘솔에 나타나기에 직관적이지도 않습니다.
그리고 복잡한 뷰의 경우 이런 하위 컴포넌트가 수백개가 된다 했을때 어떤것인지 찾을 수 없겠죠 🥲
그렇기에 이것도 의미 없습니다.
그럼 어떻게 해야할까요?
우선 제가 찾은 방법 하나는 근본으로 돌아가 생각해보는것이였습니다.
SwiftUI View가 Xcode 뷰 디버깅에 플랫하게 되는 이유와 안나오는 이유를 알았으니 그럼 UIView 환경으로 만들어주면 나오겠네?
라는 생각을 해봤어요.
적용한 예시 코드를 먼저 보고 얘기를 해볼까요?
import SwiftUI import UIKit struct DebugHostingView: UIViewRepresentable { let content: AnyView init<V: View>(_ view: V) { self.content = AnyView(view) } func makeUIView(context: Context) -> UIView { let hostingController = UIHostingController(rootView: content) hostingController.view.backgroundColor = .clear return hostingController.view } func updateUIView(_ uiView: UIView, context: Context) { } } extension View { @ViewBuilder func debugHostIfNeeded() -> some View { #if DEBUG DebugHostingView(self) #else self #endif } }이런 구현을 추가해볼께요.
간단히 호스팅을 위한 디버깅용 호스팅뷰를 만들고 편리하게 사용할 수 있게 View extension으로 만듭니다.
그럼, 이제 디버그 모드에서만 임팩트를 주겠죠?
사실 호스팅 자체를 다시 하는것이 좋은 패턴은 아니지만 우리는 이 목적이 디버그용도 이니까 실제 릴리즈 빌드에서는 해당 코드를 타지 않아 큰 문제가 되진 않아요.
그리고, 만약 성능적인 이슈나 이렇게까지 디버그하긴 싫다 하면 뷰 모디파이어 사용을 안하면 되니까요!
그럼 적용해볼까요?
import SwiftUI struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) ChildView() .debugHostIfNeeded() 🙋🏻 Text("Hello, world!") } .padding() } } private struct ChildView: View { var body: some View { VStack { Text("ChildView") } } }이렇게 모디파이어를 사용하면 됩니다.
만약, 모든 뷰에 알아서 적용되었으면 좋겠는데? 저 모디파이어 사용하는거 까먹으면 어떻게? 한다면 View를 채택한 여러분들만의 프로토콜을 만들어서 거기서 body의 view가 알아서 저걸 채택하도록 설정해두고 그 CustomView를 채택해 사용할 수 있어요.
import SwiftUI protocol CustomView: View { associatedtype Content: View @ViewBuilder var content: Content { get } } extension CustomView { var body: some View { content .debugHostIfNeeded() } } struct ChildView: CustomView { var content: some View { VStack { Text("ChildView") } } } struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) ChildView() Text("Hello, world!") } .padding() } }이런식으로 사용할수도 있죠.
그럼 이제, 아래와 같이 ChildView도 뷰 계층에서 컴포넌트 이름이 보이게 되고 문제가 생긴 컴포넌트의 코드를 빠르게 찾아 수정할 수 있겠죠?

사실 이 방법 외에 하나 더 있습니다.
뷰에 접근성 레이블을 추가하는건데요.
그럼 호스팅도 필요 없고 조금 더 간단하지만 여전히 이 뷰 계층 디버깅에서는 보이지 않고 accessibility로 실행시켜서 해당 레이블 identify를 가지고 찾는거라 오히려 더 귀찮은 작업이 될 수 있어요.
그렇지만, 그런것도 있다는거 😃
그냥 나는 뷰 디버깅 하는것도 귀찮고 그냥 화면에서 바로 보였으면 좋겠어!
라고 생각한다면, 동일하게 View extension해서 디버그 모드일 시 별도 뷰를 오버레이해서 이름을 추출해서 보여주는 방식도 있긴할것 같네요.
우선 여기까지가 우회해서 어떻게든 보여줄 수 있는 방법으로 먼저 공유해볼 수 있을것 같아요 🙇🏻
Conclusion
SwiftUI 많이들 활용하고 계실텐데 UIKit에서와 달리 불편사항들이 당연히 있기 마련입니다.
그럴때 결국 UIKit 세상을 끌어들여와야 하기에 둘의 차이를 잘 알고 적절히 혼용해서 사용할 수 있으면 좋을것 같아요!
'SwiftUI' 카테고리의 다른 글
SwiftUI Preview - Thunk (1) 2025.11.29 SwiftUI's diffing (5) 2025.09.27 Ensuring 60fps Animations in SwiftUI (GPU Rendering Optimization) (0) 2025.08.23 Bring Swift Charts to the third dimension (feat. WWDC 2025) (6) 2025.07.12 What's new in SwiftUI (feat. WWDC 2025) (3) 2025.06.11