스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
SwiftUI 성능 쉽게 이해하기
SwiftUI에서 성능에 대한 멘탈 모델을 만드는 법과 더 빠르고 효율적인 코드를 작성하는 법을 배우세요. 여러분의 앱에 더 반응성이 높은 뷰를 만들 수 있도록 성능 문제를 일으키는 흔한 원인을 알려드립니다. 또한 SwiftUI에서 우선순위를 정해 행과 히치를 해결하는 방법도 알아보세요.
챕터
- 0:00 - Introduction and performance feedback loop
- 3:30 - Dependencies
- 10:48 - Faster view updates
- 13:24 - Identity in List and Table
리소스
관련 비디오
WWDC23
WWDC21
Tech Talks
-
다운로드
♪ ♪
'SwiftUI 성능 쉽게 이해하기'에 오신 걸 환영합니다 SwiftUI는 복잡하고 강력한 앱을 쉽게 만들 수 있게 하고 리스트와 테이블 등 다양한 기능과 복잡한 제어를 제공합니다 개발을 갓 시작해서 앱이 별로 복잡하지 않으면 성능 문제는 그다지 눈에 띄지 않지만 앱이 복잡해질수록 성능 문제도 중요해집니다 작은 문제가 커질 수도 있고 프로토타입에서는 잘 작동했던 코드가 실제 프로덕션에서는 잘 작동하지 않을 수도 있죠 이 세션은 SwiftUI 성능에 대한 멘탈 모델을 만드는 법을 다룹니다 개발 과정 초기부터 빠른 코드를 작성하는 법을 이해한다면 앱이 복잡해질 때 생기는 문제가 줄어들거든요 성능 문제를 다룰 때의 피드백 루프를 살펴봅시다 성능 문제는 증상에서 시작합니다 내비게이션 푸시가 느리거나 애니메이션이 끊기거나 macOS에서 로딩 스피너가 뜨거나 하는 것 말이죠 성능 문제가 있다는 걸 파악하면 문제를 해결하는 첫 단계는 바로 측정입니다 측정을 통해 증상이 있음을 입증하고 나면 증상의 원인을 파악하세요 피드백 루프에서 이 단계가 특히 까다로울 때가 많은데 일이 어떻게 돌아가야 하는지를 직관으로 알아야 하기 때문입니다 버그는 앱에 틀린 전제가 있을 때 발생하는데 이 세션은 여러분이 앱의 전제와 현실 사이의 불일치를 파악할 수 있도록 도와드릴 겁니다
근본 원인을 파악한 후 최적화해서 문제를 해결하세요 하지만 근본 원인을 밝혀내고 코드를 최적화한다고 성능 문제가 끝나진 않습니다 고친 내용을 다시 측정하고 입증해서 문제가 해결됐는지 확인해야 하죠 어떤 버그를 잡을 때든 이 과정을 거치는 게 좋지만 이 과정은 특히 성능 향상에 중요합니다 문제가 해결됐다는 걸 입증하면 루프를 끊습니다 이 도표로 본 세션을 전체 맥락에서 이해할 수 있습니다 이 과정은 겪지 않는 게 가장 좋고 성능 문제 상당수는 프로토타입을 만들 때 빠른 코드를 작성하면 예방할 수 있습니다 하지만 앱이 복잡해질수록 어쩔 수 없이 성능에 버그가 생기게 되죠 아무리 뛰어난 사람이라도 겪는 문제입니다 그리고 성능 문제를 실제로 겪게 됐을 때 문제의 우선순위를 정하고 고칠 도구는 많을수록 좋습니다 피드백 루프를 더 쉽게 통과하도록 돕는 게 이 세션의 목적입니다
이 세션은 고급 세션이므로 다음의 요건을 갖추셔야 합니다 SwiftUI 아이덴티티를 대강은 이해하셔야 하며 암시적 아이덴티티와 명시적 아이덴티티가 어떻게 다른지도 아셔야 합니다 뷰 수명과 뷰 아이덴티티의 차이를 아는 것도 중요합니다 요건을 충족하지 못해도 걱정하지 마세요 WWDC21의 'SwiftUI 쉽게 이해하기' 세션에서 이 내용을 다뤘으니까요 오늘의 세션은 그때의 세션에서 이어집니다 다룰 의제를 살펴보겠습니다 먼저 종속성에 관해 자세하게 설명하고 SwiftUI 업데이트 과정을 상세히 살펴보겠습니다 그다음 업데이트에 관해 설명하고 SwiftUI가 인터페이스를 더 빠르게 업데이트하게 하는 법을 설명하겠습니다 마지막으로 리스트와 테이블의 아이덴티티를 설명하겠습니다 세션을 진행하면서 SwiftUI 물밑을 살짝 엿보고 개발할 때 쓸 수 있는 팁과 요령도 알아보겠습니다 이 세션은 뷰 계층 구조 업데이트가 느려지는 문제를 주로 다루지만 앱 개발 과정에서 겪을 수 있는 성능 문제를 결코 다 다루지는 못합니다 먼저 종속성 이야기부터 하겠습니다 'SwiftUI 쉽게 이해하기' 세션 후 벌써 몇 년이 지났군요 그동안 개를 주제로 한 앱을 작업하고 싶었는데 못 했죠 그러니 지난 세션에 이어서 제가 새로 개발 중인 앱을 봅시다 제가 사랑하는 털 뭉치 친구들을 관리하고 얘들과 놀아 줄 시간을 마련하는 앱인데요 이건 뷰 중 하나입니다 모든 개를 나열한 테이블이죠 여기 iPhone에서 보시다시피 이 앱엔 상세 뷰도 있어서 개의 사진을 크게 띄우고 개가 뭘 좋아하는지 보여주고 놀아 줄 시간을 설정하는 버튼도 보여줍니다 이 뷰의 코드는 바로 이겁니다 뷰에서는 개를 매개변수로 정했고 놀이 시간이 됐는지 알아보는 환경 프로퍼티도 갖추고 있죠 지난 '쉽게 이해하기' 세션에서 언급했듯이 이건 개와 놀이 시간 변수가 뷰에 종속됐다는 걸 의미합니다 이 뷰는 그래프로도 나타낼 수 있습니다 이 뷰를 거의 비슷하게 나타내는 간단한 그래프입니다 각 화살은 뷰의 본문을 나타냅니다 개 뷰는 스택을 생성하는데 스택에는 여러 자식이 딸립니다 예컨대 텍스트나 크기 조절 가능한 개 사진 디테일 뷰, 버튼 등이 있죠 계속해서 각각의 뷰에도 자식이 딸리고 그래프는 리프 뷰에 이를 때까지 이어집니다 이미지나 텍스트나 색상 같은 것 말이죠 모든 뷰는 결국 리프 뷰로 나뉩니다 SwiftUI의 리프 뷰는 여러 가지라 여기서 전부 다루지는 않을 테니 더 자세히 알고 싶으시면 문서를 참조하세요 다시 앱으로 돌아갑시다 앱을 사용할 때마다 개 친구들과 노는 시간을 기록할 수 있죠 방금 제가 로키와 공놀이를 마쳤다고 앱에 기록하면 버튼과 이미지가 업데이트됩니다 로키는 꽤 행복해 보이지만 지금은 너무 지쳐서 분명 놀지는 못할 겁니다 모델에서 이 데이터가 바뀌면 SwiftUI가 이 뷰를 업데이트합니다 업데이트 과정을 자세히 살핍시다 그래프로 돌아가서 데이터가 바뀔 때 무슨 일이 벌어지는지 보죠 그래프가 다시 나왔습니다 지난번 '쉽게 이해하기' 세션을 여기서 끝내면서 뷰들이 모여 그래프를 이루고 SwiftUI는 코드를 실행할 때 종속성을 살핀다고 설명했었죠 이제 줌인해서 이런 종속성이 어디서 생기고 종속성을 어떻게 제어할 수 있는지 더 자세히 살펴보겠습니다 각 자식 뷰는 해당 뷰의 선조가 생성하는 뷰값에 종속되지만 그 외에도 다른 형태의 종속성이 있습니다 동적 프로퍼티에서도 종속성이 흔히 발생하죠 예를 들어 DogView는 @Environment 프로퍼티 래퍼를 사용해 지금이 놀이 시간인지를 환경으로부터 읽어 내므로 부모가 생성한 값과 환경에서 도출한 값 모두에 종속돼 있습니다 시간을 X축으로 시각화하면 업데이트 과정의 첫 단계는 뷰에 새로운 값을 생성해 주는 겁니다 이 값은 뷰에 저장된 모든 프로퍼티를 포함하죠 갯값, 동적 프로퍼티의 초깃값 같은 것들을요 다음으로 SwiftUI는 뷰의 동적 프로퍼티를 전부 업데이트해 동적 프로퍼티의 값을 이 그래프에 있는 값으로 바꿉니다 마지막으로 업데이트한 값을 이용해 본문이 실행되며 뷰의 자식을 생성합니다 다시 그래프를 봅시다 이 과정이 반복되며 인터페이스가 업데이트되는데 값이 새로 바뀌거나 종속성이 바뀐 뷰만 업데이트되죠 로키가 지쳤다고 표시하면 새 개를 불러옵니다 죄송해요 개 구조체 값을 불러옵니다 그래도 불려 오는 건 똑같이 로키랍니다 우리의 데이터는 값 타입이기 때문에 데이터를 변형하면 복사본이 새로 만들어지면서 그 결과 DogView가 스택의 새 콘텐츠를 생성하게 되고 그에 따라 스택의 자식도 업데이트됩니다 여기서 전 ScalableDogImage에만 초점을 맞췄지만 갯값에 종속되는 다른 뷰도 업데이트될 수 있습니다 결국 ScalableDogImage는 새 이미지를 생성하는데 이미지는 리프 뷰이기 때문에 여기서부터 나머지 일은 SwiftUI가 전부 처리합니다 그러면 과정이 끝나고 새로운 렌더링이 생성됩니다 종속성 그래프는 바로 이렇게 보는 겁니다 이 과정을 개선하는 팁 몇 가지를 알아봅시다 업데이트 횟수를 줄여 꼭 필요한 때만 해야 합니다 뷰가 언제 업데이트되는지 알기 위해 SwiftUI에는 printChanges 메서드가 있습니다 이걸 이용하면 왜 SwiftUI 그래프 평가자가 뷰의 본문을 호출했는지를 출력할 수 있죠 예시를 통해 사용법을 알아봅시다 ScalableDogImage에는 상태가 포함돼 있는데 이미지를 탭하면 상태는 이렇게 바뀝니다
이미지 뷰에만 집중해서 뷰의 본문에 중단점을 설정하면 Self._printChanges를 LLDB 콘솔에서 호출할 수 있습니다 'expression' LLDB 커맨드를 사용해서요 printChanges는 디버깅에만 쓰는 기능으로 SwiftUI가 뷰의 본문을 요청한 이유를 최선을 다해 설명합니다 이 경우에는 scaleToFill이 바뀌었기 때문이죠 printChanges를 이용해 뷰에 다른 종속성이 있는지 알아볼 수 있습니다 예를 들어, 전 지금 앱을 실행하고 디버깅하고 있는데 이 뷰에 다른 종속성이 있는지 알아보고 싶거든요 그러면 뷰의 본문에 printChanges 호출을 추가해 뷰의 본문을 액세스할 때마다 출력하도록 하면 됩니다 그런데 printChanges 앞머리에 밑줄 기호가 붙은 게 보이시죠 이건 printChanges가 계속 존재하리라고 보장할 수 없으며 향후 릴리즈에서 없어질 수도 있다는 걸 의미합니다 그러니 이 메서드 호출은 절대 App Store에 제출하지 마세요 이 호출은 나중에 없애야겠습니다 디버깅에만 쓰일 호출이고 런타임 성능에 영향을 미치니까요 다시 앱을 실행해 로키가 가장 좋아하는 간식을 바꾸죠 비스킷에서 오이 같은 걸로 바꿔 볼까요 우리 이미지의 콘솔에 로그가 생겼습니다 'self'가 바뀌었다고 하네요 이건 뷰값이 바뀌었다는 뜻이니 조절 가능한 이미지 뷰는 분명 간식에 종속성이 있죠 그럴 필요는 없는데도요 코드에 집중해 보면 뷰값에는 scaleToFill 멤버와 개 프로퍼티밖에 없습니다 scaleToFill은 SwiftUI의 동적 프로퍼티니까 이게 바뀌었다면 체인지 로그에 나타났을 겁니다 그러니 여기서 '@Self'는 갯값이 바뀌었다는 것을 뜻합니다 하지만 이 뷰를 보는 우리에게 중요한 건 이미지뿐이죠 그러니 이미지만을 사용하여 종속성을 제거하면 됩니다 이제 제가 이미지와 관계없는 개의 프로퍼티를 수정하면 로그가 뜨지 않습니다
이 뷰의 종속성을 꼼꼼하게 스코핑했습니다 이 기법을 따라 하실 거라면 printChanges 호출은 잊지 말고 지우세요 여기에 맞춰 부모 뷰도 업데이트합시다 개 부모 뷰의 코드입니다 ScalableDogImage의 이니셜라이저를 업데이트해야 이미지를 받아 올 수 있습니다 이렇게요 ScalableDogImage를 바깥으로 추출함으로써 종속성을 줄이고 중요한 종속성만 남겼죠 마찬가지로 헤더도 새 뷰로 만들어 추출하면 됩니다 이렇게 하면 여러 이점이 있습니다 코드 읽기가 더 쉬워지고 DogHeader의 종속성이 사용 위치에서 드러나죠 이 기법은 작은 뷰에서는 잘 작동하지만 구조체가 아주 클 때는 조심하셔야 합니다 모든 종속성이 이렇게 스코핑할 가치가 있진 않거든요 여러분이 판단력을 발휘하셔야 합니다
업데이트가 적다는 건 앱에서 데이터가 바뀔 때 성능이 더 좋다는 의미입니다 아까 살펴봤듯이 종속성을 줄이면 성능을 높일 수 있습니다 뷰값이 실제로 종속된 데이터에만 한정되도록 줄여 보세요 또 다른 팁으로는 뷰를 추출해 종속성을 줄이는 방법이 있습니다 마지막으로, 새로 생긴 Observable 프로토콜도 종속성 스코핑에 유용합니다 종속성 중 읽히는 것만을 자동으로 남겨 놓거든요 'SwiftUI의 Observation 알아보기' 세션에 더 자세한 정보가 있습니다 지금까지 종속성을 살피는 방법을 간단하게 알아봤는데 이제는 빠른 업데이트 방법으로 넘어가겠습니다 이 섹션에서는 SwiftUI를 매번 업데이트하는 비용을 절감하는 법을 다루겠습니다 SwiftUI 업데이트 속도가 느리면 앱에 여러 부정적 영향이 생기는데 행이나 히치 등 반응성 감소도 여기에 포함됩니다 행이란 사용자의 상호 작용에 대한 반응이 늦어지는 것입니다 뷰가 처음 나타날 때까지 너무 오랜 시간이 걸리는 것처럼요 WWDC2023의 'Instruments로 행 분석하기' 세션에서 Instruments로 행을 분석하는 법을 자세히 다루는데 행이 SwiftUI 관련 작업 때문에 발생했는지를 파악하는 법도 함께 다룹니다 히치란 사용자가 감지할 수 있는 애니메이션 문제입니다 스크롤이 도중에 멈추거나 애니메이션 프레임을 건너뛰는 것 같은 거죠 행과 히치의 근본 원인은 특히 SwiftUI에서는 서로 밀접한 관련이 있습니다 시스템 렌더 루프의 작동을 포함해 히치에 대해 더 자세히 알려면 'UI 애니메이션 히치와 렌더 루프 살펴보기' Tech Talk 영상을 보세요 SwiftUI에서 행과 히치는 둘 다 업데이트가 느려서 생기곤 하는데 업데이트가 느려지는 원인은 흔히 다음과 같습니다 첫째, 동적 프로퍼티 인스턴스화가 너무 비싼 경우입니다 상태 객체를 할당하고 초기화하거나 상태를 초기화하는 것 말입니다 본문 안에서 작업하는 것도 원인이 될 수 있습니다 비싼 문자열 보간이나 본문 안에서 이뤄지는 데이터 필터링 및 기타 작업 등의 연산이 있는지 확인하세요 본문은 가능한 한 싸게 만드는 것이 중요합니다 이 모든 것은 서로 연관돼 있습니다 예를 들어 동적 프로퍼티를 뷰의 본문에서 계산하면 뷰를 실행하는 것도 비싸질 수 있습니다 뷰의 본문에서는 식별도 종종 늦어지곤 합니다 Fetch 앱을 예로 들어 살펴봅시다
이 예시에서 저는 앱의 루트 뷰를 작업하고 있는데 루트 뷰에는 제가 개 리스트를 만들 때 쓰는 객체가 있습니다 이 슬라이드에서 강조한 코드를 따라가면 본문의 model.dogs에 액세스할 때 객체가 지연되어 인스턴스화됩니다 그러면 우리는 이니셜라이저로 넘어가게 되고 이니셜라이저가 개 리스트를 페칭하는데 코드 코멘트에 적혀 있듯이 리스트를 페칭하는 데 시간이 오래 걸릴 겁니다 이건 동기적 작업입니다 task 수정자를 이용하면 이걸 수정할 수 있죠 먼저 페칭를 비동기적으로 만들겠습니다 여기서 전 async 키워드를 추가한 것만 보여드리고 있습니다 그다음 task 수정자에서 개 리스트를 await 해서 비동기적으로 페칭하겠습니다 이렇게 하면 앱에 반응성이 생기죠 비싼 데이터 로딩 연산이 일어나도요 여러분이 미처 모르는 사이에 작업의 여러 소스가 앱에 영향을 미칠 수 있습니다 예를 들어 문자열 보간은 비싸지기 쉬우니 자주 쓸 필요가 있는 문자열은 반드시 캐싱하세요 번들에서 값을 찾는 것도 비싼 작업일 수 있습니다 클래스 바운드 타입 할당을 비롯해 어떤 히프 할당이라도 당연히 누적될 수 있고요 이제 리스트와 테이블로 넘어갑시다 리스트와 테이블은 단순한 레이아웃뿐만 아니라 다양한 기능을 지원합니다 선택, 밀기 동작, 순서 바꾸기 그 외에도 많은 걸 추가했죠 복잡하고 고급인 이런 제어 기능이 앱에서 잘 작동하게 하려면 아이덴티티를 이해하는 건 필수입니다 이 섹션에서는 리스트와 테이블의 아이덴티티를 설명하고 이 내장된 컴포넌트의 업데이트 성능을 극대화하는 법을 쉽게 설명하겠습니다 본격적으로 이 주제를 다루기 전에 개선 사항 몇 가지를 말씀드리려 합니다 macOS Sonoma와 iOS 17에서 SwiftUI는 필터링과 스크롤 등 여러 가지 사항을 내부적으로 개선했는데 여러분은 최소한의 노력만 해도 개선의 이점을 누릴 수 있습니다 그리고 그 결과 로드의 반응성 및 큰 리스트와 테이블의 업데이트에 걸리는 시간도 극적으로 나아질 테고요 그런데 더 나은 성능으로 이어지는 리스트와 테이블을 구성하는 법은 따로 있답니다 리스트와 테이블은 식별자를 이용해 데이터가 어떻게 바뀌었는지 알아냅니다 일관성을 위해 리스트와 테이블의 ID는 모두 즉시 수집되므로 리스트와 테이블 콘텐츠의 식별자를 빨리 생성할 수 있다면 로드 및 업데이트 시간도 곧바로 빨라지게 됩니다 아이덴티티는 SwiftUI가 뷰 수명을 관리하게 돕는데 이건 계층 구조를 점차 업데이트하는 데 필수입니다 아이덴티티가 바뀌었다는 건 뷰가 바뀌었다는 뜻인데 이 점은 애니메이션과 성능에도 중요합니다 애니메이션에 대한 자세한 정보는 'SwiftUI 애니메이션 살펴보기' 세션을 참고하세요 식별 성능은 중요합니다 식별자가 자주 수집되니까요 리스트와 테이블은 특히 더 그렇죠 리스트 식별 모델을 살펴봅시다 저는 앱의 개 리스트를 열심히 작업하고 있는데 처음에는 단 한 행으로 시작했습니다 이게 리스트의 코드인데 안에는 DogCell 하나뿐이에요 다음 단계는 ForEach를 사용해 모든 개에게 이걸 반복하는 겁니다 간단한 예시이지만 아이덴티티와 직접 연관이 있죠 그리고 리스트에 ForEach를 넣는 건 성능을 평가하는 데 중요한 시간입니다 그 이유는 ForEach의 제네릭 서명을 보면 알 수 있어요 이게 SwiftUI에서 ForEach의 서명입니다 ForEach는 도출되는 일련의 뷰에 수집한 데이터를 매핑하여 각각의 뷰에 명시적 아이덴티티를 생성합니다 리스트를 사용할 때 리스트는 몇 행을 표시해야 할지 또한 각 행의 식별자가 무엇인지 알아야 하므로 리스트는 데이터 컬렉션을 곧바로 찾아가 각 엘리먼트의 ID를 결정합니다 각각의 뷰를 생성하기 위해 콘텐츠 클로저가 호출됩니다 행은 필요할 때마다 만들어집니다 리스트는 아이덴티티와 콘텐츠를 합성하여 리스트 행을 생성합니다 필요할 때마다 만들어진 행은 가시 영역과 상관관계를 맺습니다 프리페칭이나 접근성을 위해 시스템에서 정한 버퍼와도 관계가 있고요 뷰를 스크롤하면 더 많은 뷰가 존재하게 됩니다 이 ForEach를 생성하는 데 사용한 코드 조각입니다 보시다시피 콘텐츠는 DogCell뿐인데 이것 자체는 단일 뷰입니다 안에서 HStack을 사용하니까요 ForEach는 리스트가 최종적으로 사용할 행 ID를 결정하는 데 필수입니다 그리고 리스트는 자기의 모든 ID를 곧바로 알아야 하는데 만약 콘텐츠가 일정한 개수의 행으로 분할된다면 모든 콘텐츠를 찾아가야만 그걸 효율적으로 알아낼 수 있습니다 예를 들어 우리가 이 리스트를 리팩터링해서 공 물어 오기를 좋아하는 개만 나타내고 싶다고 칩시다 조건부 뷰를 사용해 필터를 추가하겠다는 생각이 들 법하죠 여기서 뷰의 숫자는 변수입니다 1 아니면 0이죠 이건 좋지 않습니다 리스트가 행 식별자를 가져오려면 결국 모든 뷰를 빌드할 수밖에 없으니까요 왜냐하면 리스트는 각 엘리먼트가 몇개의 뷰로분할되는지 모르니까요 AnyView를 사용해도 마찬가지입니다 이 경우 뷰가 몇 개인지 이젠 전혀 알 수 없어서 전과 똑같은 문제가 생깁니다 모든 행을 생성해야만 해요 필터를 데이터 컬렉션으로 옮기면 어떨까요? 이제 다시 엘리먼트당 뷰의 개수가 일정해져서 필요한 뷰에만 행 콘텐츠가 구성됩니다 하지만 조심하세요 이 경우 인라인 필터가 컬렉션 위에서 선형이거든요 이게 프로토타입에선 괜찮을 수 있지만 컬렉션의 규모가 커지면 연산이 금세 비싸지면서 업데이트가 느려질 수 있습니다 이걸 모델로 빼내는 게 낫습니다 이제 양쪽의 장점만 누릴 수 있게 됐습니다 필터가 캐싱됐으니 리스트가 구성될 때마다 실행되지 않을 것이고 엘리먼트당 뷰의 개수도 일정합니다 뷰 개수를 일정하게 유지하는 팁 몇 가지를 알려드리겠습니다 주의할 점은 이런 접근법이 리스트와 테이블 안에서 ForEach를 쓰는 상황에서만 유효하단 겁니다 왜냐하면 이 컴포넌트들은 곧바로 식별자를 수집하기 때문이죠 방금 말씀드렸듯이 AnyView나 치우친 조건문은 사용하지 말아 주세요 명시적 스택을 쓰는 게 적절한 경우에는 써도 되지만 listRowBackground 같은 몇몇 수정자는 스택 안이 아니라 스택 뒤로 가야 한다는 걸 기억하시고요 마지막으로, 중첩된 ForEach 구성은 가능한 한 평면화해 주세요 하지만 중첩된 ForEach가 유용한 경우가 딱 하나 있는데 바로 섹션화된 리스트입니다 예를 들어봅시다 이 예시에서 전 개들이 각자 좋아하는 장난감에 따라 개의 리스트를 섹션화했습니다 ForEach를 사용해 동적인 개수의 섹션을 만들고 ForEach를 중첩해 각 섹션마다 동적인 개수의 행을 만들겠습니다 리스트는 모든 식별자를 가져와야 하지만 여기서는 섹션이 사용됐기 때문에 SwiftUI는 이 구성을 이해하고 이 리스트가 빨리 렌더링될 수 있도록 합니다 동적 섹션은 중첩된 ForEach 사용이 권장되는 좋은 예입니다 이게 기본 공식인데 각 리스트에 있는 ForEach로부터 비롯한 행 개수는 엘리먼트의 개수를 각 엘리멘트가 생성한 뷰의 개수와 곱한 것과 같습니다 엘리먼트당 뷰의 개수는 반드시 상수로 유지해야 합니다 안 그러면 SwiftUI가 식별자에다가 뷰까지 빌드해야 비로소 행을 식별할 수 있으니까요 지금까지는 리스트 얘기만 했지만 이런 규칙은 대체로 테이블에도 적용됩니다 테이블은 뷰 대신 TableRow를 사용하고 TableRow는 반드시 단 하나의 행으로 분할됩니다 테이블의 예시를 하나 봅시다 여기에 나오는 개 테이블 안에는 ForEach가 하나 들어갑니다 TableRow는 항상 단일 행이므로 행의 총개수는 개 컬렉션의 엘리먼트 개수와 동일합니다 이 구성은 너무 흔하기 때문에 iOS 17과 macOS Sonoma에서 SwiftUI가 새로 제공하는 간소화한 이니셜라이저를 사용하면 데이터 컬렉션에 ForEach를 그냥 쓸 수 있고 이니셜라이저가 테이블 행을 대신 만들어 줍니다 이 이니셜라이저는 새로 나왔지만 테이블을 사용할 수 있는 과거 모든 버전의 운영 체제에도 역배포할 수 있습니다 이 구성은 더욱 단순할 뿐만 아니라 ForEach 콘텐츠의 행 개수를 강제로 상수로 유지시킴으로써 식별 성능에 도움을 줍니다 그런데 전 새로운 시맨틱 변화가 있다는 걸 알려드리고자 합니다 여러분의 코드가 이렇다면 최신 버전 OS에서는 다르게 동작할지도 모릅니다 이 예시에서는 개마다 ForEach가 있어서 새로운 개 행이 만들어집니다 그런데 여기 있는 개들은 짝이 맞지 않는군요 여기서 값에 해당하는 건 개의 단짝 친구입니다 iOS 16에서는 각 행이 행의 값으로 식별됐는데 iOS 17에서는 성능을 개선하기 위해 이 동작을 바꿨습니다 각 테이블 행을 식별하기 위해 ForEach를 조사할 필요가 더는 없기 때문입니다 그래서 이 예시에는 이제 TableRow의 값 대신 개 각각의 ID가 있습니다 역배포를 해야 해서 동작을 예전과 같게 하려면 컬렉션 위에 매핑하거나 명시적으로 ID 키 패스를 지정하면 됩니다
이게 기본 공식인데 리스트의 ForEach에서 비롯한 행 개수는 엘리먼트의 개수를 각 엘리먼트가 생성한 뷰의 숫자와 곱한 것과 같습니다 테이블도 비슷하지만 이 경우엔 엘리먼트당 TableRow의 개수입니다 리스트와 테이블을 빠르게 하는 팁과 요령을 다뤘는데 요는 식별자를 싸게 생성할 수 있어야 하고 ForEach 콘텐츠의 뷰 개수는 일정해야 한다는 것입니다 오늘 많은 내용을 다뤘네요 먼저 그래프를 살펴봄으로써 종속성을 이해하고 최적화했고 느린 업데이트와 반응성 개선 문제를 다뤘습니다 마지막으로는 아이덴티티가 리스트와 테이블에 중요하다는 걸 배웠고요 제대로 된 멘탈 모델만 있다면 개발 과정 초기부터 성능이 훌륭해지게 할 수 있습니다 그러면 앱의 세부 사항에 더 집중할 수 있게 되죠 시청해 주셔서 감사합니다 ♪ ♪
-
-
3:59 - DogView
struct DogView: View { @Environment(\.isPlayTime) private var isPlayTime var dog: Dog var body: some View { Text(dog.name) .font(nameFont) Text(dog.breed) .font(breedFont) .foregroundStyle(.secondary) ScalableDogImage(dog) DogDetailView(dog) LetsPlayButton() .disabled(dog.isTired) } } }
-
4:00 - ScalableDogImage
struct ScalableDogImage: View { @State private var scaleToFill = false var dog: Dog var body: some View { dog.image .resizable() .aspectRatio( contentMode: scaleToFill ? .fill : .fit) .frame(maxHeight: scaleToFill ? 500 : nil) .padding(.vertical, 16) .onTapGesture { withAnimation { scaleToFill.toggle() } } } }
-
4:01 - printChanges
expression Self._printChanges()
-
4:02 - ScalableDogImage + printChanges
struct ScalableDogImage: View { @State private var scaleToFill = false var dog: Dog var body: some View { let _ = Self._printChanges() dog.image .resizable() .aspectRatio( contentMode: scaleToFill ? .fill : .fit) .frame(maxHeight: scaleToFill ? 500 : nil) .padding(.vertical, 16) .onTapGesture { withAnimation { scaleToFill.toggle() } } } }
-
8:46 - ScaleableDogImage
struct ScalableDogImage: View { @State private var scaleToFill = false var dog: Dog var body: some View { dog.image .resizable() .aspectRatio( contentMode: scaleToFill ? .fill : .fit) .frame(maxHeight: scaleToFill ? 500 : nil) .padding(.vertical, 16) .onTapGesture { withAnimation { scaleToFill.toggle() } } } }
-
8:47 - Updated DogView
struct DogView: View { @Environment(\.isPlayTime) private var isPlayTime var dog: Dog var body: some View { Text(dog.name) .font(nameFont) Text(dog.breed) .font(breedFont) .foregroundStyle(.secondary) ScalableDogImage(dog) DogDetailView(dog) LetsPlayButton() .disabled(dog.isTired) } } }
-
8:48 - Final DogView
struct DogView: View { @Environment(\.isPlayTime) private var isPlayTime var dog: Dog var body: some View { DogHeader(name: dog.name, breed: dog.breed) ScalableDogImage(dog.image) DogDetailView(dog) LetsPlayButton() .disabled(dog.isTired) } } }
-
12:22 - DogRootView and FetchModel
struct DogRootView: View { @State private var model = FetchModel() var body: some View { DogList(model.dogs) } } @Observable class FetchModel { var dogs: [Dog] init() { fetchDogs() } func fetchDogs() { // Takes a long time } }
-
12:23 - Updated DogRootView and FetchModel
struct DogRootView: View { @State private var model = FetchModel() var body: some View { DogList(model.dogs) .task { await model.fetchDogs() } } } @Observable class FetchModel { var dogs: [Dog] init() {} func fetchDogs() async { // Takes a long time } }
-
15:12 - List
List { ForEach(dogs) { DogCell(dog: $0) } }
-
16:08 - List Again
List { ForEach(dogs) { DogCell(dog: $0) } }
-
17:35 - List Fixed
List { ForEach(tennisBallDogs) { dog in DogCell(dog) } }
-
18:25 - Sectioned List
// Sectioned example struct DogsByToy: View { var model: DogModel var body: some View { List { ForEach(model.dogToys) { toy in Section(toy.name) { ForEach(model.dogs(toy: toy)) { dog in DogCell(dog) } } } } } }
-
19:21 - DogTable
struct DogTable: View { var dogs: [Dog] var body: some View { Table(of: Dog.self) { // Columns } rows: { ForEach(dogs) { dog in TableRow(dog) } } } }
-
19:22 - DogTable Brief
struct DogTable: View { var dogs: [Dog] var body: some View { Table(of: Dog.self) { // Columns } rows: { ForEach(dogs) } } }
-
20:06 - DogTable Different IDs
struct DogTable: View { var dogs: [Dog] var body: some View { Table(of: Dog.self) { // Columns } rows: { ForEach(dogs) { dog in TableRow(dog.bestFriend) } } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.