View in English

  • 메뉴 열기 메뉴 닫기
  • Apple Developer
검색
검색 닫기
  • Apple Developer
  • 뉴스
  • 둘러보기
  • 디자인
  • 개발
  • 배포
  • 지원
  • 계정
페이지에서만 검색

빠른 링크

5 빠른 링크

비디오

메뉴 열기 메뉴 닫기
  • 컬렉션
  • 주제
  • 전체 비디오
  • 소개

WWDC25 컬렉션으로 돌아가기

  • 소개
  • 요약
  • 자막 전문
  • 코드
  • Instruments로 SwiftUI의 성능 최적화하기

    새로운 SwiftUI 도구를 알아보세요. SwiftUI가 뷰를 업데이트하는 방법, 앱 데이터 변화가 업데이트에 미치는 영향 및 새로운 도구가 원인과 결과를 시각화하는 방법을 살펴봅니다. 이 세션을 최대한 활용하려면 SwiftUI에서 앱를 작성하는 방법을 먼저 익히는 것이 좋습니다.

    챕터

    • 0:00 - 서론 및 어젠다
    • 2:19 - SwiftUI 도구 알아보기
    • 4:20 - 긴 뷰 본문 업데이트 진단 및 수정하기
    • 19:54 - SwiftUI 업데이트의 원인과 결과 이해하기
    • 35:01 - 다음 단계

    리소스

    • Analyzing the performance of your visionOS app
    • Improving app responsiveness
    • Measuring your app’s power use with Power Profiler
    • Performance and metrics
    • Understanding and improving SwiftUI performance
      • HD 비디오
      • SD 비디오

    관련 비디오

    WWDC25

    • Instruments를 사용하여 CPU 성능 최적화하기

    WWDC23

    • Instrumets로 행 분석하기
    • SwiftUI 성능 쉽게 이해하기
    • SwiftUI 애니메이션 살펴보기

    WWDC22

    • SwiftUI로 맞춤형 레이아웃 작성

    WWDC21

    • Demystify SwiftUI

    Tech Talks

    • Explore UI animation hitches and the render loop
  • 비디오 검색…

    안녕하세요, 저는 Instruments 팀의 Jed입니다 저는 Apple Music 팀의 Steven입니다 훌륭한 앱은 성능이 뛰어납니다 앱에서 실행되는 모든 코드는 속도를 저하시킬 가능성이 있죠 앱을 분석해 코드에서 병목 현상에 해당하는 영역을 파악하고 이 문제를 해결해 앱이 최대한 원활하게 실행되도록 해야 합니다 오늘 세션에서는 SwiftUI 코드가 병목 현상인 경우를 파악하는 방법을 알아보고 SwiftUI가 더 효율적으로 작동하게 하는 방법을 안내합니다 애초에 성능 문제가 있음을 어떻게 알 수 있을까요? 한 가지 증상은 끊김이나 무반응으로 인해 앱의 반응성이 떨어지는 것입니다 애니메이션이 일시 정지되거나 튀고 스크롤이 느릴 수 있습니다 성능 문제를 식별하는 가장 좋은 방법은 Instruments를 사용해 앱을 프로파일링하는 것입니다 오늘은 SwiftUI를 사용하는 코드에서 성능 문제를 진단하는 데 중점을 둡니다 먼저 Instruments 26에 포함된 새로운 SwiftUI instrument를 소개하겠습니다 다음으로 긴 보기 본문 업데이트가 있는 앱을 살펴보고 이것이 일반적인 성능 문제인 이유를 설명하고 instrument를 사용해 문제를 찾고 해결합니다 마지막으로 SwiftUI 업데이트의 원인과 효과를 알아봅니다 불필요한 업데이트를 식별하는 데 instrument를 사용할 것이며 해당 업데이트를 제거하는 방법도 안내합니다 성능 문제는 근본 원인이 다양할 수 있는데 오늘은 SwiftUI 사용으로 발생하는 문제에 집중합니다

    앱의 문제가 SwiftUI 코드와 관련이 없다면 “Instruments로 행 분석하기”와 “Instruments로 CPU 성능 최적화하기”를 확인해 보세요 상태를 파악하는 데 시작점을 제시해 줍니다

    Steven과 제가 함께 작업 중인 앱이 있습니다 Steven, 지금까지 빌드한 걸 보여 주시겠어요? 감사합니다, Jed Landmarks라는 앱인데요 세계 곳곳의 놀라운 장소를 소개하는 앱입니다 각 명소는 현재 위치와의 거리가 표시되어 있어 다음 여행지로 참고할 수 있습니다 장시간 비행기를 타고 가야 할 수도 있고 자동차로 잠깐일 수도 있죠 앱이 꽤 괜찮아 보이지만 테스트해 보니 가끔 스크롤이 기대만큼 매끄럽지 않았습니다 그 이유를 확인하고 싶습니다 Jed, 새로운 SwiftUI instrument를 언급하셨죠 설명해 주시겠어요? 물론입니다 Instruments 26에 SwiftUI 앱의 성능 문제를 식별하는 새로운 방법이 도입됩니다 차세대 SwiftUI instrument죠

    업데이트된 SwiftUI 템플릿에는 앱 성능을 평가할 수 있는 몇 가지 instrument가 있습니다 먼저 새 SwiftUI instrument인데 잠시 후 자세히 설명하겠습니다

    다음은 Time Profiler입니다 시간 경과에 따라 앱이 CPU에서 수행하는 작업 샘플을 보여 주죠 마지막으로 Hangs 및 Hitches instrument가 있습니다 앱의 반응성을 추적하는 역할을 합니다

    앱에서 잠재적인 성능 문제를 조사할 때 첫 번째 단계는 SwiftUI instrument가 제공하는 최상위 정보를 살펴보는 것입니다

    SwiftUI instrument 트랙의 첫 번째 행은 “Update Groups”죠 SwiftUI가 작동 중일 때를 보여 줍니다

    이 행이 비어 있을 때 CPU 사용량이 급증하면 SwiftUI 외부에 문제가 있을 가능성이 높습니다 SwiftUI instrument 트랙의 다른 행에서는 긴 SwiftUI 업데이트와 발생 시점을 쉽게 확인 가능하고요

    Long View Body Updates는 보기의 ‘body’ 속성 실행이 너무 오래 걸릴 때를 강조 표시합니다 Long Representable Updates는 너무 오래 걸리는 보기 및 보기 컨트롤러 표현 가능 업데이트를 식별합니다

    마지막으로 Other Long Updates는 기타 긴 SwiftUI 작업 유형입니다 이 3개 행으로 앱의 성능을 저하시킬 수 있는 긴 업데이트를 모두 심층적으로 볼 수 있습니다 업데이트는 끊김이나 무반응에 기여할 가능성 수준에 따라 주황색과 빨간색으로 표시됩니다 이러한 업데이트가 실제로 앱에서 끊김이나 무반응을 유발하는지 여부는 기기 조건에 따라 다르지만 이러한 긴 업데이트를 빨간색부터 조사해 보는 것이 일반적으로 좋은 시작점입니다

    SwiftUI instrument를 시작하려면 Xcode 26을 설치합니다 그런 다음 이것을 실행해 앱을 프로파일링할 기기에서 최신 OS 릴리즈로 업데이트합니다 여기에는 SwiftUI 추적 기록 지원이 포함됩니다 처음으로 Landmarks 앱을 프로파일링할 준비가 되었습니다 -Steven, 계속 진행해 주시죠 -고맙습니다, Jed

    Xcode에서 이미 프로젝트가 열려 있습니다 프로파일링을 시작하기 위해 Command-I를 누르면 Xcode가 릴리즈 모드에서 앱을 컴파일한 후 Instruments를 자동 실행합니다

    템플릿 선택기에서 SwiftUI 템플릿을 선택하고

    기록 버튼을 클릭해 기록을 시작합니다

    먼저 명소 목록을 스크롤합니다 대륙마다 수평 선반이 있습니다

    북미 선반까지 수평으로 스크롤해 보기를 더 로드합니다

    그리고 기록 중지를 클릭합니다

    기록이 중지되면 Instruments가 기기의 프로파일링 데이터를 복사해 분석용으로 처리합니다 처리가 완료되면 SwiftUI Instrument를 사용해 대응해야 하는 잠재적 성능 문제가 있는지 판단할 수 있습니다 내용이 잘 보이도록 창을 최대화하겠습니다

    먼저 SwiftUI instrument 트랙의 최상위 긴 업데이트 행을 조사합니다

    실행에 너무 오래 걸리는 보기 본문이 SwiftUI 성능 문제의 일반적인 원인이므로 Long View Body Updates 행부터 보겠습니다

    주황색과 빨간색의 긴 업데이트가 몇 개 있습니다 조사해 볼까요 SwiftUI 트랙을 클릭해 확장합니다

    그러면 하위 트랙 3개가 열립니다 View Body Updates, Representable Updates, Other Updates입니다 각 하위 트랙에 주황색과 빨간색의 긴 업데이트가 있습니다 최상위 행과 동일하죠 나머지 업데이트는 회색입니다 View Body Updates 트랙을 선택합니다

    아래의 세부 정보 패널에 프로파일링 세션 중에 실행된 모든 보기 본문의 계층 요약이 있습니다 앱의 계층 구조 프로세스를 확장하면 실행된 모든 보기 본문 업데이트의 모듈 목록이 나옵니다

    긴 업데이트만 보이도록 필터링하려면 드롭다운을 클릭하고 Long View Body Updates 요약을 선택하면 됩니다

    개수를 보니 조사할 긴 업데이트가 많이 있군요

    앱 모듈을 클릭해 확장합니다 LandmarkListItemView에 긴 업데이트가 많으니 여기부터요

    보기 이름을 마우스로 가리키면 화살표가 나타납니다

    화살표를 클릭하면 빠른 메뉴가 표시됩니다 “Show Updates”를 선택합니다 이 보기 본문의 긴 업데이트 전체에 대한 순차적 목록이 나오죠

    긴 업데이트 하나를 마우스 오른쪽 버튼으로 클릭하고 “Set Inspection Range and Zoom” 을 클릭합니다

    그러면 이 보기 본문 업데이트의 간격에 대한 추적 선택이 설정되죠 Time Profiler instrument 트랙을 클릭합니다

    여기서 보기 본문이 실행 중일 때 CPU의 상태를 볼 수 있습니다

    Time Profiler는 CPU에서 실행 중인 작업을 정기적으로 샘플링해 데이터를 수집합니다 샘플마다 현재 실행 중인 함수를 확인하고 이 정보를 프로파일링 세션에 저장합니다 아래의 Profile 세부 정보 패널에 추적 중 기록된 샘플의 호출 스택이 표시됩니다 여기서는 보기 본문이 실행되는 동안 기록된 샘플입니다

    Option 키를 누른 채로 클릭하면 메인 스레드 호출 스택이 확장되죠

    SwiftUI 작업은 매우 깊은 호출 스택으로 표현됩니다 제가 가장 관심 있는 것은 LandmarkListItemView입니다 호출 스택을 검색하기 위해 Command-F를 눌러 검색 필드에 이름을 입력합니다

    여기 보기 본문이 있고요 가장 왼쪽 열에서 Time Profiler는 호출 스택의 프레임별로 소요된 시간을 표시합니다

    이 열을 보니 보기 본문에서 소요된 시간 대부분이 distance라는 계산된 속성에서 발생했습니다 distance에서 가장 무거운 프레임 둘은 두 형식자에 대한 호출입니다

    이 측정 형식자와 이 숫자 형식자입니다 Xcode로 돌아가서 코드의 상황을 확인해 보겠습니다

    LandmarkListItemView이고요 목록의 각 명소에 대한 보기입니다

    여기 Time Profiler에서 본 distance 속성이 있습니다 이 속성은 명소와의 거리를 형식화된 문자열로 변환해 보기에 표시합니다

    이것이 숫자 형식자인데 Time Profiler에 따르면 생성 비용이 많이 듭니다

    여기서 측정 형식자가 문자열을 생성합니다 이것도 보기 본문에서 소요되는 시간에 크게 기여했죠

    보기 본문에서 레이블의 텍스트를 빌드하기 위해 distance 속성을 읽고 있습니다 보기 본문이 실행될 때마다 발생하고요 보기 본문이 메인 스레드에서 실행되기 때문에 앱이 거리 텍스트가 형식화될 때까지 기다렸다가 UI를 업데이트해야 합니다

    이것이 왜 중요할까요? 보기 본문을 실행하는 데 걸리는 1밀리초는 찰나일지 몰라도 누적된 총 소요 시간은 상당할 수 있습니다 특히 화면에 SwiftUI가 업데이트할 보기가 많다면 말입니다 Jed, SwiftUI가 보기 본문을 실행하는 데 걸리는 시간을 어떻게 생각해야 할까요? 좋은 질문입니다 먼저 Apple 플랫폼에서 렌더링 루프가 작동하는 방식을 설명하죠 앱은 프레임마다 깨어나 터치, 키 눌림 같은 이벤트를 처리합니다 그런 다음 UI를 업데이트합니다 이때 변경된 SwiftUI 보기의 body 속성을 실행합니다 이 모두가 각 프레임 데드라인 전에 완료되어야 합니다 이후 앱은 작업을 시스템에 넘기고 시스템은 다음 프레임 데드라인 전까지 보기를 렌더링합니다 렌더링된 출력은 해당 데드라인 직후에 화면에 표시됩니다 여기서는 모든 것이 제대로 작동하고 있습니다 업데이트가 해당 프레임 데드라인 전에 완료되므로 시스템이 각 프레임을 렌더링하고 화면에 표시하기에 시간이 충분합니다 보기 본문이 너무 오래 걸려 끊김이 있는 앱과 비교해 보죠

    역시 먼저 이벤트를 처리하고요 그리고 UI 업데이트를 실행합니다 하지만 첫 번째 프레임에서 한 UI 업데이트가 너무 오래 걸렸습니다 그래서 UI 업데이트 부분이 프레임 데드라인 후에 실행되었죠 즉 다음 업데이트는 한 프레임 뒤에야 시작 가능합니다 이 프레임은 데드라인에 렌더러에 넘길 항목이 준비되어 있지 않고요

    결과적으로 시스템이 다음 프레임 렌더링을 끝낼 때까지 이전 프레임이 화면에 계속 표시됩니다 프레임이 화면에 너무 오래 표시되어 후속 프레임이 지연되는 것을 끊김이라고 합니다 끊김이 있으면 애니메이션이 부드러워 보이지 않습니다 끊김에 대한 자세한 내용은 “앱의 끊김 이해하기” 문서를 확인하세요 렌더링 루프와 다양한 끊김을 해결하는 방법을 자세히 설명하는 Tech Talk도 있고요 Steven, 보기 본문 런타임이 중요한 이유가 이해되셨나요? 네, 큰 도움이 되었습니다 보기 본문 업데이트가 필요 이상으로 오래 실행되면 앱이 프레임 데드라인을 놓쳐 끊김이 발생할 위험이 있습니다 따라서 보기 표시 전에 랜드마크별 distance 문자열을 계산하고 캐싱할 방법이 필요합니다 본문이 실행 중일 때 하는 것이 아니라요 코드로 돌아가 볼까요

    여기 보기 본문이 업데이트될 때마다 실행되는 distance 속성이 있습니다 보기 본문이 실행되는 동안 이 작업을 수행하는 대신 좀 더 중앙화된 위치로 옮기겠습니다 위치 업데이트를 관리하는 클래스죠

    LocationFinder 클래스는 위치가 변경될 때마다 업데이트를 받는 것을 담당합니다 보기 본문에서 형식화된 distance 문자열을 계산하는 대신 이 문자열을 미리 생성하고 여기에 캐싱해 보기에 이를 표시해야 할 때마다 이미 계산된 상태가 되도록 할 수 있습니다

    먼저 이니셜라이저를 업데이트해 앞서 보기 본문에서 만들었던 형식자를 만듭니다

    formatter라는 속성을 추가해 측정 형식자를 저장했고요

    이니셜라이저 상단에 앞서 보기에서 만들었던 숫자 형식자를 만듭니다

    측정 형식자는 아까 추가한 새 속성에 저장합니다 형식은 변하지 않기 때문에 distance 문자열을 업데이트해야 할 때마다 형식자를 다시 사용해 보기 본문이 실행될 때마다 새 형식자를 다시 만드는 비용을 방지할 수 있습니다 다음으로 문자열을 캐싱된 상태로 유지해 필요할 때 보기에서 사용하도록 할 방법이 필요합니다 해당 업데이트를 관리하는 코드를 추가합니다

    명소를 저장하는 배열이 있는데 이것으로 거리를 계산하고요

    계산 후 distance 문자열을 캐싱하는 딕셔너리도 있습니다

    updateDistances라는 이 함수는 위치가 변경될 때마다 문자열을 다시 계산합니다

    여기서 형식자를 사용해 거리 텍스트를 생성합니다

    그리고 여기 캐시에 텍스트를 저장합니다

    잠시 후 보기에서 이 마지막 함수를 호출해 캐싱된 텍스트를 가져옵니다

    마지막으로 할 일이 있는데요 위치가 업데이트되면 문자열 캐시를 업데이트해야 합니다

    점프 막대 드롭다운을 클릭해

    didUpdateLocations 함수로 점프합니다 위치가 변경될 때 CoreLocation이 호출하는 함수죠

    여기서 앞서 만든 updateDistances 함수를 호출합니다

    이제 보기로 돌아가겠습니다

    그리고 캐싱된 값을 사용하도록 보기를 업데이트합니다

    이렇게 변경하면 느린 보기 본문 업데이트 문제가 해결될 것입니다

    이제 해결책이 구현된 후 생성된 Instruments 추적을 보며 상황이 개선되었는지 확인해 보죠

    View Body Updates 트랙을 선택해 세부 정보 패널의 Long View Body Updates 요약을 보면 LandmarkListItemView의 긴 업데이트가 사라졌습니다

    요약에 긴 보기 본문 업데이트가 아직 두 개 더 있지만 이러한 업데이트는 추적 극초반에 발생한다는 점이 중요합니다 앱이 첫 번째 프레임 렌더링을 준비하는 시점이죠 앱 실행 직후 시스템이 앱의 초기 보기 계층 구조를 빌드하는 동안 업데이트가 더 오래 걸리는 경우는 흔합니다 하지만 이 때문에 끊김이 발생하지는 않습니다 여기서 중요한 것은 스크롤 시 끊김을 유발한 긴 LandmarkListItemView 업데이트 문제가 해결되어 목록에서 사라졌다는 점입니다 이번 수정으로 SwiftUI가 모든 보기를 화면에 표시하는 과정에서 SwiftUI 속도를 저해하지 않는다는 확신이 생긴 거죠

    긴 보기 본문 업데이트 문제를 해결하면 앱의 성능을 향상할 수 있습니다 하지만 고려할 다른 사항이 있는데 불필요한 보기 본문 업데이트가 너무 많으면 성능 문제가 발생할 수 있다는 겁니다 이유를 살펴보겠습니다

    이것은 Jed가 보여 드린 다이어그램입니다 이번에는 나머지보다 긴 단일 업데이트가 없습니다 대신 비교적 빠르면서 이 프레임 중에 모두 발생해야 하는 업데이트가 다수 있습니다

    이 모든 추가 작업으로 앱이 프레임을 제출할 데드라인을 놓칩니다 아까처럼 다음 업데이트가 한 프레임만큼 지연되고요 렌더러에 넘길 항목이 없기 때문에 마찬가지로 끊김이 발생합니다 이전 프레임이 두 프레임 동안 계속 표시되니까요 불필요한 보기 업데이트의 잠재적인 성능 영향을 언급하는 이유는 지금 작업 중인 앱의 미래에서 이 부분이 중요할 것으로 생각되기 때문입니다 모든 명소를 스크롤하면서 새로운 장소를 방문할 생각에 들떴는데 막상 어디로 갈지 우선순위를 정하기가 어렵습니다 이 문제를 해결할 아이디어가 떠올랐는데요 보여 드리겠습니다 각 명소에 하트 버튼을 새로 추가했습니다 탭하여 즐겨찾기를 추가하거나 제거할 수 있죠

    코드를 함께 볼까요

    LandmarkListItemView에서 새 하트 버튼을 표시하는 오버레이를 추가했습니다

    버튼의 동작은 모델 데이터 클래스에서 toggleFavorite 함수를 호출해 명소 즐겨찾기를 추가/제거합니다

    즐겨찾기 추가 시 안이 찬 하트 즐겨찾기 제거 시 안이 빈 하트가 레이블 아이콘에 표시됩니다

    toggleFavorite을 Command 클릭해 이 함수로 점프합니다

    즐겨찾기 추가 방식은 이렇고요

    모델은 즐겨찾기 명소 배열을 저장하며 저는 즐겨찾기가 추가되면 명소를 배열에 추가합니다

    즐겨찾기를 제거하려면 반대로 하면 됩니다

    지금까지 작업한 내용은 이 정도입니다 기능을 좀 더 손봐야겠지만 Instruments에서 초기에 그리고 개발 중 자주 프로파일링하면 좋습니다 이제 새 기능이 어떤지 보겠습니다 Command-I를 눌러 앱을 빌드하고 Instruments로 전환해

    기록 버튼을 다시 클릭합니다

    아까처럼 북미 목록까지 스크롤해

    오른쪽에서

    좋아하는 뮤어 우즈의 하트를 탭해 즐겨찾기에 추가합니다 제가 사는 곳에서 멀지 않은데 아직 못 가 봤네요 다시 위로 스크롤합니다 먼 곳을 즐겨찾기에 추가해 보죠 후지산은 어떨까요? 재미있는 여행이 될 것 같네요 이제 기록을 중지합니다

    새 즐겨찾기 버튼을 탭할 때 불필요한 추가 업데이트가 발생하지 않는지 확인하고 싶습니다 예상치에 대한 아이디어를 염두에 두고 추적을 분석하고 이상해 보이는 부분에 집중하면 잠재적 문제를 식별하는 데 효과적일 수 있습니다

    SwiftUI instrument 트랙을 클릭해 확장하고 View Body Updates 하위 트랙을 선택합니다

    뮤어 우즈, 후지산과 같이 두 즐겨찾기 버튼을 탭했으므로 해당 두 보기가 업데이트되었어야 합니다 추적의 후반부에 있는 버튼까지 스크롤한 후 이를 탭했습니다 추적의 해당 부분을 강조 표시하죠 필요한 곳에 집중할 수 있게요

    이제 아래의 세부 정보 패널을 보겠습니다 계층 구조를 확장해 보기의 업데이트 목록을 찾습니다

    LandmarkListItemView가 여러 번 업데이트된 것이 예상 밖입니다 이유가 뭘까요? 저는 UIKit 앱에서 보기 업데이트를 디버깅할 때 코드에 중단점을 두고 백트레이스를 검사해 보기가 업데이트된 이유를 찾곤 합니다 하지만 Landmarks 같은 SwiftUI 앱에는 맞지 않았습니다 SwiftUI 호출 스택은 이해하기 더 어렵더군요 Jed, 이 접근 방식이 SwiftUI 앱에 맞지 않는 이유는 뭘까요? 설명해 드리겠습니다 Xcode는 UIKit 앱 등에서 명령형 코드의 원인과 효과를 이해하는 데 도움이 됩니다 중단점에 도달하면 백트레이스를 보여 주거든요 UIKit은 명령형 프레임워크라 백트레이스가 흔히 원인과 효과를 디버깅하는 데 유용합니다 여기서 viewDidLoad에 isOn 속성을 설정했기 때문에 레이블이 업데이트되고 있음을 알 수 있습니다 백트레이스에 있는 일부 시스템 프레임의 이름으로 추측하건대 앱이 첫 번째 장면을 실행할 때 발생했고요 동일한 작업을 수행하는 비슷한 SwiftUI 앱과 비교하니 SwiftUI 내부 항목에 재귀적 업데이트가 많습니다 각기 AttributeGraph라는 항목 안의 프레임으로 구분되죠 하지만 보기를 업데이트해야 하는 구체적인 이유는 알 수 없습니다

    SwiftUI는 선언형이라 백트레이스로는 보기 업데이트 이유를 이해할 수 없습니다 그렇다면 SwiftUI 보기 업데이트의 원인을 어떻게 알 수 있을까요? 먼저 SwiftUI의 작동 방식을 이해해야 합니다

    간단한 예시 보기를 보면서 SwiftUI의 데이터 모델인 AttributeGraph가 보기 간 종속성을 정의하고 불필요한 보기 재실행을 방지하는 방법을 설명하겠습니다 자세한 내용을 전부 다루지는 않겠지만 이 섹션은 앱에서 업데이트가 흐르는 방식을 이해하는 데 기초가 되어 줍니다

    보기는 View 프로토콜 준수를 선언합니다 그런 다음 본문 속성을 구현해 다른 View 값을 반환하여 모습과 동작을 정의합니다

    여기서 OnOffView는 본문에서 Text 보기를 반환하고 isOn 상태 변수의 값에 따라 변경되는 레이블을 전달합니다

    이 보기가 보기 계층 구조에 처음 추가되면 SwiftUI는 보기 구조체를 저장하는 상위 보기에서 속성이라는 객체를 수신합니다 보기 구조체는 자주 재생성되지만 속성은 보기의 전체 수명에서 정체성과 상태를 유지합니다 따라서 상위 보기가 업데이트되면 이 속성의 값은 변경되지만 정체성은 변하지 않습니다 보기는 자체 속성을 생성해 상태를 저장하고 동작을 정의하도록 요청받습니다 보기는 먼저 isOn 상태 변수를 위한 저장소와 이 상태 변수가 변경되었을 때 추적하는 속성을 생성합니다 그런 다음 보기는 이 두 가지를 기준으로 본문을 실행하는 새로운 속성을 생성합니다 보기 본문 속성은 새 값을 생성하라는 요청을 받을 때마다 상위 보기에서 전달된 현재 보기의 값을 읽습니다 다음으로 속성이 상태 변수의 현재 값으로 해당 보기 구조체의 사본을 업데이트합니다 그런 다음 해당 보기 임시 사본의 ‘body’ 계산 속성에 접근해 이것이 속성의 업데이트된 값으로 반환하는 값을 저장합니다 보기의 본문이 Text 보기를 반환했으므로 SwiftUI는 텍스트를 표시하는 데 필요한 속성을 설정합니다

    텍스트 보기는 환경에 따라 속성을 생성해 전경 색상, 서체 등 현재 기본 스타일에 접근하여 렌더링된 텍스트의 모습을 결정합니다 이 속성은 보기 본문에서 종속성을 추가해 사용자가 반환한 Text 구조체로 렌더링할 문자열에 접근합니다 마지막으로 Text는 스타일 지정된 텍스트를 기준으로 렌더링할 사항의 설명을 빌드하는 또 다른 속성을 생성합니다

    이제 상태 변수를 변경하면 어떻게 되는지 설명하겠습니다 이 변수 변경 시 SwiftUI는 보기를 즉시 업데이트하지 않습니다 대신 새로운 트랜잭션을 생성하죠 트랜잭션은 SwiftUI 보기 계층 구조의 변경 사항으로서 다음 프레임 전에 적용해야 하는 것입니다

    이 트랜잭션은 상태 변수의 신호 속성을 오래된 것이라고 표시합니다 그런 다음 SwiftUI는 다음 프레임을 업데이트할 준비가 되면 트랜잭션을 실행하고 예정된 업데이트를 적용합니다 이제 속성이 오래된 것으로 표시되었으므로

    SwiftUI가 오래된 상태의 속성에 종속된 속성 체인을 살펴보며 각 해당 속성에 플래그를 설정해 오래된 것으로 표시합니다 플래그 설정은 매우 빠르며 아직 추가 작업이 필요 없습니다 다른 트랜잭션을 모두 실행한 후 SwiftUI는 이 프레임의 화면에 드로우할 내용을 파악해야 하죠 하지만 해당 정보가 오래된 것으로 표시되어 접근할 수 없습니다

    따라서 SwiftUI는 이 정보의 모든 종속성을 업데이트해 드로우할 내용을 결정해야 합니다

    우선은 State 신호처럼 오래된 종속성이 없는 것부터 시작합니다 이제 보기 본문 속성을 업데이트할 준비가 되었습니다 재실행으로 업데이트된 문자열이 있는 새로운 Text 구조체 값이 생성되어 기존 Apply 스타일 지정 속성에 전달되고 드로우해야 하는 사항을 파악하는 데 필요한 모든 속성이 업데이트될 때까지 업데이트가 계속됩니다 이제 SwiftUI는 질문의 답을 확보했습니다 화면에 드로우해야 하는 내용이요

    “보기 본문이 왜 실행되었는가?” 하는 질문은 정확히 하자면 “보기 본문이 오래된 것으로 표시되는 이유는 무엇인가?”죠 다른 보기 같은 종속성이 보기 본문을 오래된 것으로 표시하는 시점은 대개 제어 가능한데 특히 여러분의 자체 보기일 때 그렇죠 다만 SwiftUI는 보기를 표시하기 위한 추가 작업도 수행합니다 이 작업은 필수이고 일반적으로 회피할 수 없지만 언제 발생하는지 이해하면 유용할 수 있습니다 보기 업데이트의 원인과 효과에 대한 정보를 확보하는 것은 새로운 SwiftUI instrument의 주요 기능입니다 원인 및 효과 그래프는 이 모든 원인 및 효과 관계를 기록해 이런 그래프로 표시합니다

    조사 중인 보기 본문 업데이트부터 볼까요 업데이트는 보기 본문 업데이트임을 나타내는 아이콘과 해당하는 보기 유형을 나타내는 제목이 있는 노드로 표시됩니다

    State 변경을 나타내는 노드에서 해당 노드로 향하는 화살표가 있고 “Update”라고 레이블 지정되어 있습니다 상태 변경으로 인해 보기가 업데이트되었기 때문입니다 “Creation”이라고 표시된 에지도 있는데 보기가 보기 계층 구조에 처음 나타난 이유를 알려 줍니다

    상태 변경 노드에는 상태 변수의 이름과 연결된 보기의 유형을 알려 주는 제목이 있습니다 상태 변경 사항을 선택하면 값이 업데이트된 곳의 백트레이스가 표시됩니다

    원인 및 효과 그래프의 왼쪽으로 계속 가면 버튼 탭 같은 제스처 때문에 상태가 변경되었음을 알 수 있죠

    Steven, Landmarks 앱의 원인 그래프는 어떨까요? 원인 및 효과 그래프를 확인해 추가 보기 본문 업데이트가 발생한 이유를 확인해 보죠

    이것이 원인 및 효과 그래프 보기입니다 LandmarkListItemView.body의 노드가 선택되었고요 그래프의 파란색 노드는 자체 코드의 일부 또는 앱과 상호작용할 때 제가 수행한 동작을 나타냅니다 그래프는 왼쪽에서 오른쪽으로 원인 및 효과 체인을 보여 줍니다

    “Gesture” 노드는 즐겨찾기 버튼 탭 동작을 나타냅니다

    이로 인해 즐겨찾는 명소 배열이 업데이트되면서

    LandmarkListItemView의 본문이 여러 번 업데이트되었습니다 예상보다 많네요

    즐겨찾기 버튼을 탭했을 때 화면에서 탭한 항목 하나가 아니라 여러 항목 보기가 업데이트되는 것 같습니다 코드로 돌아가서 상황을 알아보겠습니다

    LandmarkListItemView로 다시 전환하고요

    명소가 즐겨찾기로 표시되었는지 확인하는 방법은 modelData.isFavorite을 호출하고 명소를 전달하는 것입니다 ModelData는 최상위 모델 객체이고 @Observable 매크로를 사용해 속성이 변경될 때마다 SwiftUI가 보기를 업데이트하도록 허용합니다 isFavorite을 Command 클릭해 이 함수로 점프합니다

    favoritesCollection.landmarks 배열에 접근해 이 명소가 즐겨찾기인지 확인합니다 이로 인해 @Observable이 각 항목 보기와 전체 즐겨찾기 배열 간에 종속성을 설정합니다 그래서 배열에 즐겨찾기를 추가할 때마다 배열이 변경되어 모든 항목 보기의 본문이 실행되는 것입니다 어떻게 작동하는지 보여 드리죠

    이것은 LandmarkListItemView 중 일부이고요 이것은 favoritesCollection이 있는 ModelData 클래스로서 즐겨찾기 명소를 추적합니다 현재 유일한 즐겨찾기는 두 번째 명소입니다 � ModelData 클래스에는 isFavorite 함수가 있습니다 각 LandmarkListItemView는 이 함수를 호출해 아이콘을 강조 표시할지 여부를 판단합니다

    isFavorite 함수는 컬렉션에 명소가 포함되어 있는지 확인하며 각 보기가 자체 버튼을 렌더링하죠 각 보기가 간접적으로라도 즐겨찾기 배열에 접근했기 때문에 @Observable 매크로가 전체 즐겨찾기 배열의 각 보기에 대해 종속성을 생성했습니다

    그러면 다른 보기에서 즐겨찾기 버튼을 탭해 새 즐겨찾기를 추가하려고 하면 어떤 일이 일어날까요? 보기가 toggleFavorite을 호출해 즐겨찾기에 새 명소가 추가됩니다 모든 LandmarkListItemView가 favoritesCollection에 대해 종속성이 있기 때문에 모든 보기가 오래된 것으로 표시되고 그 본문이 다시 실행되죠

    적절하지 않은 상황입니다 실제로 변경한 보기는 세 번째 보기 하나니까요 따라서 보기의 데이터 종속성을 더 세분화해 앱의 데이터가 변경되었을 때 필요한 보기 본문만 업데이트되도록 해야 합니다

    이제 다시 생각해 보죠 각 보기에는 자체 즐겨찾기 상태가 있는 명소가 있고 상태는 즐겨찾기에 추가됨 또는 제외됨입니다

    이러한 상태를 추적하기 위해 보기의 Observable 보기 모델을 생성합니다 모델에는 즐겨찾기 상태를 추적하는 isFavorite 속성이 있고 각 보기에는 자체 보기 모델이 있습니다

    이제 ModelData 클래스에 보기 모델을 저장할 수 있습니다 각 보기는 자체 모델을 검색하고 필요에 따라 즐겨찾기 상태를 전환할 수 있습니다 따라서 각 보기가 전체 즐겨찾기 배열에 종속되는 대신 각 보기가 자체 명소의 보기 모델에만 직접 종속됩니다 즐겨찾기를 하나 더 추가해 볼까요 버튼을 탭하면 toggleFavorite이 호출됩니다 그러면 첫 번째 보기의 보기 모델이 업데이트됩니다 첫 번째 보기는 자체 보기 모델에만 종속되어 있으므로 이 보기의 본문만 다시 실행됩니다 이렇게 변경한 결과 Landmarks가 어떻게 되었는지 보죠

    이것은 새로운 보기 모델 개선 사항을 구현한 후 기록한 추적입니다 View Body Updates 하위 트랙을 다시 클릭합니다 그리고 앞서의 타임라인에서 같은 부분을 선택합니다

    세부 정보 패널에서 과정과 Landmarks 모듈을

    확장합니다

    이제 업데이트가 두 개뿐입니다 즐겨찾기 두 개를 변경한 것에 부합하죠 그래도 그래프를 한 번 더 확인하겠습니다 보기 이름을 마우스로 가리키고 화살표를 클릭한 후 “Show Cause & Effect Graph”를 선택합니다

    그래프가 다시 표시되고요

    이제 @Observable 노드에서 보기 본문으로 향하는 화살표에 버튼당 하나씩 업데이트가 두 개만 표시됩니다 전체 즐겨찾기 배열에 대한 각 항목 보기의 종속성을 긴밀하게 결합된 보기 모델로 교체한 결과 불필요한 보기 본문 업데이트가 상당수 사라졌습니다 앱이 원활하게 실행되는 데 도움이 되겠죠 이 예에서 그래프는 비교적 작았는데 보기 본문 업데이트의 원인이 매우 제한적이었기 때문입니다 하지만 더 뚜렷한 원인이 있다면 그래프가 더 커질 수 있습니다 이렇게 될 수 있는 상황은 보기가 Environment에서 읽을 때입니다 Jed, 예를 보여 주시겠어요? 물론입니다 먼저 환경의 작동 방식을 설명하겠습니다 환경의 값은 EnvironmentValues 구조체에 저장되는데 이는 딕셔너리와 비슷한 값 유형입니다 각 보기는 전체 EnvironmentValues 구조체에 대한 종속성이 있습니다 각 보기가 환경 속성 래퍼를 사용해 환경에 접근하기 때문이죠 환경의 값이 업데이트되면 환경에 종속성이 있는 각 보기는 본문을 실행해야 할 수 있다는 알림을 받습니다 그런 다음 각 보기는 읽고 있는 값이 변경되었는지 확인합니다 값이 변경되었으면 보기 본문을 다시 실행해야 합니다 변경되지 않았으면 보기가 이미 최신 상태이므로 SwiftUI는 보기 본문 실행을 건너뛸 수 있습니다 이러한 업데이트가 원인 및 효과 그래프에 어떻게 나타나는지 보죠

    그래프에는 환경에 대한 업데이트를 나타내는 주요 노드 유형 두 가지가 있습니다 External Environment 업데이트는 SwiftUI 외부에서 업데이트되는 색상 체계 등 앱 수준 항목을 포함합니다 EnvironmentWriter 업데이트는 SwiftUI 내부에서 발생하는 환경의 값 변경 사항을 나타냅니다 여러분이 dot-environment 수정자를 사용해 앱에서 수행하는 업데이트가 이 카테고리에 속하죠 기기가 다크 모드로 전환되어 색상 체계 환경 값이 업데이트된다고 해 보겠습니다 해당 보기의 원인 및 효과 그래프에서 어떻게 보일까요? View1의 “External Environment”에 대한 노드가 표시됩니다 색상 체계는 시스템 수준 환경 업데이트이기 때문입니다 View1의 본문이 실행되었음을 나타내는 노드도 표시됩니다 View2도 환경을 읽기 때문에 그래프에 External Environment 업데이트가 원인으로 표시됩니다 하지만 View2는 색상 체계 값을 읽지 않으므로 본문이 실행되지 않습니다 그래프에서 본문이 실행되지 않은 보기 업데이트는 흐린 아이콘으로 표시됩니다 여기서 두 외부 환경 노드는 동일한 업데이트를 나타냅니다 이 업데이트의 노드 중 하나를 마우스로 가리키거나 클릭하면 둘 모두 동시에 강조 표시되므로 식별하기 더 쉽습니다 두 보기 업데이트 모두 그래프에 표시되는데 환경 업데이트의 결과로 보기의 본문을 실행할 필요가 없는 경우라도 보기의 관심 값에 대한 업데이트를 확인하는 데 비용이 발생하기 때문입니다 환경에서 읽는 값이 많은 앱이라면 소요 시간이 빠르게 누적될 수 있습니다 그래서 환경에서 지오메트리 값 타이머 등 매우 자주 업데이트되는 값은 저장하지 않아야 합니다 이렇게 원인 및 효과 그래프를 알아보았는데요 앱을 통해 데이터가 흐르는 방법을 시각화해 보기가 불필요하게 업데이트되는 것을 방지하기에 좋은 방법입니다 이 세션에서는 SwiftUI 앱에서 우수한 성능을 실현하는 모범 사례를 다루었습니다 중요한 점은 보기 본문의 속도를 빠르게 유지해 SwiftUI가 지연 없이 화면에 UI를 표시할 시간을 충분히 확보하는 것입니다 불필요한 보기 본문 업데이트는 매우 소모적일 수 있습니다 필요시에만 보기를 업데이트하도록 데이터 흐름을 설계하고 매우 자주 변경되는 종속성에 특히 유의하세요 마지막으로 Instruments를 초기에 자주 사용해 개발 중에 앱의 성능을 분석하세요 오늘 참 많은 내용을 다루었습니다 하지만 가장 중요한 요점은 이겁니다 보기 본문이 빠르게 필요시에만 업데이트되도록 하여 SwiftUI 성능을 향상하는 것입니다 SwiftUI instrument를 사용해 앱의 성과를 꾸준히 검증하세요

    오늘 세션에서는 SwiftUI instrument로 앱을 프로파일링하는 방법을 보여 드렸는데 이외에도 살펴볼 만한 내용이 많습니다 비디오 설명에 링크된 문서를 확인해 instrument의 다른 기능에 대해 자세히 알아보세요 앱 성능 분석 및 개선에 대한 기타 비디오 및 참고 자료의 링크도 추가해 두었습니다 함께해 주셔서 감사합니다 새로운 SwiftUI Instrument를 사용해 앱 성능을 극대화하실 수 있기를 기대합니다

    • 8:47 - LandmarkListItemView

      import SwiftUI
      import CoreLocation
      
      /// A view that shows a single landmark in a list.
      struct LandmarkListItemView: View {
          @Environment(ModelData.self) private var modelData
      
          let landmark: Landmark
      
          var body: some View {
              Image(landmark.thumbnailImageName)
                  .resizable()
                  .aspectRatio(contentMode: .fill)
                  .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                  .overlay { ... }
                  .clipped()
                  .cornerRadius(Constants.cornerRadius)
                  .overlay(alignment: .bottom) {
                      VStack(spacing: 6) {
                          Text(landmark.name)
                              .font(.title3).fontWeight(.semibold)
                              .multilineTextAlignment(.center)
                              .foregroundColor(.white)
      
                          if let distance {
                              Text(distance)
                                  .font(.callout)
                                  .foregroundStyle(.white.opacity(0.9))
                                  .padding(.bottom)
                          }
                      }
                  }
                  .contextMenu { ... }
          }
      
          private var distance: String? {
              guard let currentLocation = modelData.locationFinder.currentLocation else { return nil }
              let distance = currentLocation.distance(from: landmark.clLocation)
      
              let numberFormatter = NumberFormatter()
              numberFormatter.numberStyle = .decimal
              numberFormatter.maximumFractionDigits = 0
      
              let formatter = MeasurementFormatter()
              formatter.locale = Locale.current
              formatter.unitStyle = .medium
              formatter.unitOptions = .naturalScale
              formatter.numberFormatter = numberFormatter
              return formatter.string(from: Measurement(value: distance, unit: UnitLength.meters))
          }
      }
    • 12:13 - LocationFinder Class with Cached Distance Strings

      import CoreLocation
      
      /// A class the app uses to find the current location.
      @Observable
      class LocationFinder: NSObject {
          var currentLocation: CLLocation?
          private let currentLocationManager: CLLocationManager = CLLocationManager()
      
          private let formatter: MeasurementFormatter
      
          override init() {
              // Format the numeric distance
              let numberFormatter = NumberFormatter()
              numberFormatter.numberStyle = .decimal
              numberFormatter.maximumFractionDigits = 0
      
              // Format the measurement based on the current locale
              let formatter = MeasurementFormatter()
              formatter.locale = Locale.current
              formatter.unitStyle = .medium
              formatter.unitOptions = .naturalScale
              formatter.numberFormatter = numberFormatter
              self.formatter = formatter
      
              super.init()
              
              currentLocationManager.desiredAccuracy = kCLLocationAccuracyKilometer
              currentLocationManager.delegate = self
          }
      
          // MARK: - Landmark Distance
      
          var landmarks: [Landmark] = [] {
              didSet {
                  updateDistances()
              }
          }
      
          private var distanceCache: [Landmark.ID: String] = [:]
      
          private func updateDistances() {
              guard let currentLocation else { return }
      
              // Populate the cache with each formatted distance string
              self.distanceCache = landmarks.reduce(into: [:]) { result, landmark in
                  let distance = self.formatter.string(
                      from: Measurement(
                          value: currentLocation.distance(from: landmark.clLocation),
                          unit: UnitLength.meters
                      )
                  )
                  result[landmark.id] = distance
              }
          }
      
          // Call this function from the view to access the cached value
          func distance(from landmark: Landmark) -> String? {
              distanceCache[landmark.id]
          }
      }
      
      extension LocationFinder: CLLocationManagerDelegate {
          func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
              switch currentLocationManager.authorizationStatus {
              case .authorizedWhenInUse, .authorizedAlways:
                  currentLocationManager.requestLocation()
              case .notDetermined:
                  currentLocationManager.requestWhenInUseAuthorization()
              default:
                  currentLocationManager.stopUpdatingLocation()
              }
          }
          
          func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
              print("Found a location.")
              currentLocation = locations.last
              // Update the distance strings when the location changes
              updateDistances() 
          }
          
          func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
              print("Received an error while trying to find a location: \(error.localizedDescription).")
              currentLocationManager.stopUpdatingLocation()
          }
      }
    • 16:51 - LandmarkListItemView with Favorite Button

      import SwiftUI
      import CoreLocation
      
      /// A view that shows a single landmark in a list.
      struct LandmarkListItemView: View {
          @Environment(ModelData.self) private var modelData
      
          let landmark: Landmark
      
          var body: some View {
              Image(landmark.thumbnailImageName)
                  .resizable()
                  .aspectRatio(contentMode: .fill)
                  .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                  .overlay { ... }
                  .clipped()
                  .cornerRadius(Constants.cornerRadius)
                  .overlay(alignment: .bottom) { ... }
                  .contextMenu { ... }
                  .overlay(alignment: .topTrailing) {
                      let isFavorite = modelData.isFavorite(landmark)
                      Button {
                          modelData.toggleFavorite(landmark)
                      } label: {
                          Label {
                              Text(isFavorite ? "Remove Favorite" : "Add Favorite")
                          } icon: {
                              Image(systemName: "heart")
                                  .symbolVariant(isFavorite ? .fill : .none)
                                  .contentTransition(.symbolEffect)
                                  .font(.title)
                                  .foregroundStyle(.background)
                                  .shadow(color: .primary.opacity(0.25), radius: 2, x: 0, y: 0)
                          }
                      }
                      .labelStyle(.iconOnly)
                      .padding()
                  }
          }
      }
    • 17:20 - ModelData Class

      /// A structure that defines a collection of landmarks.
      @Observable
      class LandmarkCollection: Identifiable {
          // ...
          var landmarks: [Landmark] = []
          // ...
      }
      
      /// A class the app uses to store and manage model data.
      @Observable @MainActor
      class ModelData {
          // ...
          var favoritesCollection: LandmarkCollection!
          // ...
      
          func isFavorite(_ landmark: Landmark) -> Bool {
              var isFavorite: Bool = false
              
              if favoritesCollection.landmarks.firstIndex(of: landmark) != nil {
                  isFavorite = true
              }
              
              return isFavorite
          }
      
          func toggleFavorite(_ landmark: Landmark) {
              if isFavorite(landmark) {
                  removeFavorite(landmark)
              } else {
                  addFavorite(landmark)
              }
          }
      
          func addFavorite(_ landmark: Landmark) {
              favoritesCollection.landmarks.append(landmark)
          }
      
          func removeFavorite(_ landmark: Landmark) {
              if let landmarkIndex = favoritesCollection.landmarks.firstIndex(of: landmark) {
                  favoritesCollection.landmarks.remove(at: landmarkIndex)
              }
          }
          // ...
      }
    • 20:50 - OnOffView

      struct OnOffView: View {
          @State private var isOn = true
          var body: some View {
              Text(isOn ? "On" : "Off")
          }
      }
    • 29:21 - Favorites View Model Class

      @Observable class ViewModel {
          var isFavorite: Bool
          
          init(isFavorite: Bool = false) {
              self.isFavorite = isFavorite
          }
      }
    • 29:21 - ModelData Class with New ViewModel

      @Observable @MainActor
      class ModelData {
          // ...
          var favoritesCollection: LandmarkCollection!
          // ...
      
          @Observable class ViewModel {
              var isFavorite: Bool
              init(isFavorite: Bool = false) {
                  self.isFavorite = isFavorite
              }
          }
      
          // Don't observe this property because we only need to react to changes
          // to each view model individually, rather than the whole dictionary
          @ObservationIgnored private var viewModels: [Landmark.ID: ViewModel] = [:]
      
          private func viewModel(for landmark: Landmark) -> ViewModel {
              // Create a new view model for a landmark on first access
              if viewModels[landmark.id] == nil {
                  viewModels[landmark.id] = ViewModel()
              }
              return viewModels[landmark.id]!
          }
      
          func isFavorite(_ landmark: Landmark) -> Bool {
              // When a SwiftUI view, such as LandmarkListItemView, calls
              // `isFavorite` from its body, accessing `isFavorite` on the 
              // view model here establishes a direct dependency between
              // the view and the view model
              viewModel(for: landmark).isFavorite
          }
      
          func toggleFavorite(_ landmark: Landmark) {
              if isFavorite(landmark) {
                  removeFavorite(landmark)
              } else {
                  addFavorite(landmark)
              }
          }
      
          func addFavorite(_ landmark: Landmark) {
              favoritesCollection.landmarks.append(landmark)
              viewModel(for: landmark).isFavorite = true
          }
      
          func removeFavorite(_ landmark: Landmark) {
              if let landmarkIndex = favoritesCollection.landmarks.firstIndex(of: landmark) {
                  favoritesCollection.landmarks.remove(at: landmarkIndex)
              }
              viewModel(for: landmark).isFavorite = false
          }
          // ...
      }
    • 31:34 - Cause and effect: EnvironmentValues

      struct View1: View {
          @Environment(\.colorScheme)
          private var colorScheme
      
          var body: some View {
              Text(colorScheme == .dark
                      ? "Dark Mode"
                      : "Light Mode")
          }
      }
      
      struct View2: View {
          @Environment(\.counter) private var counter
      
          var body: some View {
              Text("\(counter)")
          }
      }
    • 0:00 - 서론 및 어젠다
    • 새로운 SwiftUI 도구 및 Instruments 26 내 템플릿으로 SwiftUI 앱 성능을 최적화하는 방법을 알아보세요. Instruments를 사용하여 앱을 프로파일링하고 긴 뷰 본문 업데이트 및 불필요한 SwiftUI 업데이트와 같은 병목 현상을 식별할 수 있어 끊김, 멈춤, 일시 정지된 애니메이션 및 전환, 지연된 스크롤을 유발할 수 있습니다. 예시 앱인 Landmarks는 전 세계의 랜드마크와 사용자 위치로부터의 거리를 표시합니다. SwiftUI 코드 내 성능 문제를 진단하고 해결하여 앱의 부드러운 스크롤링을 개선하기 위해 새로운 SwiftUI 도구를 사용하는 방법을 알아보세요.

    • 2:19 - SwiftUI 도구 알아보기
    • Instruments 26은 새로운 SwiftUI 도구와 SwiftUI 앱 프로파일링을 위한 템플릿을 소개합니다. Time Profiler 및 Hangs 및 Hitches 도구와 유사하게 성능 문제를 식별하는 데 도움이 됩니다. Update Groups 행에는 SwiftUI 작업이 표시됩니다. 나머지 3개 행은 긴 뷰 바디 업데이트, 긴 표현 가능한 업데이트, 기타 업데이트를 강조하고 끊김 또는 정지를 유발할 가능성이 있는 항목에 따라 주황색과 빨간색으로 표시했습니다. 새로운 SwiftUI 도구를 사용하려면 Xcode 26을 설치하고 기기 OS를 최신 릴리스로 업데이트하세요.

    • 4:20 - 긴 뷰 본문 업데이트 진단 및 수정하기
    • 이 예제에서는 Xcode 26 및 Instruments 26을 사용하여 SwiftUI로 작성된 Landmarks 앱을 프로파일링합니다. Instruments를 실행하고 SwiftUI 템플릿을 선택하여 앱의 성과를 기록하기 시작합니다. 그런 다음 랜드마크 목록을 스크롤하여 iPhone의 앱과 상호작용하면 추가 뷰가 로드됩니다. 기록이 중지된 이후 Instruments가 데이터를 처리하면 사용자는 SwiftUI 트랙을 분석할 수 있습니다. ‘LandmarkListItemView’와 같이 성능 문제를 일으키는 특정 뷰를 식별할 수 있는 Long View Body Updates 행에 집중하세요. SwiftUI 트랙을 확장하고 Time Profiler 도구를 사용하면 뷰 본문 업데이트를 하는 동안 CPU 사용량을 더 자세히 살펴볼 수 있습니다. 특히 거리 데이터를 변환하고 표시하는 데 사용되는 포매터처럼 계산된 특정 속성이 과도하게 시간을 소모한다는 사실을 알 수 있습니다. SwiftUI에서 뷰 바디 런타임 최적화의 중요성을 고려하세요. 특히 뷰 바디는 메인 스레드에서 실행되고 지연이 발생하면 앱이 프레임 마감일을 놓쳐 끊김이 발생할 수 있습니다. 끊김은 애니메이션의 유연성을 떨어뜨리고 전반적인 사용자 경험에 부정적인 영향을 미칠 수 있습니다. 예제 프로젝트에서 이러한 성능 문제를 해결하려면 뷰 본문 업데이트를 하는 동안 계산을 수행하는 대신 미리 거리 문자열을 계산하고 캐시하면 앱 성능이 더 원활해지고 반응성이 뛰어납니다. Xcode에는 위치 업데이트를 관리하는 ‘LocationFinder’ 클래스 내 최적화 프로세스가 있습니다. 이전에는 시스템이 ‘LandmarkListItemView’의 뷰 본문 내에서 서식이 지정된 거리 문자열을 계산했기 때문에 업데이트가 효율적이지 못했습니다. 이러한 문제를 해결하기 위해 코드는 이 논리를 ‘LocationFinder’ 클래스로 옮깁니다. 이 경우, 시스템은 이니셜라이저에 포매터를 생성 및 보관하여 재사용함으로써 중복 생성을 방지합니다. 사전은 계산 후 거리 문자열을 캐시합니다. ‘updateDistances’ 함수에는 위치가 변경될 때마다 이러한 문자열을 다시 계산하는 책임이 있습니다. 이 함수는 이전에 생성된 포매터를 활용하여 거리 문자열을 생성하고 캐시에 저장합니다. CoreLocation 프레임워크는 기기의 위치가 변경되면 ‘CLLocationManagerDelegate’ 객체에서 ‘locationManager(_:didUpdateLocations:)’ 메서드를 호출합니다. 이 메서드 내에서 ‘updateDistances’를 호출하면 캐시가 최신 상태로 유지됩니다. 그런 다음 뷰에서 캐시된 거리 문자열을 검색하여 뷰 본문 업데이트를 하는 도중 다시 계산할 필요가 없어집니다. 다음으로, 새로운 기능을 추가할 수 있습니다. 좋아하는 랜드마크에 대한 하트 버튼이죠. 누군가 버튼을 탭하면 `toggleFavorite` 함수가 호출되어 모델 데이터 클래스를 업데이트하여 즐겨찾기 목록에 랜드마크를 추가하거나 제거합니다. 그런 다음 뷰는 채워졌거나 비어 있는 하트 아이콘을 표시하여 이러한 변화를 반영합니다. Instruments에서 앱의 새로운 즐겨찾기 기능을 프로파일링할 때 ‘LandmarkListItemView’가 예상보다 더 자주 업데이트되는 것을 확인할 수 있습니다. 예상치 못한 이러한 동작으로 인해 뷰 업데이트 논리에 대한 조사가 필요하게 돼 SwiftUI 앱에서 뷰 업데이트를 디버깅하는 데 어려움이 있다는 점과 UIKit 앱에서 기존의 중단점 기반 검사가 선언형 프레임워크에 비해 간단하지 않을 수 있다는 점이 강조되었습니다.

    • 19:54 - SwiftUI 업데이트의 원인과 결과 이해하기
    • Xcode에서는 백트레이스를 사용하여 UIKit 앱과 같은 필수 코드를 디버깅하는 것이 간단합니다. 그러나 이러한 접근 방식은 SwiftUI의 선언적 특성으로 인해 효과가 떨어집니다. SwiftUI의 데이터 모델인 ‘AttributeGraph’는 뷰 간의 종속성을 관리하여 업데이트를 최적화합니다. SwiftUI 뷰가 선언되면 ‘View’ 프로토콜을 따르고 ‘body’ 속성을 통해 외관 및 행동을 정의합니다. 이 ‘body’ 속성은 또 다른 ‘View’ 값을 반환하고 SwiftUI는 속성을 사용하여 뷰의 상태와 업데이트를 내부적으로 관리합니다. 상태 변수가 변경되면 거래가 트리거되어 관련 속성이 오래된 것으로 표시됩니다. SwiftUI는 그런 다음 프레임에서 뷰 계층 구조를 효율적으로 업데이트하여 종속성 체인을 탐색하여 필요한 부분만 새로 고칩니다. SwiftUI 뷰가 업데이트된 이유를 이해하려면 새로운 SwiftUI Instrument Cause & Effect 그래프를 활용하면 됩니다. 이 그래프는 제스처와 같은 사용자 상호작용에서 상태 변경 그리고 궁극적으로 뷰 본문 업데이트에 이르는 원인 사슬을 보여 주며 업데이트 간의 관계를 시각화합니다. 이 그래프를 살펴보면 불필요한 업데이트 등 비효율적인 부분을 파악하고 이에 따라 코드를 최적화할 수 있습니다. Landmarks 앱에서 ‘ModelData’ 클래스에는 즐겨찾기에 추가된 랜드마크를 배열로 저장하는 ‘favoritesCollection’ 속성이 포함되어 있습니다. 처음에 각 ‘LandmarkListItemView’는 전체 ‘favoritesCollection’ 배열에 접근하여 랜드마크가 즐겨찾기인지 확인했고 각 항목 뷰와 전체 배열 사이에 종속성을 생성했습니다. 이로 인해 즐겨찾기가 추가될 때마다 모든 항목 뷰의 본문이 실행되어 성능이 비효율적으로 발생했습니다. 이러한 문제를 해결하기 위해 접근 방식이 재고되었습니다. 각 랜드마크마다 ‘관찰 가능한’ 데이터 모델이 생성되어 랜드마크가 선호하는 상태를 직접 저장합니다. 이제 각 ‘LandmarkListItemView’에는 자체 데이터 모델이 있기 때문에 즐겨찾기 전체 배열에 대한 종속성이 제거되었습니다. 이러한 변경 사항을 구현하면 누군가 즐겨찾기를 토글할 때만 시스템이 필요한 뷰 본문을 업데이트합니다. Cause & Effect 그래프에서 관찰되는 뷰 본문 업데이트 수가 줄어든 것에서 알 수 있는 것처럼 이러한 최적화를 통해 성능이 크게 향상되었습니다. 또한 그래프는 색상 구성표의 변경 사항과 같은 환경 업데이트가 뷰에 어떤 영향을 미칠 수 있는지 보여 줍니다. 환경 업데이트로 인해 뷰의 본문을 실행할 필요가 없더라도 이러한 업데이트를 확인하는 데 비용이 발생하기 때문에 빈번하게 변경되는 값을 환경에 저장하지 않는 것이 중요합니다.

    • 35:01 - 다음 단계
    • Instruments 26의 새로운 SwiftUI 도구에 대한 추가 기능, 비디오, 앱 성능 분석 및 개선에 대한 관련 리소스는 개발자 설명서에서 확인할 수 있습니다.

