nyancoder

WWDC 2021 - Demystify SwiftUI 본문

WWDC/WWDC 2021

WWDC 2021 - Demystify SwiftUI

nyancoder 2021. 9. 8. 03:20

원본 영상: https://developer.apple.com/videos/play/wwdc2021/10022/

 

  • SwiftUI는 선언적 UI 프레임워크로 앱에서 높은 수준으로 설명하면 SwiftUI가 대부분 정확하게 동작합니다.

  • 하지만 SwiftUI가 예상치 못한 동작을 하게 되면 SwiftUI가 배후에서 수행하는 작업을 조금 더 이해하는 것이 도움이 될 것입니다.
  • SwiftUI는 코드에서 ID, 수명, 종속성의 세 가지를 확인합니다.

 

Identity

  • 두 개가 같은 개인지 다른 개인지를 구분하기에는 정보가 충분하지 않습니다.

  • 이처럼 두 사물이 같은지 다른지에 대한 여부가 "정체성"의 핵심입니다.
  • 이것이 SwiftUI가 앱을 이해하는 중요한 한 가지 측면입니다.

  • 위의 예제 앱에서는 두 개의 상태가 있으며 서로 다른 위치에 View가 존재합니다.

  • 여기에서 두 View는 같은 View인지 다른 View인지 여부는 동작에서 많은 것을 결정합니다.

  • 만일 이 두 View가 서로 다른 View라고 가정하면 전환 시에 각각 독립적으로 전환되어야 합니다.
  • 예를 들면, 페이드 인 및 아웃과 같이 각 View가 독립적으로 전환되는 효과가 적용됩니다.

  • 만일 이 두 View가 같은 View라면 하나의 위치에서 다른 위치로 전환하는 애니메이션을 해야 합니다.

  • 동일한 ID를 공유하는 View는 동일한 UI 요소의 다른 상태를 나타냅니다.

  • 하지만 서로 다른 UI View는 항상 다른 ID를 갖습니다.

  • 이러한 구분자의 종류는 크게 두 가지가 있습니다.
  • 첫째는 명시적 ID로 사용자 지정 식별자나, 데이터 기반 식별자를 사용합니다.
  • 두 번째는 구조적 ID로 View 계층 구조에서 유형과 위치에 따라 View를 구분합니다.

  • 위처럼 유사한 강아지가 있을 때 이들을 구분하는 가장 간단한 방법은 이름을 물어보는 것입니다.
  • 두 강아지가 똑같이 생겼고 같은 이름을 가진다면 실제로 같은 강아지일 가능성이 매우 높으며, 이름이 다르다면 다른 강아지임을 보장할 수 있습니다.

  • 이처럼 이름과 같은 식별자를 식별자를 지정하는 것은 명시적 ID의 한 형태입니다.
  • 명시적 ID는 강력하고 유연 하지만 어디에선가 이러한 ID를 모두 관리해야 합니다.
  • 이미 익숙한 명시적 ID의 한 형태는 UIKit 및 AppKit 전체에서 사용되는 포인터 ID입니다.

  • UIView와 NSView는 클래스이기 때문에 각각 메모리에 대한 고유한 포인터가 있으며 이는 명시적 ID가 될 수 있습니다.

  • 그러나 SwiftUI는 일반적으로 구조체인 값 유형이기 때문에 포인터를 사용하지 않습니다.

  • SwiftUI의 View는 값 유형이므로 할당되지 않으며, 포인터도 존재하지 않습니다.

  • 따라서 SwiftUI는 id 매개변수와 같은 명시적 ID에 의존합니다.
  • 컬렉션이 변경되면 SwiftUI는 해당 ID를 사용하여 변경된 내용을 이해하고 올바른 애니메이션을 생성할 수 있습니다.

  • 위 예제의 ScrollViewReader는 버튼을 사용하여 최상단으로 이동할 수 있으며, 이를 위해서 id(_:) 함수를 사용하여 명시적 ID를 제공해야 합니다.
  • 이러한 방식의 장점은 View를 생성한 곳 이외에서도 참조할 때 원하는 View를 정확하게 식별할 수 있다는 것입니다.
  • 반대로 ScrollViewReader, ScrollView, backstory Text, Button은 명시적 식별자가 필요하지 않습니다.

  • 그러나 ID가 명시적이지 않더라도 모든 View는 ID를 가지고 있습니다.

  • 이때 사용되는 암시적 View ID가 구조적 구분자입니다.

  • 예를 들어 비슷한 강아지가 두 마리가 있고 이름을 모르지만 식별해야 한다고 가정해봅시다.

  • 이 두 강아지가 움직이지 않는다면 "왼쪽의 강아지", "오른쪽의 강아지"와 같이 위치를 기반으로 식별할 수 있습니다.
  • 이러한 것이 구조적 구분자입니다.

  • SwiftUI는 API 전체에서 구조적 ID를 활용하며, View 코드 내에서 if 문을 사용하는 경우가 대표적인 예입니다.
  • 조건문의 구조는 각 View를 식별하는 명확한 방법을 제공합니다.

  • 첫 번째 View는 조건이 참일 때만 표시되고 두 번째 View는 조건이 거짓일 때만 표시됩니다.
  • 이 방법으로 각 View를 구분할 수 있지만, SwiftUI에서 View가 제자리에 유지되고 위치를 바꾸지 않도록 정적으로 보장할 수 있는 경우에만 작동합니다.

  • SwiftUI는 View의 계층 구조를 참조하여 각각을 구분합니다.
  • SwiftUI가 View를 볼 때는 제네릭 타입을 확인하며 위의 예제에서 if 문 _ConditionalContent View로 전환됩니다.
  • 이러한 변환은 Swift의 ViewBuilder에 의해 제공됩니다.

  • body 속성의 some View라는 반환 타입은 이와 같은 복잡한 유형의 타입을 숨겨줍니다.
  • SwiftUI는 True일 때의 View가 항상 AdoptionDirectory이고 False일 때의 View가 항상 DogList가 되도록 보장할 수 있으므로 안정적인 암시적 ID를 할당할 수 있습니다.

  • 이 예제 코드는 if문을 가지고 있기 때문에 각 분기의 View가 서로 다른 고유한 ID를 가진 View를 가진 다는 것을 의미합니다.
  • 따라서 두 상태가 전환될 때 기존의 View가 사라지면서 새로운 View가 나타납니다.

  • 또 다른 방법으로는 레이아웃과 색상을 변경하는 단일 PawView를 가지는 방법이 있습니다.
  • 이 경우에는 하나의 ID를 가진 View이기 때문에 두 상태가 전환될 때 View가 이동하는 애니메이션을 가집니다.
  • 이 두 가지 방식 모두 잘 동작하지만 SwiftUI는 ID를 유지하고 자연스러운 전환을 위해 두 번째 접근 방식을 권장합니다.

  • 이 구조적인 구분자의 악영향을 끼치는 항목으로는 AnyView가 있습니다.

  • 위의 예제 함수에서는 각 조건의 분기는 서로 다른 View를 반환합니다.
  • 하지만 함수의 리턴 타입은 하나이기 때문에 이에 맞추기 위해 AnyView로 래핑하고 있습니다.

  • 하지만 이러한 구조로 인해 반환 유형이 AnyView가 되어 SwiftUI가 코드의 조건부 구조를 볼 수 없게 됩니다.
  • 이런 이유로 AnyView는 "타입을 지우는 래퍼 타입"이라고 할 수 있습니다.
  • 또한 이 코드는 사람이 읽기에도 어렵다는 문제가 있습니다.

  • 이 코드를 단순화하기 위해서 우선 SheepView가 조건부로 추가되는 로직을 볼 수 있습니다.

  • 이 로직을 HStack을 조건부로 추가하는 로직에서 HStack 내부서 View를 조건부로 추가하도록 수정하여 단순화할 수 있습니다.

  • 이제 지역 변수가 필요하지 않게 되었으므로, 지역 변수를 삭제하고 return 문으로 대체할 수 있습니다.

  • 이전의 예제처럼 SwiftUI View의 if문에서는 서로 다른 타입을 반환할 수 있기 때문에 왼쪽처럼 AnyView를 제거하는 방향으로 수정하려고 할 수 있습니다.

  • 그러나 코드에서 AnyView를 삭제하려고 하면 하나의 반환 타입을 원하기 때문에 오류가 발생합니다.
  • 이전 예제의 SwiftUI의 body속성은 암시적으로 ViewBuilder로 감싸 지기 때문에 문제가 발생하지 않았습니다.
  • 따라서 이와 경우에는 명시적으로 @ViewBuilder를 추가하여야 합니다.

  • 이제는 보다 읽기가 좋은 코드가 되었으며, SwiftUI도 보다 많은 View의 구조적인 정보를 알 수 있게 되었습니다.

  • 또 다른 개선점으로는 항상 breed 변수를 조건문으로 비교하기 때문에 switch문으로 변경할 수 있다는 점입니다.

  • switch 문은 조건문에 대한 문법적 지원이기 때문에 결과 View의 타입은 정확히 동일하게 유지됩니다.

  • 이처럼 가능하면 AnyView를 피하는 것이 좋은데, AnyView가 너무 많으면 코드를 읽고 이해하기가 어려워지는 경우가 많기 때문입니다.
  • if/else 및 switch와 같은 기존 제어문을 사용하면 보다 깔끔한 코드를 얻을 수 있습니다.
  • 또한 AnyView는 정적 유형 정보를 숨기기 때문에 컴파일러가 진단이나 오류 및 경고를 하는 것을 방해합니다.
  • 마지막으로 필요하지 않은 경우 AnyView를 사용하면 성능이 저하될 수 있습니다.

  • 명시적 ID를 사용하면 View의 ID를 데이터에 연결하거나 특정 View를 참조하는 등의 기능을 수행할 수 있습니다.

  • 그리고 구조적 식별자를 통해 SwiftUI가 View 계층 구조 내에서 View의 유형과 위치를 기반으로 뷰를 식별한다는 것을 알 수 있었습니다.

 

