지난 포스팅에 이어 SwiftUI 상태관리에 대해서 공부해보자!
이번 포스팅은 아주 길지만, 이거 하나로 SwiftUI 상태관리에 대한 내용은 끝이니 같이 잘 공부해보자!
앞으로 SwiftUI 공부 시리즈는 Github 레포에 올리겠다.
(GITHUB : https://github.com/Oct7/SwiftUI-Study.git)
모바일 앱에서의 상태 관리
모바일 앱 개발에서 가장 자주 마주하는 것이 바로 데이터의 변화에 따른 화면 업데이트이다.
필연적으로, 어떤 방식을 채택할 것인가는 개발에서 굉장히 큰 부분을 차지한다.
현재 네이티브 개발에서는 각 플랫폼이 정해준 그대로 사용해야 하고, 크로스플랫폼인 RN이나 Flutter 등에서는 다양한 방식의 State 라이브러리들이 존재한다.
SwiftUI에서는 어떻게?
SwiftUI에서는 화면의 노출과 관련된 함수 2개(onAppear/onDisappear)를 제외하고는 없고, 전체적으로 화면을 업데이트하는 방법 또한 파악되지 않았다. SwiftUI에서는 변수의 변화를 감지하고, 이 변수가 연관되어 있는 화면이 업데이트되는 방식을 채택한다. 이 방식은 Flutter에서 처음 경험해볼 수 있었다. 변화하고자 하는 화면을 업데이트하는 것이 아니라, 변화되는 변수가 포함된 부분만 업데이트된다.
SwiftUI는 이것도 여러가지 옵션을 제공하는데,
@State / @Binding / @StateObject / @ObservedObject / @EnvironmentObject가 있다.
개발할 때, 어떤 옵션을 사용해야 하나?
개발할 때 범용성과 편의성은 매우 중요하다. 작성하는 코드가 되도록이면 현재 개발 범위 이상으로 확장됐을 때도 사용할 수 있도록 범용성을 가져야하며, 사용하는 방식 자체가 복잡하거나 어렵지 않도록 편의성 또한 갖춰야한다. 물론 효율성을 무조건 포기하고 편의성을 추구하자는 것은 아니지만, 조금의 차이일 경우에는 개발 편의성을 우선하여도 되지 않겠냐라는 것이다. 우리가 사용하는 디바이스들은 하드웨어적으로는 조금의 메모리 누수가 문제가 되는 상황이 아니라고 판단하기 때문이다.
따라서, 우리는 어디에 무엇이 쓰일지 규정하고, 최대한 적은 옵션들로 코드들을 작성하려고 노력할 필요가 있다.
어떤 옵션들을 사용할지 결정하려면 5가지 모두 정확하게 알아볼 필요가 있다. 정확히 알아야 어디서 무엇을 쓸지 고를 수 있기 때문이다.
(아래에 공부에서는 각 옵션들에 대해서 내 주관적인 선택이 만들어진 이유에 대해서 서술되기도 한다. 이 글을 보는 분들은 본인에게 맞는 것을 고르면 된다!)
@State & @Binding
@State와 @Binding은 한 세트다.
@State는 화면에 종속적인 값으로 화면이 수명을 다하면 @State 변수의 값도 초기화되며, 선언된 하나의 화면에서만 변화가 감지된다.
@Binding은 @State를 다른 화면에서도 연계하여 사용할 수 있게끔 연결해준다.
변수 자체의 주소값을 전달하는 방식으로 보이며, 따라서 @Binding 변수의 값을 변화시키면 결국엔 @State의 값이 변화하고, 그에 따라 화면도 업데이트된다.
(아래 예제는
1. 다른 화면으로 넘어가서 @Binding 변수의 값에 변화를 줬을 때 @State도 변화하는 것을 보여준다.
2. @State가 선언된 화면을 벗어나면 @State값이 초기화됨을 보여준다.)
@ObservedObject & @StateObject
@ObservedObject와 @StateObject의 기능은 거의 동일하다.
ObservableObject 클래스 프로퍼티 안에 @Published라고 클래스 안에 선언된 변수의 변화를 감지하여 업데이트한다.
공식 문서에서 정의하는 다른 점은 한가지인데,
1. @ObservedObject는 ObservableObject를 지속적으로 관찰한다.(공식문서에서는 subscribe 한다는데, 이런 뜻인 것 같다.)
2. @StateObject는 ObservableObject를 객체화한다.
결론적으로 뷰를 생성할 때, 이 두 옵션이 최초 생성된다면
@ObservedObject는 임의의 주소값에 ObservableObject 객체를 생성하고, 지속적으로 관찰.
@StateObject는 객체화되어 화면 내에 종속된 변수로 생성.
한다라고 해석할 수 있을 것 같다.
그 결과, 화면이 초기화되는 순간이 찾아오면 @ObservedObject가 참조할 주소값도 초기화되어, @ObservedObject도 초기화되는 것 같다. 하지만 @StateObject는 화면 내에 종속된 변수임으로, 화면이 없어지기 전까지는 그 값이 유지되는 것 같다.
결론적으로,
1. @StateObject는 최초로 생성된 화면이 유지되는 한, 외부적 요인에 의해서 의도치 않은 초기화는 벌어지지 않는다.
2. @ObservedObject는 최초로 생성된 화면 자체가 외부적 요인에 의해서 초기화되는 상황이 오면, 초기화가 벌어진다.
(무슨 이유에선지 @ObservedObject는 생성된 화면이 pop된 후에 다시 이 화면을 push하면 @ObservedObject의 데이터가 유지된다. 하지만, 생성된 화면이 초기화되면 @ObservedObject도 초기화되는 걸로 보아 SwiftUI 자체에서 pop되더라도 직전 화면 데이터가 메모리에 있어서 주소값 또한 유지되는게 아닌가 싶다.)
아래 코드와 영상 순서대로 예시를 확인해보면,
1. 둘 다 ObservableObject라는 클래스 프로퍼티에 @Published에 해당하는 값을 변경시키는 모습을 확인할 수 있다.
2. @StateObject는 화면에서 벗어남과 함께 데이터가 없어졌다.
3. @ObservedObject들은 1번 pop된 화면에서는 다시 원래 페이지로 돌아가면 데이터가 유지된다. 2번 pop된 화면에서 check_observedObject 변수를 확인하면 다시 돌아왔을 땐 데이터가 사라졌다.
4. StateCheckNavigationView의 생성을 위한 인풋 인자가 상위 화면에 의해서 업데이트됨에 따라 화면이 초기화(업데이트)가 벌어질 때 @ObservedObject는 초기화되고, @StateObject는 데이터가 유지된다.
(코드를 보기 쉽게 보여주기위해 코드에서 @State&@Binding 부분과 NavigationLink를 통해 @ObservedObject&@StateObject를 넘겨주는 부분은 생략했다.)
@EnvironmentObject
@EnvironmentObject는 앱 전반적으로 상태 변수를 공유를 위한 옵션이다. 간단하게 보자면, 상태 변화를 추적할 수 있는 로컬DB 같은 거라고 생각할 수 있겠다. 굉장히 편해보이고, Flutter의 GetXController와 굉장히 비슷한 방식의 상태 변수이다.
아래처럼, 가장 처음의 View 코드 뒤에 ObservableObject 클래스 프로퍼티를 가진 한 변수를 등록하면, 이 변수는 앱 프로세스가 살아있는 동안 계속해서 상태변화를 감지하고 업데이트가 벌어진다. (동영상의 다른 코드들은 제외하고 코드를 올렸다. 모든 코드는 GITHUB에 있다.)
@EnvironmentObject는 @ObservedObject 그 기능과 컨셉 자체는 굉장히 흡사하고 이 둘의 기능을 전역으로 행사할 수 있는 편리한 수단이다. 하지만, 이 둘의 개념은 완전히 다르다.
@EnvironmentObject는 최초에 .environmentObject(ObservableObject: 변수)를 통해 공유받은 ObservableObject 클래스를 앱 전체에 걸쳐서 공유된다. @EnvironmentObject는 앱이 .environmentObject(ObservableObject: 변수)를 통해 공유된 클래스를 상속받는 것처럼 작동한다.(상위 뷰의 ObservableObject를 하위뷰에 @ObservedObject로 받는 것과 같은 효과를 보여준다.) 따라서, @EnvironmentObject 옵션은 사실 상태 변화를 감지하는 변수라기보다는 앱 전체적으로 영향을 미칠 수 있는 상태변수를 전달하기 위한 기능이다. 또한, 정해진 클래스를 앱이 형성되는 시점에 @EnvironmentObject로 선언하기 때문에 우회하지 않고서는 상속, 동적바인딩이 되지 않는다.
그래서, 이 옵션들을 정리하면?
1) @State : 한 화면에서 상태 변화가 모두 일어나는 변수의 경우
2) @Binding : @State 변수를 다른 화면에서 사용하고 싶은 경우
3) @ObservedObject : 뷰의 라이프사이클 중 초기화(업데이트) 상황에서 영향을 받기 때문에, ObservableObject를 상위 뷰에서 받아서 사용하는 경우
4) @StateObject : 뷰의 라이프사이클에 영향받지 않고, 단독 인스턴스로써 작동하고 싶은 경우
5) @EnvironmentObject : 모든 화면에서 영향을 미치는 어떠한 환경적인 변수일 경우
라고 정리할 수 있겠다.
결론, 뭐를 어디에 어떻게 쓰면 좋을까?
결론을 내기 전, 먼저 옵션들간에 겹치는 역할들이 존재하기 때문에 이것부터 짚고 넘어가야한다.
1. @State & @Binding을 같이 써야하는 경우에는 @ObservedObject나 @StateObject를 쓰는 게 코드를 작성하는 입장에서 쉽다.
2. 다른 화면과 공유해야되는 상태 변수를 최초 선언할 때는 @StateObject를 쓰자. @ObservedObject는 최초 선언될 때 사용될 경우에는 라이프사이클 문제를 가진다.
3. 상태 변수로써 최초 선언되는 것 이외에는 상태 변수로써 @ObservedObject를 쓰는 것이 효율성 측면에서 앞선다.(@StateObject를 상속받는 경우 최초 선언된 @StateObject 변수가 업데이트될 때 모든 화면에 있는 @StateObject 인스턴스가 재생성된다. 낭비가 심하다..)
이렇기 때문에 나는 이 옵션들을 아래의 경우에 맞춰서 쓸 것이다.
1. @State : 한 화면에서만 상태 변화할 경우
2. @StateObject & @ObservedObject : 다른 화면에서도 상태 변화 변수를 공유할 경우. 변수 선언은 @StateObject로, 다른 화면으로 변수 전달한 경우 @ObservedObject로.
3. @EnvironmentObject : 전 화면에 걸쳐서 쓰는 상태 변화 변수일 경우. (예를 들어, 이중 클릭방지용 변수 등..)
@Binding은 역할이 여러개 겹치고, 다른 옵션들에 비해 편하지 않아서 배제됐고,
@StateObject와 @ObservedObject는 각자의 특성에 의해 역할이 정해졌다.
@EnvironmentObject의 경우에도 더 많은 경우 사용할 수 있겠지만, 관리의 편의성과 코드의 범용성을 위해 위와 같은 역할로 제한됐다.
프로그래밍을 하면서 더 깊게 공부할 수 있는 부분들이 남아있지만, 일단은 개념/기능/정의적으로는 얼추 공부가 된 것 같다.
거의 100개에 달하는 공식 문서와 정의 예시 등을 통해서 공부하면서 글을 쓰다보니, 잘못되거나 오해할 수 있는 글들이 너무 많았다.
그래서, 최대한 한스텝 한스텝 다지면서 공부할 수 있도록 했지만 단어가 애매하거나 하는 경우는 어쩔 수 없이 생기는 것 같다.
혹시나 질문이 있다면 Github issue나 블로그 댓글로 남기면 답변할 수 있도록 하겠다.
(공부하며 쓰다보니 글 작성기간이 길어져서 잘못되었거나 애매해서 수정이 필요한 경우에도 적극적인 피드백 바란다...)
'SWIFT > Study' 카테고리의 다른 글
[SwiftUI, 상태관리][0편] 스위프트UI 에서의 상태관리 (0) | 2022.02.03 |
---|