View in English

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

빠른 링크

5 빠른 링크

비디오

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

더 많은 비디오

  • 소개
  • 요약
  • 자막 전문
  • 코드
  • SwiftUI에서 동시성 살펴보기

    SwiftUI가 Swift 동시성을 활용하여 안전하고 반응성이 뛰어난 앱을 빌드하는 방법을 알아보세요. SwiftUI가 기본적으로 메인 액터를 사용하고 다른 액터에 작업을 오프로드하는 방법을 살펴보세요. SwiftUI의 이벤트 루프를 사용함으로써 동시성 주석을 해석하고 비동기 작업을 관리하여 원활한 애니메이션 및 UI 업데이트를 수행하는 방법을 학습하세요. 데이터 경쟁을 방지하고 두려움 없이 코드를 작성하는 법을 알게 되실 겁니다.

    챕터

    • 0:00 - 서론
    • 7:17 - 동시성
    • 16:53 - 코드 구성
    • 23:47 - 다음 단계

    리소스

    • Concurrency
    • Mutex
    • The Swift Programming Language: Concurrency
    • Updating an App to Use Swift Concurrency
      • HD 비디오
      • SD 비디오

    관련 비디오

    WWDC25

    • 코딩 실습: Swift 동시성으로 앱 수준 높이기
    • Swift 동시성 사용하기

    WWDC23

    • SwiftUI 애니메이션 살펴보기
  • 비디오 검색…

    안녕하세요, 여러분 SwiftUI 팀의 투어 가이드 Daniel입니다

    오늘은 동시성과 SwiftUI 앱 개발 환경을 함께 살펴보려 합니다

    여러분은 위험한 생물체인 데이터 레이스 버그 때문에 여기 오셨을 것입니다

    과거에 직접 겪은 분도 계실지 모르겠네요

    이로 인해 비정상 앱 상태 버그가 있는 애니메이션 심지어 영구적인 데이터 손실이 일어날 수도 있습니다

    걱정하지 마세요 이 투어는 100% 안전합니다 Swift와 SwiftUI를 사용하면 데이터 레이스 짐승은 과거의 산물이 됩니다 SwiftUI는 다양한 방법으로 코드를 동시에 실행합니다 이 투어에서는 SwiftUI API의 동시성 주석으로 이를 식별하는 방법을 배웁니다 SwiftUI 앱 개발에 더 자신 있고 두려움 없이 도전하게 되길 바랍니다

    Swift 6.2에는 새 언어 모드가 도입되었습니다

    모듈의 모든 유형에 암묵적인 @MainActor 주석을 표시합니다

    투어에서 배우는 모든 내용은 새 모드와 상관없이 적용됩니다 투어에는 3가지 명소가 등장합니다

    메인 액터의 아름다운 메도우에서 시작하여 SwiftUI가 메인 액터를 애플리케이션 컴파일 시점과 런타임 기본값으로 처리하는 방법을 살펴봅니다

    그런 다음 동시성 절벽을 방문해 SwiftUI가 메인 스레드에서 작업을 분리하여 앱 UI 지연을 방지하는 방법과 데이터 레이스 버그 방지 방법을 살펴봅니다

    마지막에는 캠프에 도착해 동시 코드와 SwiftUI API 간의 관계에 대해 생각해 봅니다

    메인 액터 메도우로 가보죠

    저는 자연에서 영감을 받은 색상 조합을 수집하고 싶어 앱을 만들었습니다 사진을 찍은 후 색상 수를 선택하고 추출 버튼을 누르면 됩니다 앱이 사진에서 보색을 자동으로 선택해 화면에 표시합니다

    스크롤을 내려 추출한 색상 조합을 모두 확인하고 마음에 드는 조합을 선택해 내보낼 수 있습니다

    추출 UI로는 ColorExtractorView 구조체를 만들었습니다

    @MainActor 격리를 선언하는 SwiftUI의 뷰 프로토콜을 준수하죠

    Swift는 데이터 격리로 모든 가변 상태의 안전성을 이해하고 검증합니다 투어에서는 이와 같은 동시성 개념을 접하게 됩니다 Swift 동시성이 처음이거나 복습이 필요하다면 ‘Swift 동시성 사용하기’를 시청하세요 SwiftUI에서 View는 @MainActor에 격리되며 저는 구조체를 View에 맞게 조정합니다

    따라서 ColorExtractorView는 @MainActor로 격리됩니다 이 점선은 추론된 격리를 나타냅니다 즉 이 주석은 컴파일 시점에 암시적으로 적용되지만 제가 작성한 코드의 일부가 아닙니다

    @MainActor에 격리된 전체 유형은 모든 멤버도 암시적으로 격리됩니다

    View의 요구 사항을 구현하는 본문 속성뿐만 아니라

    @State 변수 같은 제가 선언한 다른 멤버도 포함됩니다

    뷰의 본문을 마무리하면서 모델의 스키마나 colorCount에 바인딩된 속성 같은 다른 멤버 속성을 참조합니다

    공유된 @MainActor 격리가 액세스 안전을 보장하기 때문에 컴파일러에서 허용됩니다

    이 역시 직관적으로 알 수 있죠

    @MainActor는 SwiftUI의 컴파일 타임 기본값입니다 따라서 대부분의 시간을 앱 기능 개발에 집중하고 동시성 문제를 고민할 필요가 없습니다

    동시성을 위해 코드에 주석을 달지 않아도 됩니다 자동으로 안전해지니까요

    더 많은 코드를 넣기 위해 이 추론된 격리는 숨기겠습니다

    이 @MainActor의 컴파일 시간 기본값은 제 뷰의 동기식 코드를 넘어 확장됩니다

    제 데이터 모델의 유형에는 @MainActor 주석이 필요 없습니다

    뷰의 선언 내부에 모델을 인스턴스화하기 때문에 Swift는 모델 인스턴스가 적절히 격리되도록 보장합니다

    이 SchemeContentView에는 색상 추출 작업을 시작하는 탭 제스처가 있습니다

    색상 추출 함수는 비동기식이므로 비동기 컨텍스트로 전환하기 위해 Task로 함수를 호출합니다

    뷰 본문이 @MainActor로 격리되기 때문에 제가 이 작업에 전달한 클로저도 메인 스레드에서 실행됩니다 정말 편리하죠

    @MainActor 격리는 SwiftUI의 컴파일 타임 기본값입니다

    따라서 뷰 작성이 편리하고 접근하기 쉬워집니다 하지만 훨씬 더 실용적인 이유가 있습니다 AppKit 및 UIKit의 API는 @MainActor에만 격리됩니다

    SwiftUI는 이러한 프레임워크와 원활하게 상호작용합니다 예를 들어 UIViewRepresentable 프로토콜은 View 프로토콜을 확장합니다 구조체처럼 UIViewRepresentable을 @MainActor에 격리합니다

    따라서 UIViewRepresentable 을 준수하는 유형도 View입니다 따라서 @MainActor에 고립됩니다

    UILabel의 이니셜라이저는 @MainActor 격리를 요구합니다 제 makeUIView에서 작동하는데 makeUIView는 @MainActor에 고립된 표현 가능한 유형의 멤버이기 때문입니다

    @MainActor로 주석을 달지 않아도 됩니다 SwiftUI는 API에 @MainActor 주석을 답니다 SwiftUI가 구현하는 기본 런타임 동작을 반영하기 때문이죠

    이러한 주석은 프레임워크의 련타임 시 의도된 의미론의 하위 단계에 위치합니다

    SwiftUI의 동시성 주석은 런타임 의미론을 표현합니다 이전에 살펴본 컴파일 시 편의 기능과 비슷해 보일지 모르지만 이것은 근본적인 차이점입니다 이 개념을 강화해 줄 또 다른 예를 살펴보겠습니다

    다음 목적지는 흥미진진할 것입니다 안전띠를 단단히 매세요 여러분의 전자기기도요

    앱 개발 도중 기능을 추가할 때 메인 스레드가 처리해야 할 작업이 너무 많으면 앱 프레임 드롭이나 지연이 발생할 수 있습니다 작업과 구조화된 동시성을 이용해 메인 스레드에서 컴퓨팅 작업을 분리할 수 있습니다 ‘동시성으로 앱 수준 높이기’ 세션에서 앱 성능을 개선하는 실용적인 기법을 확인할 수 있습니다 꼭 확인하세요

    이 투어의 핵심은 SwiftUI가 Swift의 동시성 기능으로 앱의 성능을 개선하는 방법입니다

    SwiftUI 팀은 기본 애니메이션이 중간 상태를 계산하기 위해 배경 스레드를 사용한다고 밝혔습니다

    SchemeContentView에 있는 이 원을 조사해 확인해 보죠

    색상 추출 작업이 시작되고 종료되면 원이 점차 커지며 애니메이션과 함께 원래 크기로 다시 줄어듭니다

    isLoading 속성에 반응하는 scaleEffect를 사용하고 있습니다

    각 프레임에는 1~1.5의 스케일 값이 필요합니다

    이러한 애니메이션 값에는 복잡한 수학이 동반됩니다 프레임 단위로 계산하면 비용이 많이 들 수 있습니다 SwiftUI는 이 계산을 배경 스레드에서 수행하여 메인 스레드가 다른 작업에 더 집중하게 합니다

    이 최적화는 여러분이 구현하는 API에도 적용됩니다

    맞습니다 SwiftUI가 코드를 메인 스레드에서 실행하지 않을 때도 있습니다

    걱정하지 마세요 그렇게 복잡하지 않습니다

    SwiftUI는 선언형입니다 UIView와 달리 View 프로토콜을 준수하는 구조체는 메모리에서 고정된 위치를 차지하는 객체가 아닙니다

    런타임에 SwiftUI는 View에 대한 별도의 표현을 만듭니다

    이 표현은 다양한 최적화 기회를 제공합니다 중요한 점은 뷰 표현의 일부를 배경 스레드에서 평가하는 것입니다

    SwiftUI는 사용자를 대신해 많은 계산을 수행할 때 이 기법을 사용합니다 예를 들어 대부분의 경우에 고빈도 기하 계산이 동반됩니다

    Shape 프로토콜이 대표적인 예입니다

    Shape 프로토콜에는 경로를 반환하는 메서드가 필요합니다

    휠에서 추출한 색상을 표현하는 맞춤형 웨지를 만들었습니다

    경로 방법을 구현하는 웨지죠

    각 웨지마다 방향이 다릅니다 웨지 모양이 움직이면 제가 작성한 경로 메서드는 배경 스레드에서 호출을 받습니다

    SwiftUI는 맞춤형 로직으로 클로저 인수도 실행합니다

    원의 중앙에는 흐릿한 텍스트가 있습니다

    SwiftUI Text에서 visualEffect로 구현합니다

    펄스 값이 true와 false를 전환할 때마다 두 값 사이의 블러 반경을 조정합니다 뷰 수정자 visualEffect는 대상 뷰(즉 텍스트)에 적용될 효과를 정의하는 클로저를 허용합니다 시각 효과는 화려하지만 렌더링 비용은 비쌉니다

    그래서 SwiftUI는 백그라운드 스레드에서 이 클로저를 호출하죠

    백그라운드 스레드에서 코드를 호출하는 API 2개가 있죠

    추가 목적지를 빠르게 살펴보겠습니다

    Layout 프로토콜은 메인 스레드 에서 요구 사항 메서드를 호출하죠 visualEffect처럼 onGeometryChange의 최초 인수는 배경 스레드에서도 호출될 수 있는 클로저입니다

    배경 스레드를 사용한 이 런타임 최적화는 오랫동안 SwiftUI의 일부였습니다

    SwiftUI는 Sendable 주석을 사용하여 이 런타임 동작 또는 의미론을 컴파일러와 사용자에게 전달합니다

    여기서도 SwiftUI의 동시성 주석은 런타임 의미론을 표현합니다

    코드를 별도의 스레드에서 실행하면 메인 스레드가 해제되어 앱의 응답 속도가 개선됩니다

    Sendable 키워드는 @MainActor에서 데이터를 공유할 때 발생하는 데이터 레이스 조건을 알려 줍니다

    Sendable은 절벽길 경고 표지판과 같습니다 ‘위험! 주행 금지!’라고 적혀 있죠

    조금 과장된 설명인 것 같네요 실제 환경에서 Swift는 코드 내의 레이스 조건을 안정적으로 탐지하고 컴파일러 오류로 이를 알립니다 데이터 레이스 조건을 방지하는 최고의 전략은 동시 실행되는 작업 간에 데이터를 공유하지 않는 것입니다

    SwiftUI API가 전송 가능 함수 작성을 요구하면 프레임워크는 대부분의 변수를 함수 인수로 제공합니다 간단한 예시를 살펴보죠

    ColorExtactorView에서 설명하지 않은 내용이 있습니다 색상 휠과 슬라이더는 EqualWidthVStack 유형 덕분에 너비가 동일합니다

    EqualWidthVStack은 맞춤형 레이아웃입니다

    레이아웃은 중요하지 않습니다 중요한 점은 SwiftUI가 전달하는 인수를 사용하면 외부 변수를 건드리지 않고도 복잡한 계산을 수행할 수 있다는 것이죠

    하지만 전송 가능 함수 밖에서 변수에 접근해야 한다면요?

    SchemeContentView에서 상태 펄스 가 이 visualEffect에 필요합니다

    하지만 Swift는 데이터 레이스 조건이 있다고 합니다

    쌍안경을 꺼내 컴파일러 오류가 무엇을 말하는지 확인해 보죠

    pulse 변수는 self.pulse의 약자입니다 @MainActor 격리 변수를 전송 가능 클로저에서 공유할 때 자주 발생하는 상황이죠

    Self는 뷰입니다 메인 액터에 격리되어 있습니다 이것이 우리의 출발점입니다 최종 목표는 전송 가능 클로저에서 pulse 변수에 접근하는 것입니다 이러게 하려면 두 가지 일이 일어나야 합니다

    먼저 값 자체가 메인 액터에서 배경 스레드 코드 영역으로 넘어가야 합니다

    Swift에서는 이를 변수 자체의 배경 스레드 전송이라 부릅니다

    이렇게 하려면 self의 유형이 Sendable이어야 합니다

    이제 self가 적절한 위치에 있으니 이 비격리 영역에서 관련 속성 pulse를 읽겠습니다 컴파일러는 속성 pulse가 어떤 액터에도 격리되지 않은 경우에만 이를 허용합니다

    코드를 다시 살펴보면 self는 View이므로 @MainActor로 보호됩니다

    그래서 컴파일러는 이를 Sendable로 간주합니다

    그래서 Swift는 이 self 참조가 @MainActor 격리 영역에서 Sendable 클로저로 넘어가도록 허용합니다

    사실 Swift는 pulse 속성 접근 시도를 경고합니다

    물론 View의 멤버인 pulse는 @MainActor에 격리되어 있습니다

    컴파일러는 self 전달은 가능하지만 @MainActor에 격리된 속성 pulse 접근은 안전하지 않다고 말합니다

    이 컴파일 오류를 수정하려면 View를 참조하여 속성 읽기를 방지해야 합니다

    작성 중인 시각적 효과에는 뷰의 전체 값이 필요 없습니다 pulse가 true인지 false인지 알기만 하면 됩니다 클로저의 캡처 목록에 있는 pulse 변수의 복사본을 만들어 대신 참조할 수 있습니다 이렇게 하면 self가 클로저에 전송되지 않습니다

    Bool은 단순 값 유형이므로 전송 가능 pulse의 복사본을 전송하고 있습니다

    이 복사본은 이 함수 범위 내에서만 존재하므로 접근해도 데이터 레이스 문제가 발생하지 않습니다

    예시에서는 글로벌 액터가 보호하기 때문에 전송 가능 클로저에서 해당 pulse 변수에 접근할 수 없었습니다

    이 작업을 위한 또 다른 전략은 우리가 읽는 모든 내용이 격리되지 않게 하는 것입니다

    여러분, 캠프에 도착했습니다 이제 동시 코드 조직화 이야기를 해보죠

    노련한 SwiftUI 개발자라면 버튼의 액션 콜백 같은 SwiftUI API가 대부분 동기식임을 눈치채셨겠죠

    동시 코드를 호출하려면 먼저 Task를 이용해 비동기 컨텍스트로 전환해야 합니다

    그런데 왜 Button은 비동기 클로저를 받지 않을까요?

    동기식 업데이트는 좋은 사용자 경험의 필수 요소입니다 앱에 장기 실행 작업이 있으며 사용자가 결과물을 기다려야 할 때는 더 중요하죠

    비동기 함수로 장기 실행 작업을 시작하기 전에 UI를 업데이트해 작업 진행을 표시해야 합니다

    이 업데이트는 동기식이어야 하며 시간에 민감한 애니메이션을 트리거해야 할 때는 반드시 그래야 합니다

    언어 모델에게 색상 추출을 요청하면 어떻게 될까요? 추출에 시간이 꽤 걸릴 겁니다 그래서 저는 앱에서 withAnimation으로 다양한 로딩 상태를 동기식으로 트리거합니다

    작업이 완료되면 또 다른 동기식 상태 변경으로 로딩 상태를 되돌립니다

    SwiftUI의 액션 콜백은 제 로딩 상태 같은 UI 업데이트 설정에 필요한 동기식 클로저를 허용합니다

    반면 비동기 함수는 특히 애니메이션을 다룰 때 추가 고려사항이 있습니다 자세히 살펴보죠

    제 앱에서 위로 스크롤하면 이전 색상 체계를 확인할 수 있습니다 각 체계가 화면에 나타나면 색상이 애니메이션을 통해 드러나야 합니다

    뷰 수정자 onScrollVisibilityChange는 색상 체계가 표시될 때 이벤트를 제공합니다 이 경우 저는 즉시 상태 변수를 true로 설정하여 애니메이션을 트리거합니다 각 색상의 Y 오프셋이 애니메이션과 함께 업데이트됩니다

    UI 프레임워크로서 프레임마다 부드러운 상호작용을 구현하려면 SwiftUI는 기기가 요구하는 특정 화면 재생 빈도를 고려해야 합니다

    스크롤 같은 연속 제스처에 반응하는 코드를 작성할 때는 이 맥락을 반드시 고려해야 합니다 코드를 타임라인에 넣어 보겠습니다

    이 녹색 삼각형을 이용해 SwiftUI가 onScrollVisibilityChange를 호출하는 시점을 표시하겠습니다 파란색 원은 상태 변환을 통해 애니메이션을 트리거하는 순간을 표시합니다

    이 설정에서는 변환이 제스처 콜백과 동일한 프레임에서 발생하느냐에 따라 시각적으로 완전히 달라질 수 있습니다

    애니메이션 변환 전에 비동기 작업을 추가하려 합니다 비동기 작업이 시작되는 시점을 주황 선으로 표시하고 대기합니다

    Swift에서 비동기 함수를 기다리면 중단 지점이 생성됩니다

    Task는 async 함수를 인수로 허용합니다

    대기를 목격하면 컴파일러는 비동기 함수를 두 개로 나눕니다

    첫 번째 부분을 실행하면 Swift 런타임은 함수를 일시 중지하고 CPU에서 다른 작업을 수행할 수 있습니다 이 과정은 임의의 시간 동안 지속될 수 있습니다 이후 런타임은 원래 비동기 함수로 돌아가 두 번째 부분을 실행합니다

    이 프로세스는 함수에서 대기가 발생할 때마다 반복됩니다

    타임라인으로 돌아가면 이 중단은 작업 완료가 기기에서 지정된 새로 고침

    마감 시간 이후에도 재개되지 않을 수 있음을 의미합니다

    그래서 제 애니메이션이 버벅거리는 것처럼 보입니다 따라서 비동기 함수 변형은 목표 달성에 도움이 되지 않죠

    SwiftUI는 기본적으로 동기식 콜백을 제공합니다 이렇게 하면 비동기 코드의 중단을 방지할 수 있습니다 동기식 액션 클로저에서의 UI 업데이트는 쉽게 수행 가능합니다 사용자는 언제든 Task로 비동기 컨텍스트에 참여 가능하죠

    애니메이션 같은 시간에 민감한 로직은 SwiftUI의 입력과 출력이 동기화되어야 합니다 관측 가능한 속성의 동기화된 변환과 동기화된 콜백은 프레임워크와의 상호작용에서 가장 자연스러운 유형입니다 좋은 사용자 경험을 위해 맞춤형 동시 로직이 많을 필요는 없습니다 동기식 코드는 많은 앱의 좋은 시작점이자 종착점입니다

    앱이 많은 동시 작업을 수행한다면 UI 코드와 비 UI 코드 사이의 경계를 찾아보세요

    비동기 작업의 로직을 뷰 로직에서 분리하세요

    상태 일부를 브리지로 사용할 수 있습니다 상태는 UI 코드와 비동기 코드를 분리합니다

    상태는 비동기 작업을 시작할 수 있습니다

    비동기 작업이 완료되면 상태에 동기식 변경을 수행하여 UI가 이 변경에 반응해 업데이트되게 합니다

    이런 방식으로 UI 로직은 대부분 동기식이 됩니다

    보너스로 비동기 코드에 대한 테스트 작성이 더 쉬워집니다 UI 로직과 독립적으로 작동하기 때문이죠

    뷰는 여전히 Task를 이용해 비동기 컨텍스트로 전환 가능하죠

    하지만 코드를 최대한 간단하게 유지해야 합니다 UI 이벤트를 모델에 알리는 역할을 합니다

    UI 코드 중 시간에 민감한 변경이 많이 필요한 부분과 장기 실행되는 비동기 로직 간의 경계를 찾으면 앱 구조를 개선할 수 있습니다

    뷰의 동기성과 반응성 유지에도 도움이 됩니다 UI가 아닌 코드를 잘 정리하는 것도 중요합니다

    이 베이스캠프에서 보여드린 팁을 활용하면 쉽게 하실 수 있습니다

    Swift 6.2는 우수한 기본 액터 격리 설정을 제공합니다 기존 앱이 있다면 시도해 보세요 대부분의 @MainActor 주석을 삭제할 수 있습니다

    Mutex는 클래스를 전송 가능으로 만드는 중요 도구입니다 자세한 내용은 공식 문서를 확인하세요

    앱의 비동기 코드에 대한 단위 테스트를 작성해 보세요 SwiftUI를 가져오지 않고 할 수 있는지 확인해보세요

    좋습니다 SwiftUI는 이렇게 Swift 동시성을 활용하여 빠르고 데이터 레이스 없는 앱을 개발할 수 있게 합니다

    이 투어를 마무리하면서 SwiftUI에서의 동시성을 잘 이해하게 되셨길 바랍니다

    투어에 동참해 주셔서 감사합니다 멋진 모험이 가득하길 바랍니다

    • 2:45 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorScheme: Identifiable, Hashable {
          var id = UUID()
          let imageName: String
          var colors: [Color]
      }
      
      @Observable
      final class ColorExtractor {
          var imageName: String
          var scheme: ColorScheme?
          var isExtracting: Bool = false
          var colorCount: Float = 5
      
          func extractColorScheme() async {}
      }
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 5:55 - AppKit and UIKit require @MainActor: an example

      // AppKit and UIKit require @MainActor
      // Example: UIViewRepresentable
      
      struct FancyUILabel: UIViewRepresentable {
          func makeUIView(context: Context) -> UILabel {
              let label = UILabel()
              // customize the label...
              return label
          }
      }
    • 6:42 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorScheme: Identifiable, Hashable {
          var id = UUID()
          let imageName: String
          var colors: [Color]
      }
      
      @Observable
      final class ColorExtractor {
          var imageName: String
          var scheme: ColorScheme?
          var isExtracting: Bool = false
          var colorCount: Float = 5
      
          func extractColorScheme() async {}
      }
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractorModel()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack(spacing: 30) {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 8:26 - Animated circle, part of color scheme view

      // Part of color scheme view
      
      struct SchemeContentView: View {
          let isLoading: Bool
          @State private var pulse: Bool = false
      
          var body: some View {
              ZStack {
                  // Color wheel …
      
                  Circle()
                      .scaleEffect(isLoading ? 1.5 : 1)
      
                  VStack {
                      Text(isLoading ? "Please wait" : "Extract")
      
                      if !isLoading {
                          Text("^[\(extractCount) color](inflect: true)")
                      }
                  }
                  .visualEffect { [pulse] content, _ in
                      content
                          .blur(radius: pulse ? 2 : 0)
                  }
                  .onChange(of: isLoading) { _, newValue in
                      withAnimation(newValue ? kPulseAnimation : nil) {
                          pulse = newValue
                      }
                  }
              }
          }
      }
    • 13:10 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 13:47 - Part of color scheme view

      // Part of color scheme view
      
      struct SchemeContentView: View {
          let isLoading: Bool
          @State private var pulse: Bool = false
      
          var body: some View {
              ZStack {
                  // Color wheel …
      
                  Circle()
                      .scaleEffect(isLoading ? 1.5 : 1)
      
                  VStack {
                      Text(isLoading ? "Please wait" : "Extract")
      
                      if !isLoading {
                          Text("^[\(extractCount) color](inflect: true)")
                      }
                  }
                  .visualEffect { [pulse] content, _ in
                      content
                          .blur(radius: pulse ? 2 : 0)
                  }
                  .onChange(of: isLoading) { _, newValue in
                      withAnimation(newValue ? kPulseAnimation : nil) {
                          pulse = newValue
                      }
                  }
              }
          }
      }
    • 17:42 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 18:55 - Animate colors as they appear by scrolling

      // Animate colors as they appear by scrolling
      
      struct SchemeHistoryItemView: View {
          let scheme: ColorScheme
          @State private var isShown: Bool = false
      
          var body: some View {
              HStack(spacing: 0) {
                  ForEach(scheme.colors) { color in
                      color
                          .offset(x: 0, y: isShown ? 0 : 60)
                  }
              }
              .onScrollVisibilityChange(threshold: 0.9) {
                  guard !isShown else { return }
                  withAnimation {
                      isShown = $0
                  }
              }
          }
      }
    • 0:00 - 서론
    • SwiftUI는 Swift 동시성을 활용하여 개발자가 빠르고 데이터 경쟁이 없는 앱을 빌드하도록 지원합니다. Swift 6.2에서는 모듈의 모든 유형을 @MainActor 주석으로 암묵적으로 표시하는 새로운 언어 모드가 도입됩니다. SwiftUI는 다양한 방법으로 코드를 동시에 실행하고 API에서 동시성 주석을 제공하여 개발자가 동시성을 식별하고 관리할 수 있도록 지원합니다. 이 세션은 SwiftUI가 데이터 경쟁을 피하고 앱 성능을 개선하기 위해 동시성을 처리하는 방법을 이해하는 데 중점을 둡니다.

    • 7:17 - 동시성
    • SwiftUI는 애니메이션 및 형태 계산과 같은 계산 집약적 작업에 배경 스레드를 사용하여 UI 장애를 방지합니다(예: ‘Shape’ 프로토콜의 ‘path’ 메서드, ‘visualEffect’ 클로저, ‘Layout’ 프로토콜, ‘onGeometryChange’ 클로저). ‘Sendable’ 주석은 메인 액터와 배경 스레드 간에 데이터를 공유할 때 잠재적인 데이터 경쟁 조건을 알립니다. 데이터 경쟁을 피하려면 데이터 공유를 최소화하세요. 공유가 필요하면 데이터 사본을 만드세요.

    • 16:53 - 코드 구성
    • SwiftUI의 액션 콜백은 특히 애니메이션과 로딩 상태의 즉각적인 UI 업데이트를 보장하기 위해 동기식으로 설계되었습니다. 장기 실행 작업은 비동기적으로 시작해야 하지만, UI 업데이트는 동기적으로 유지되어야 합니다. 비동기 작업이 완료된 후 UI 업데이트를 트리거하기 위해 상태를 브리지로 사용하여 UI 로직과 비 UI(비동기) 로직을 분리합니다. 뷰의 비동기 코드를 간단하게 유지하고 UI 이벤트에 대한 모델 정보 전달에 집중하세요. 시간에 민감한 논리에서는 SwiftUI의 입력과 출력이 동기적으로 이루어져야 합니다.

    • 23:47 - 다음 단계
    • Swift 6.2는 우수한 기본 액터 격리 설정을 제공합니다. 기존 앱이 있다면 시도해 보세요. 대부분의 @MainActor주석을 삭제할 수 있습니다. Mutex는 클래스를 전송 가능하게 만드는 중요 툴입니다. 공식 문서에서 그 방법을 확인하세요. 앱의 비동기 코드에 대한 단위 테스트를 작성해 보세요. SwiftUI를 가져오지 않고 할 수 있는지 확인해 보세요.

Developer Footer

  • 비디오
  • WWDC25
  • 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. 모든 권리 보유.
    약관 개인정보 처리방침 계약 및 지침