View Lifetime

  • 이제 ID가 View와 데이터의 수명과 연결되는 것을 볼 것입니다.
  • 위의 이미지처럼 애완 고양이에게 이름이 있다면 어느 위치에서 어떤 동작을 하더라도 동일한 고양이일 것입니다.

  • 이처럼 이름과 같은 구분자를 통해 시간의 흐름에 따라 다른 값을 가지는 안정적인 요소를 정의할 수 있습니다.

  • 이 예제에서는 3가지 순간에 서로 다른 상태를 가지고 있는 예를 볼 수 있습니다.

  • 예를 들어 위의 예에서는 처음에는 25의 강도를 가진 View에서 이후에는 50의 강도를 가진 View로 변경됩니다.
  • 이 값은 매 호출 순간마다 생성되고 사라집니다.

  • 하지만 중요한 점은 뷰 값이 뷰의 구분자와 다르다는 것입니다.
  • View의 값은 일시적 이므로 수명에 의존해서는 안 됩니다.

  • 뷰가 처음 생성되고 나타날 때, SwiftUI는 이전의 명시적/암시적 구분자를 사용하여 뷰에 ID를 할당합니다.
  • 그리고 시간이 지남에 따라 View에는 새 값이 할당되지만, SwiftUI의 관점에서는 동일한 View입니다.
  • 뷰의 ID가 변경되거나 뷰가 제거되면 해당 수명이 종료됩니다.

  • 따라서 View의 수명이란 해당 View의 구분자의 지속 기간입니다.
  • View의 ID의 수명을 View의 수명과 연결 짓는 것이 SwiftUI가 상태를 유지하는 방법을 이해하는 것입니다.

  • SwiftUI에서 State 또는 StateObject는 뷰의 수명 동안 해당 데이터를 유지하기 때문에 View의 ID와 연결된 저장소라고 볼 수 있습니다.

  • View의 ID가 처음 생성될 때 SwiftUI는 초기 값을 사용하여 State 및 StateObject를 메모리에 할당합니다.
  • View의 수명 동안 SwiftUI는 View의 body가 재평가되더라도 저장소의 메모리를 유지합니다.

  • 위의 예처럼 분기에서 서로 다른 CatRecorder를 선언한다면 위에서 배운 것처럼 이 두 CatRecorder View는 암시적으로 서로 다른 ID를 가지게 됩니다.
  • 따라서 dayTime이 참인 동안 SwiftUI는 CatRecorder View에 메모리 공간을 할당합니다.

  • 이후 dayTime의 값이 false로 변경되면 새로운 ID를 가진 View가 CatRecorder View의 메모리는 할당 해제되고 새로운 메모리를 할당합니다.

  • 이후 dayTime의 값이 다시 true로 변경될 때도 마찬가지로 새 ID에 맞는 새 메모리를 할당받고 이전의 메모리가 해제됩니다.
  • 따라서 ID가 변경될 때마다 상태가 새로 만들어집니다.

  • 따라서 View의 State 메모리 공간은 View의 수명과 같습니다.

  • 따라서 코드에서 View가 유지되는 동안 데이터가 보관되는 부분을 명확하게 알 수 있습니다.

  • SwiftUI는 데이터의 ID를 View의 명시적 ID로 사용하는 몇 가지 데이터 기반 구조를 가지고 있는데, 대표적인 예가 ForEach입니다.

  • ForEach는 일반적으로 일정 범위를 필요로 하며, 이 고정된 범위를 통해 View의 생명주기 동안 ID가 안정적임을 보장합니다.

  • 따라서 ForEach에 동적 범위를 지정하는 것은 오류입니다.
  • 올해 제공되는 새로운 기능으로 일정하지 않은 범위를 지정하면 경고가 표시됩니다.

  • 데이터의 동적 컬렉션을 예로 들면 컬렉션과 키 경로를 ForEach의 입력으로 사용합니다.
  • SwiftUI는 컬렉션의 요소를 통해 생성된 모든 뷰에 ID를 할당하기 위해 해당 값이 해시가 가능해야 합니다.

  • 데이터에 대한 안정적인 ID를 제공하는 것은 중요하기 때문에 표준 라이브러리는 Identifiable 프로토콜을 제공합니다.
  • SwiftUI는 이 프로토콜을 활용하여 ForEach에 키 경로를 생략하고도 데이터와 View의 ID를 연결할 수 있습니다.

  • ForEach에는 컬렉션(위의 예제에서는 Data)과 컬렉션의 각 요소에서 뷰를 생성하는 방법이 중요합니다.
  • 이 생성자의 모양을 통해 컬렉션과, 그 컬렉션의 데이터에서 뷰를 생성한다는 관계를 알 수 있습니다.
  • 그리고 컬렉션의 요소가 Identifiable 프로토콜을 따르도록 하여 SwiftUI가 데이터에서 안정적으로 ID를 얻어올 수 있도록 제약합니다.

  • 이렇게 생성된 View는 View의 ID를 제공한 데이터와 연결되므로, 데이터에서 좋은 ID를 선택하면 View의 수명을 데이터에 맞게 결정할 수 있습니다.

  • View의 값은 일시적입니다.
  • 그러나 View의 ID는 그렇지 않고 View의 생명 주기와 같이 유지됩니다.
  • View ID를 제어할 수 있으며, 이를 통해 View의 상태 값의 수명을 결정할 수 있습니다.
  • SwiftUI는 Identifiable 프로토콜을 최대한 활용하므로 데이터와 연결된 경우 데이터에서 안정적인 식별자를 선택하는 것이 중요합니다.

 

