안녕하세요 Niro 🚗 입니다!
🔗 [WWDC 23 / SwiftUI] Demystify SwiftUI Performance with FeedBack Loop
첫번째 편으로 갈수록 다양한 기능을 담고 있는 App 에서 문제가 발생했을 때 빠르게 대처할 수 있는 FeddBack Loop 에 대해 알아보았습니다.
성능 문제를 해결하는 프로세스를 배웠으니 이제는 진짜로 해결하는 방법을 알아야겠죠?
[WWDC 23] - Demystify SwiftUI Performance 세션의 두번째 주제인 Dependencies - 의존성에 대해 알아보고자 합니다!
자, 귀여운 강아지를 보여주는 상세 View 가 있네요!
dog 를 매개변수로 받고 PlayTime 여부를 알기 위한 Environment 프로퍼티를 갖고 있습니다.
두 프로퍼티는 View 의 종속성을 의미하며 그래프로 나타내면 다음과 같습니다.
DogView
는 VStack 을 생성하고 VStack 은 위와 같이 여러 자식을 갖고 있습니다. 이와 같이 쭉 그래프는 확장이 되고 Image, Text, Color 와 같은 최종적인 View (leaf View) 에 도달할때 까지 계속 이어지겠죠?
다시 앱을 돌아가보니 뭔가 화면이 바뀐거 같습니다. SwiftUI 에서는 Model 에서 데이터가 변경되면 View 가 업데이트가 되는 특징을 갖고 있습니다.
우리는 여기서 데이터 변화가 있을 때 View 가 어떻게 업데이트 되는지에 대해 깊이 알아볼 필요가 있습니다.
위의 그래프에서 자식 View 들은 부모가 생성하는 View 를 기준으로 의존적입니다. 이렇게 View 끼리도 의존적이지만 Dynamic Property 도 또 다른 형태의 의존성의 일반적인 예시입니다.
예를 들어 DogView 는 @Environment
Property 를 통해서 isPlayTime
을 읽어오게 되어있는데 이것은 부모 View 로 부터 생성된 값 뿐만 아니라 environment 의 값에도 종속적이게 되는 것이죠!
이것을 시각화 해봅시다!
X 축을 시간이라 두고 업데이트의 프로세스 첫번째 단계는 바로 View 를 위한 새로운 값을 생성하는 것입니다.
즉, dog
와 isPlayTime
프로퍼티와 같이 모든 저장 프로퍼티를 의미합니다.
그 다음 SwiftUI 는 View 의 모든 동적 프로퍼티를 업데이트하게 되고
업데이트된 값으로 body 가 실행되어 View 의 자식들을 실행하게 되죠!
이런 과정은 UI 를 업데이트하기 위해 재귀적으로 반복이 되고 변경된 종속성이나 새로운 값이 있는 View 만 업데이트하게 됩니다.
Dog 라는 Model 은 구조체로 이루어져 값이 변경되면 새로운 복사본이 생성이 됩니다. 그러면 DogView
는 Stack 에 대한 새로운 컨텐츠를 생성하고 Stack 의 하위 항목들을 업데이트 하게 되는 거죠!
이렇게 변경된 ScalableDogImage
는 새로운 이미지를 생성하게 되고 Image 는 leaf View 이기 때문에 여기부터는 SwiftUI 가 작업을 진행하게 됩니다.
이것이 바로! 의존성 그래프를 보는 방법이고 이러한 프로세스를 개선하기 위해 몇가지 팁을 살펴보고자합니다.
자, 우리는 방금 View 가 업데이트 되는 과정을 살펴보았습니다. 굉장히 복잡했는데.. 이러한 과정을 줄인다면 더욱 좋겠죠?
SwiftUI 에서는 View 가 업데이트 되는 시기를 이해하기 위해 printChanges
라는 메서드를 제공하는데 해당 메서드를 통해 SwiftUI graph evaluator 가 특정 View 의 body 를 왜 호출했는지를 출력할 수 있게 됩니다.
예시를 볼까요?
State 가 포함된 ScalableDogImage
가 있습니다. Image 를 탭하게 되면 State 프로퍼티인 scaleToFill
이 바뀌게 되고 이미지가 커지게 되는 것이죠!
지금 View 의 body 에 breakPoint 를 걸어보았습니다.
그럼 우리는 이처럼 llDB 콘솔을 사용할 수 있게 되는데 printChanges
메서드를 호출해서 SwiftUI 가 View 의 body 를 요청한 이유를 설명해 주고 있습니다. (디버깅 전용이라 아쉽네요...)
여기서는 scaleToFill 값이 변경되어서 View 의 body 가 SwiftUI 에 의해 요청이 되었다는 것으로 해석할 수 있겠네요!
우리는 이렇게 printChanges
를 사용하여 View 에 추가적인 종속성이 있는지 여부를 이해할 수 있습니다.
이번엔 llDB 가 아닌 View 내에서 선언을 해볼까요?
이렇게 앱이 실행하고 디버깅 중인 상태에서 View 에 추가적인 종속성이 있는지 확인을 하고 싶다면 다음과 같이 View 의 본문에 접근할때마다 print 하도록 printChanges
를 호출할 수 있습니다.
하지만 중요한 점은 디버깅 전용이기 때문에 런타임 성능에 영향을 미치게됩니다... 즉, 릴리즈 할때는 꼭 제거를 해줘야 한다는점!
앱을 다시 실행해서 Rocky 의 좋아하는 간식을 비스킷에서 오이로 변경하게 되니 다음과 같은 결과가 나왔습니다.
로그를 보니 self 가 변경되었다 라고 나와있는데 이것은 View 의 값이 변경되었다는 것을 의미합니다.
즉, Scalable Image View 는 간식에 어떠한 종속성이 있는 것으로 보입니다.
이제 코드에 집중해보겠습니다.
View 에는 scaleToFill
과 dog
프로퍼티가 존재합니다.
scaleToFill
은 SwiftUI 동적 프로퍼티이며 변경되었다면 로그에 나타났을 겁니다.
로그에 보이지 않았다는 것은 결과적으로 @Self 는 dog
값이 바뀌었다는 의미입니다!
하지만 ScalableDogImage
View 는 dog
라는 값 보다 Image 에 관심이 있는 View 입니다.
따라서 우리는 Image
에 관련 없는 dog
라는 값에 대한 의존성을 제거하여 코드를 변경하였고 이렇게 다시 앱을 동작하게 되면 로그가 표시되지 않게됩니다.
당연히 printChanges 에 대한 호출을 지우는 것도 잊지 마세요!
자 우리는 자식 View 인 ScalableDogImage
에 대해 종속성을 제거했으니 부모 View 에 대한 코드도 바꿔야겠죠?
ScalableDogImage
에서 dog
프로퍼티를 없애고 image
프로퍼티로 바꾸었으니 ScalableDogImage
의 이니셜라이저의 값도 image
로 바뀌여야합니다.
ScalableDogImage(dog) -> ScalableDogImage(dog.image)
ScalabelDogImage
를 추출함으로써 필요한 의존성만 남기게 되었는데 Header 에 대해서도 동일한 방법으로 그 자체 View 로 만들 수 있습니다!
다음과 같이 2개의 Text 를 DogHeader
라는 View 로 만들고 dog
라는 프로퍼티에 의존성을 낮출 수 있게 되었죠!
이렇게 코드를 바꾸면서 우리는 더 읽기 쉬워졌습니다. 하지만 작은 View 에 대해서 효과적이라도 매우 큰 구조체에 대해서는 신중해야한다고 합니다.
우리는 의존성을 줄여나가는 방법을 알아보았습니다. 다시 정리하자면!
Observable
Protocol 은 읽기만 하는 의존성으로 자동으로 제한하여 의존성 범위를 조절하는데 도움이 될 수도 있습니다.아주 간단한 방법이지만 각 View 에 사용할 프로퍼티를 알맞게 사용해서 의존성을 줄여나가는 코드가 이렇게 중요한 것인지에 대해서 알 수 있는 시간이였고 다음 주제인 더 빠른 업데이트에 대해 알아보겠습니다!
긴 글 읽어주셔서 감사하고 피드백은 환영입니다!
그림과 함께 설명해주셔서 이해하기 쉬웠던 거 같아요 🔥 좋은 글 감사합니다!