본문 바로가기

iOS/SwiftUI

SwiftUI View Hierarchy & Navigation Hierarchy와 Environment의 관계

 

 

 🔍 서론

Environment를 사용하던 중 분명히 상위 계층에서 Observable 클래스를 environment로 등록했음에도 하위 계층에서 등록한 environment 클래스를 찾을 수 없는 문제가 발생하여 Environment의 내부 동작 원리에 대해 자세히 알아보던 중 Environment가 제대로 동작하지 않았던 이유와, 뷰 계층 구조와 내비게이션 계층 구조에 관한 내용들을 자세히 알게 되어서 이렇게 글을 남기려고 한다.

 

먼저, 뷰 계층 구조와 내비게이션 계층 구조에 대해 정리하고 Environment와 연결 지어서 내용을 정리해 보겠다.

 

 

 

 View Hierarchy vs Navigation Hierarchy

먼저, 뷰 계층 구조는 말 그대로 뷰의 계층을 얘기하며, 일반적으로 말하는 상위뷰 또는 하위뷰는 A 뷰의 body 내에 B 뷰가 있을 경우 A 뷰를 상위 뷰라고 지칭하고 B를 하위 뷰라고 지칭한다.

 

내비게이션 계층 구조는 NavigationStack 또는 NavigationView 내에서 발생하는 계층을 말한다. A 뷰에서 B 뷰로 navigate 했을 때, 내비게이션 계층 구조의 관점에서 A 뷰를 상위 뷰, B 뷰를 하위 뷰라고 볼 수 있다.

 

하나의 프로젝트에서 계층 구조의 관점에 따라 표현되는 계층은 완전히 다르게 나타난다. 뷰 계층 구조의 관점에서 보는 것과, 내비게이션 계층 구조의 관점에서 보는 것은 완전히 다르다.

 

아래의 코드로 예시를 들어보겠다.

@main
struct BlogExampleTestApp: App {
    init() {
        print("App init")
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    init() {
        print("ContentView")
    }
    
    var body: some View {
     	View1()
    }
}

struct View1: View {
    var body: some View {
        NavigationStack {
            NavigationLink {
                View2()
            } label: {
                Text("View2로 이동")
            }
        }
    }
}

struct View2: View {
    var body: some View {
        NavigationLink {
            View3()
        } label: {
            Text("View3로 이동")
        }
    }
}

struct View3: View {
    var body: some View {
        Text("View3")
    }
}

 

먼저, 이 코드를 뷰 계층 구조의 관점에서 계층을 표현해 보면 다음과 같다.

 

< 뷰 계층 구조의 관점 >

 

ContentView

⬇️

View1

⬇️

---(NavigationStack)---

|        View2 - View3        |

-------------------------

 

 

 

이번에는 내비게이션 계층 구조의 관점에서 계층을 표현해 보겠다.

 

< 내비게이션 계층 구조의 관점 >

 

View2

⬇️

View3

 

 

 

차이점을 보면, 뷰 계층 구조에서는 View2와 View3가 같은 계층으로 표현되고 있지만, 내비게이션 계층 구조의 관점에서는 View2가 View3의 상위 계층으로 표현되고 있다.

 

뷰 계층 구조의 NavigationStack 안에서 navigate 되는 뷰들은 같은 계층으로 볼 수 있으며, View2와 View3는 NavigationStack안에 감싸서 표현했다.

 

내비게이션 계층 구조에서는 NavigationStack 안에 있는 부분만 신경 쓰면 되므로, ContentView와 View1은 표현하지 않았다.

 

 

 

 

 View Hierarchy와 Environment

결론부터 말하면, Environment는 뷰 계층 구조를 따른다. 이 말은 즉슨, Environment를 사용할 때 Environment가 내비게이션 계층 구조에 영향을 받는 게 아니라 뷰 계층 구조에 영향을 받는다는 뜻이다.

 

이해를 돕기 위해 위에서 사용한 예시에 Environment를 적용해 보겠다.

 

@Observable
class TestViewModel {
    
}

struct View1: View {
    @State private var viewModel = TestViewModel()
    
    var body: some View {
        NavigationStack {
            NavigationLink {
                View2()
                    .environment(viewModel)
            } label: {
                Text("View2로 이동")
            }
        }
    }
}

struct View2: View {
    @Environment(TestViewModel.self) var viewModel
    
    var body: some View {
        NavigationLink {
            View3()
        } label: {
            Text("View3로 이동")
        }
    }
}

struct View3: View {
    @Environment(TestViewModel.self) var viewModel
    
