🔍 서론
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가 의도한 방식으로 동작하지 않을 경우 빠르게 원인을 파악하고 문제를 해결할 수 있을 것이다.
'iOS > SwiftUI' 카테고리의 다른 글
SwiftUI NavigationStack을 활용한 상위 뷰에서 루트 뷰 이동 (Pop to Root) (0) | 2024.08.27 |
---|---|
SwiftUI @Bindable VS @State 차이점 (iOS17+ Observable Macro) (0) | 2024.08.09 |