View Update

  • 다음으로 SwiftUI가 UI를 업데이트하는 방법을 위해 위의 간단한 View 예제가 있습니다.
  • 처음의 두 가지 속성과 같은 값은 Dependency라고 불리며 View에 대한 입력입니다.
  • Dependency가 변경되면 새로운 구성으로 View가 업데이트됩니다.
  • Body는 View의 계층을 구성합니다.
  • Action은 View의 Dependencies에 대한 변경을 일으킵니다.

  • 위 코드와 동일한 구조의 다이어그램을 보면 각 View가 있고 Action을 수행하면 변화가 생겨 다시 View를 업데이트하는 구조를 나타냅니다.

  • 이러한 다이어 그램을 단순화하면 트리 구조와 같이 보일 수 있습니다.
  • SwiftUI에서 각 View는 고유한 Dependencies들을 가질 수 있기 때문입니다.
  • 하지만 실제로는 여러 데이터에 종속된 View가 있을 수 있기 때문에 트리 구조와는 약간 다르게 보입니다.

  • 결과적으로 이 연결을 다르게 표시하면 그래프임을 알 수 있습니다.
  • 따라서 이러한 구조를 "종속성 그래프"라고 합니다.

  • 예를 들어 맨 아래에 있는 항목이 수정되면 연결된 두 개의 뷰만 업데이트가 필요합니다.
  • 따라서 SwiftUI는 각 View의 body를 호출하여 갱신합니다.

  • 이후에 새로 생성된 body를 통해서 새 View도 업데이트되어야 하는지를 판단할 수 있습니다.
  • 이때 생성되는 값은 View보다 수명이 짧은 비교용으로만 사용되는 값입니다.
  • 이러한 구조를 바탕으로 SwiftUI가 변경이 필요한 뷰만 효율적으로 업데이트할 수 있습니다.

  • ID는 종속성 그래프의 핵심입니다.
  • 모든 뷰는 명시적, 암시적 ID를 통해 동일성을 체크하며, 이를 통해 SwiftUI가 변경 사항을 올바른 View에 효과적으로 적용합니다.

  • 지금까지 예제에서 본 것 이외에도 많은 종류의 Dependency를 가질 수 있습니다.

  • View의 수명은 ID의 지속 기간이며, 이는 ID의 안정성이 중요하다는 것을 의미합니다.
  • 불안정한 ID는 View 수명을 단축시킬 수 있습니다.
  • 안정적인 식별자를 가지면 View를 효과적으로 업데이트할 수 있기 때문에 성능에 도움이 됩니다.
  • SwiftUI는 ID를 사용하여 메모리 공간의 수명을 결정하므로 안정적인 식별자는 상태 정보의 손실을 방지하는 역할도 합니다.

  • 식별자의 안정성이 중요하다는 것을 볼 수 있는 위의 예가 있습니다.
  • 위의 예제 코드는 새로운 애완동물을 추가할 때마다 화면의 전체가 깜빡입니다.
  • 이 버그의 원인은 데이터가 변경될 때마다 새 식별자를 얻기 때문에 식별자가 안정적이지 않다는 점입니다.

  • 애완동물 배열의 인덱스를 ID로 사용하여도 비슷한 문제가 발생합니다.
  • 맨 처음에 항목을 추가하는 경우에는 그 뒤의 모든 동물의 인덱스가 변경되기 때문에 ID가 변경됩니다.

  • 따라서 인덱스와 같이 불안정한 ID 대신 데이터베이스에서 얻어온 정보나, 애완동물의 속성에서 얻은 것과 같은 안정적인 식별자를 사용해야 합니다.

  • 이와 같은 식별자가 영구한 값을 가지는 것도 중요하지만, 각 식별자는 고유한 값을 가져야 한다는 점도 중요합니다.
  • 각 식별자가 유일한 값이어야 애니메이션이 자연스럽고, 계층 구조의 의존관계가 효율적으로 유지됩니다.

  • 예를 들어, 애완동물이 좋아하는 간식 리스트에서 간식 이름을 ID로 하는 경우, 이미 존재하는 View와 동일한 이름의 간식을 추가하면 View에 표시되지 않을 수 있습니다.

  • 따라서 일련번호나 다른 고유 ID를 사용해야 합니다.

  • SwiftUI에서는 모든 식별자가 안정적이고 변경되지 않으며, 고유한 값을 가져야 합니다.

  • 이 예제에서는 구조적 동일성에 대해 볼 것입니다.
  • 예제에서는 애완동물의 간식의 유통기한이 지나면 셀을 흐리게 표시하는 기능을 추가하였습니다.

  • 흐리게 하는 로직을 보면, 날짜를 비교한 다음 View를 흐리게 만듭니다.
  • 언뜻 보기에는 괜찮아 보이지만 if문을 통한 분기가 있기 때문에 각 분기는 서로 다른 ID를 가지게 됩니다.
  • 따라서 실제로는 두 개의 View ID가 존재하게 됩니다.

  • 분기를 제거하면 View가 단일 ID를 가질 수 있도록 수정이 가능합니다.

  • 이렇게 해도 큰 문제가 없는 것은 opacity(1.0)을 적용해도 성능적 비용이 거의 없고, 화면도 opacity(1.0)를 적용하지 않는 것과 하는 것이 동일하기 때문입니다.

  • 이처럼 구조적으로 결정되는 ID를 위해 불필요한 분기는 최대한 자제하여야 합니다.
  • 분기를 추가할 때는 여러 View를 나타내는지 아니면 동일한 View의 두 가지 상태를 나타내는지 고려해야 합니다.
  • 단일 View를 위해서는 분기 대신 수정자를 사용하는 것이 종종 더 잘 작동합니다.

목차: https://nyancoder.tistory.com/2

Comments