    var body: some View {
        Text("View3")
    }
}

 

Environment로 등록할 Observable 클래스를 하나 선언해 주고, View2에 environment 키워드를 사용하여 TestViewModel을 Environment로 등록하였다.

 

이 경우 보통 View2에 Environment를 등록하였으니까 View3로 navigate 되었을 때 View3에서도 등록한 TestViewModel을 사용할 수 있을 거라고 생각한다.

 

하지만 결과는 아래와 같다.

SwiftUICore/Environment+Objects.swift:36: Fatal error: No Observable object of type TestViewModel found. A View.environmentObject(_:) for TestViewModel may be missing as an ancestor of this view.

 

View3에서 TestViewModel을 찾을 수 없다는 오류가 뜬다.

 

왜 이런 일이 생긴 것일까?

 

 

 

 

 

그 이유는 바로 우리가 위의 코드를 내비게이션 계층 구조의 관점에서 바라보았기 때문이다.

 

내비게이션 계층 구조에서 보면, View3가 View2의 하위 뷰이므로, View2에 등록한 Environment 클래스를 당연히 View3에서도 사용할 수 있을 것이라고 생각한다.

 

하지만, 위에서 말했다시피, Environment는 네비게이션 계층 구조를 따르는 것이 아닌 뷰 계층 구조를 따른다.

 

뷰 계층 구조에서 보면 View3는 View2의 하위 뷰가 아니다. 같은 계층상의 뷰인 것이다.

 

따라서 Environment는 하위뷰에서만 사용할 수 있는데, View3는 View2의 하위뷰가 아닌 같은 계층의 뷰이므로 View3에서 사용이 불가능한 것이다.

 

 

 

 

그럼 이번에는 Environment를 상위 계층에 적용시켜 보겠다.

struct View1: View {
    @State private var viewModel = TestViewModel()
    
    var body: some View {
        NavigationStack {
            NavigationLink {
                View2()
            } label: {
                Text("View2로 이동")
            }
        }
        .environment(viewModel) // environment를 NavigationStack에 적용
    }
}

 

이전의 코드에서는 View2에 Environment를 등록하였지만, 이번에는 NavigationStack에 Environment를 등록하였다.

 

이렇게 실행하면 View3에서도 TestViewModel을 인식할 수 있다.

 

이 경우에서 제대로 동작하는 이유는 아까도 말했지만 뷰 계층 구조의 관점에서 NavigationStack 내에서 navigate 되는 뷰들은 다 같은 계층인데, 이 계층 바로 위에 Environment를 등록하면 NavigationStack 내의 모든 뷰에서 사용이 가능하다.

 

 

그러면, 이번에는 좀 더 복잡한 경우를 살펴보겠다.

struct ContentView: View {
    @State private var viewModel = TestViewModel()
    
    init() {
        print("ContentView")
    }
    
    var body: some View {
        NavigationStack {
            View1()
                .environment(viewModel)
        }
    }
}

struct View1: View {
    @Environment(TestViewModel.self) var viewModel
    
    var body: some View {
        View2()
    }
}

struct View2: View {
    @Environment(TestViewModel.self) var viewModel
    
    var body: some View {
        View3()
    }
}

struct View3: View {
    @Environment(TestViewModel.self) var viewModel
    
    var body: some View {
        NavigationLink {
            View4()
        } label: {
            Text("View4로 이동")
        }
    }
}

struct View4: View {
    @Environment(TestViewModel.self) var viewModel
    
    var body: some View {
        Text("View4")
    }
}

 

위의 코드의 뷰 계층 구조를 표현해보겠다.

 

ContentView

⬇️

----(NavigationStack)----

        View1     View4        

⬇️               

View2               

⬇️               

View3               

 

현재 NavigationStack 안에서 View1, View2, View3가 차례로 뷰 계층 구조를 형성하고 있다. 그런데 View3에서 View4로 navigate 하는 상태이다.

 

여기서 신기한 점은 이 코드를 내비게이션 계층 구조의 관점으로 봤을 때이다.

 

{ View1 -> View2 -> View3 }

⬇️

View4

 

내비게이션 계층의 관점에서는 View1, View2, View3가 한 덩어리로 묶여서 한 계층을 형성한다.

 

아무튼 위의 코드에서 View1에 Environment를 등록하였으므로, 뷰 계층 구조상 하위뷰에 있는 View2, View3는 모두 등록한 Environment를 사용할 수 있다.

 

 

 

이렇게 뷰 계층 구조와 내비게이션 계층 구조 그리고 Environment와의 관계에 대해 알아보았다.

 

 이 내용들을 깊이 있게 숙지하면 Environment를 어디에 등록해야 되는지와, Environment가 의도한 방식으로 동작하지 않을 경우 빠르게 원인을 파악하고 문제를 해결할 수 있을 것이다.