큰 View를 하위 View로 쪼갤때 할 때 Environment Object 활용하기

SteadySlower·2022년 8월 24일
0

기능 개발에만 집중을 하다보면 View의 body 부분이 엄청나게 길어지고 중복되는 요소도 많아지게 됩니다. 이렇게 되면 나중에 유지보수에 엄청난 시간과 고생이 필요하게 됩니다.

따라서 중간중간 리팩토링을 통해서 코드를 정리하는 작업이 필요합니다. body 부분의 중복되는 부분을 없애고 너무 비대한 부분을 각각의 View 별로 쪼개서 파악하기 쉽고 유지보수하기 쉬운 코드를 만들어야 합니다.

이렇게 하나의 통짜(?) View를 작은 View로 쪼개는 작업을 하다보면 최상위 View에 존재하는 하나의 ViewModel의 참조를 하위 View에 계속해서 전달해야하는 귀찮은 일이 발생하게 됩니다.

꼬리에 꼬리를 무는 viewModel

아래 리팩토링한 코드를 보시면 body 전부 구현되어 있던 View들을 WordBookPickerView, TextInputView, ImageInputView 등으로 쪼개놓은 상태입니다. 길이가 훨씬 줄어들고 구조를 한 눈에 파악하기도 좋습니다. 그리고 TextInputView, ImageInputView에 inputType을 전달해서 구분함으로서 중복되는 코드도 많이 줄일 수 있었습니다.

하지만 모든 SubView에 ViewModel의 참조를 계속 전달해야 합니다. 같은 코드가 계속해서 반복되고 있고 만약에 하위 View의 하위 View를 만드는 경우에는 또 다시 ViewModel의 참조를 전달해야 하는 불상사가 발생할 수도 있습니다.

더 불상사는 뷰 계층이 복잡해질 때 발생합니다. 만약에 뷰 계층이 더 복잡해져서 A - B - C의 다단 뷰 계층을 가지게 된다고 가정해봅시다. 만약에 A뷰와 C뷰에서만 ViewModel이 필요하다고 생각을 해봅시다. 이 경우 아래와 같은 방식으로 구현하면 B뷰는 단지 C뷰에 전달하기 위해서 자신은 필요도 없는 ViewModel의 참조를 가지고 있어야 합니다🤮

var body: some View {
  ScrollView {
      VStack {
          WordBookPickerView(viewModel: viewModel)
          VStack {
              TextInputView(inputType: .meaning, editFocus: $editFocus, viewModel: viewModel)
              ImageInputView(inputType: .meaning, viewModel: viewModel)
          }
          .padding(.bottom)
          VStack {
              TextInputView(inputType: .gana, editFocus: $editFocus, viewModel: viewModel)
              ImageInputView(inputType: .gana, viewModel: viewModel)
          }
          VStack {
              TextInputView(inputType: .kanji, editFocus: $editFocus, viewModel: viewModel)
              ImageInputView(inputType: .kanji, viewModel: viewModel)
          }
			}
		}
}

Environment Object로 리팩토링 하자

예전에 이 포스팅에서 Environment Object에 대해 소개한 적이 있습니다. 이 포스팅은 Authentication 등을 담당하는 앱 전체적으로 쓰이는 객체에 주안점을 두고 소개하고 있습니다만, Environment Object은 그 보다 작은 단위에도 유용하게 사용할 수 있습니다.

최상위 View에 Environment Object를 주입하자

최상위에서 Environment Object 주입을 위해서 뷰 계층을 하나 더 만들었습니다. 기존의 View들을 모두 담은ContentView를 만들고 그 View에 ViewModel의 인스턴스를 만들어서 environment object로 주입합니다. ViewModel의 인스턴스는 여기서 딱 1번만 만들어지고 이 인스턴스를 모든 View가 공유합니다.

struct MacAddWordView: View {
    var body: some View {
        ContentView()
            .environmentObject(ViewModel())
    }
}

하위 View에서 @EnvironmentObject로 사용한다.

하위 View에서는 @EnvironmentObject로 선언해서 사용하면 됩니다. 여기에 타입만 명시하면 최상위 View에서 만든 인스턴스를 공유해서 사용할 수 있습니다. 이렇게 하면 initializer에 참조를 일일히 전달할 필요가 없습니다.

extension MacAddWordView {
    struct ContentView: View {
        @EnvironmentObject private var viewModel: ViewModel
        @FocusState private var editFocus: InputType?
        
        var body: some View {
            ScrollView {
                VStack {
                    WordBookPickerView()
                    VStack {
                        TextInputView(inputType: .meaning, editFocus: $editFocus)
                        ImageInputView(inputType: .meaning)
                    }
                    .padding(.bottom)
                    VStack {
                        TextInputView(inputType: .gana, editFocus: $editFocus)
                        ImageInputView(inputType: .gana)
                    }
                    VStack {
                        TextInputView(inputType: .kanji, editFocus: $editFocus)
                        ImageInputView(inputType: .kanji)
                    }
                    SaveButton()
                }
            }
        }
    }
}

하위 View의 하위 View에서도 동일하게 사용한다.

하위 View의 하위 View에서도 동일하게 사용하면 됩니다. 아무리 많은 뷰 계층을 내려가도 사용할 수 있고 EnvironmentObject를 사용하지 않는 View의 하위 View라도 사용할 수 있습니다.

struct WordBookPickerView: View {
    @EnvironmentObject private var viewModel: ViewModel
}
struct TextInputView: View {
    @EnvironmentObject private var viewModel: ViewModel    
}
struct ImageInputView: View {
    @EnvironmentObject private var viewModel: ViewModel
}
struct SaveButton: View {
    @EnvironmentObject private var viewModel: ViewModel
}
profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.

0개의 댓글