Developer Footer

  • 비디오
  • WWDC25
  • Instruments로 SwiftUI의 성능 최적화하기
  • 메뉴 열기 메뉴 닫기
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    메뉴 열기 메뉴 닫기
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    메뉴 열기 메뉴 닫기
    • 손쉬운 사용
    • 액세서리
    • 앱 확장 프로그램
    • App Store
    • 오디오 및 비디오(영문)
    • 증강 현실
    • 디자인
    • 배포
    • 교육
    • 서체(영문)
    • 게임
    • 건강 및 피트니스
    • 앱 내 구입
    • 현지화
    • 지도 및 위치
    • 머신 러닝 및 AI
    • 오픈 소스(영문)
    • 보안
    • Safari 및 웹(영문)
    메뉴 열기 메뉴 닫기
    • 문서(영문)
    • 튜토리얼
    • 다운로드(영문)
    • 포럼(영문)
    • 비디오
    메뉴 열기 메뉴 닫기
    • 지원 문서
    • 문의하기
    • 버그 보고
    • 시스템 상태(영문)
    메뉴 열기 메뉴 닫기
    • Apple Developer
    • App Store Connect
    • 인증서, 식별자 및 프로파일(영문)
    • 피드백 지원
    메뉴 열기 메뉴 닫기
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(영문)
    • News Partner Program(영문)
    • Video Partner Program(영문)
    • Security Bounty Program(영문)
    • Security Research Device Program(영문)
    메뉴 열기 메뉴 닫기
    • Apple과의 만남
    • Apple Developer Center
    • App Store 어워드(영문)
    • Apple 디자인 어워드
    • Apple Developer Academy(영문)
    • WWDC
    Apple Developer 앱 받기
    Copyright © 2025 Apple Inc. 모든 권리 보유.
    약관 개인정보 처리방침 계약 및 지침