[WWDC] Demystify SwiftUI - SwiftUI가 코드를 보는법
SwiftUI가 코드를 볼 때 무엇을 보는가?
- Identity(정체성) - SwiftUI가 앱의 여러 업데이트에서 동일 요소로 보거나 별개 요소로 보는 방법
- Lifetime(수명) - SwiftUI가 시간이 지남에 따라 View와 Data 존재를 추적하는 방법
- Dependencies(의존성) - SwiftUI가 인터페이스를 업데이트해야 하는 시기와 이유를 이해하는 방법
Identity(정체성) - SwiftUI가 앱의 여러 업데이트에서 동일 요소로 보거나 별개 요소로 보는 방법

사진에서 두 강아지가 다른 강아지인지 혹은 두 장의 같은 강아지인지 알 수 없다.
정보가 충분하지 않아 사실을 말할 수 없기 때문이다. 이 질문을 SwiftUI는 Identity(정체성) 이라고 부른다.

다른 예시로, 아이콘이 서로 완전히 독립 객체인지 알 수 없다.
완전히 다른 객체일 수도 있고 단지 위치만 다르고 다른 색을 가진 동일한 객체일수도 있다.
이러한 구분을 “한 상태에서 다른 상태로 전환하는 방식”을 변경하기 때문에 중요한 부분이다.
만약 서로 같은 객체라면 현 위치에서 다른 위치로 이동하는 동일한 뷰이기 때문에 전환 중에 객체가 아래로 내려가야 한다.
따라서 서로 다른 상태에 걸쳐 뷰를 연결하는 것이 중요하다. 이것이 SwiftUI가 뷰 사이를 전달하는 방법을 이해하는 방식이기 때문이다. 이게 View Identity 핵심 가치다.
SwiftUI는 두 가지 다른 유형의 Identity에 초점을 두고 있다.
- Explicit of Identity(명시적 정체성): 맞춤형 또는 데이터 기반 식별자를 사용한다.
- Structural Identity(구조적 정체성): 뷰 계층 구조에서의 유형과 위치에 따라 뷰를 구별한다.
Explicit of Identity(명시적 정체성)

사물을 식별하기 위해 보통 이름을 부여할 수 있다.
하지만 두 사물이 똑같이 생기고 같은 계열일 가능성이 높을 수 있어서 이름으로는 고유성이 부족할 수 있다.
이름이나 식별자를 할당하는 것은 Explicit of Identity(명시적 정체성) 의 한 형태이다.
이는 강력하고 유연하지만 누군가 어딘가에서 이름을 추적해야 한다.

이미 익숙할 수 있는 Explicit of Identity의 한 형태는 UIKit 및 AppKit 전체에서 사용되는 포인터 Identity다.
SwiftUI에서는 포인터 Identity를 사용하지 않고 UIView와 NSView에서 사용한다.
UIView와 NSView는 클래스이므로 메모리 할당에 필요한 고유한 포인터를 가진다. 포인터는 Explicit of Identity의 자연스러운 원천이라고 한다.
포인터를 사용해 개별 뷰를 참조할 수 있으며 두 뷰가 동일한 포인터를 공유하는 경우 실제로 동일한 뷰임을 보장할 수 있다.

하지만 SwiftUI View는 일반적으로 클래스 대신 구조체로 표현하는 값 타입이기 때문에 SwiftUI는 포인터를 사용하지 않는다.
가장 중요한 점은 값 타입은 SwiftUI가 해당 View에 대한 지속적인 Identity로 사용할 수 있는 정식 참조가 없다는 것이다.

여기서 사용된 id는 Explicit of Identity의 한 형태이다.
각 강아지의 인식표 Identity는 View를 명시적으로 식별하는 데 사용된다.
강아지 컬렉션이 변경되면 SwiftUI는 해당 Id를 사용해 무엇이 변경되는지 이해하고 목록 내에서 올바른 애니메이션을 생성할 수 있다.

또 다른 예제인데 여기서는 ScrollViewReader를 사용해 하단 버튼을 사용하여 뷰 상단으로 이동한다.
.id() Modifier는 사용자 정의 식별자를 사용해 뷰를 명시적으로 식별하는 방법을 제공한다.
현재 페이지 상단의 헤더 뷰에 식별자를 추가한 것이다.
추가로 해당 식별자를 뷰 프록시의 scrollTo(_:) 메서드에 전달해 SwiftUI에게 특정 뷰로 이동하도록 지시할 수 있다.
이 장점은 모든 뷰를 명시적으로 식별할 필요가 없고 헤더와 같이 코드의 다른 곳에서 참조해야 하는 뷰만 식별할 수 있다는 것이다.
ScrollViewreader, ScrollView, 텍스트 및 버튼에는 명시적인 식별자가 필요하지 않는다.
그러나 정체성이 명시적이지 않다고 해서 이러한 뷰가 전혀 정체성이 없다는 것을 의미하지는 않는다.
명시적이지 않더라도 모든 View는 Identity가 있기 때문이다. → 여기서 구조적 정체성이 등장
Structural Identity(구조적 정체성)
SwiftUI는 뷰 계층 구조를 사용하여 뷰에 대한 암시적 ID를 생성하므로 사용자가 직접 ID를 생성할 필요가 없다.

비슷한 두 마리의 강아지가 있지만 이름은 모르지만 각각을 식별해야 한다고 가정해보자.
움직이지 않음을 보장할 수 있다면 “왼쪽에 있는 개”, “오른쪽에 있는 개”와 같이 위치에 따라 식별할 수 있다.
SwiftUI는 이런 객체를 구별하기 위해 상대적인 배열을 사용한다고 한다 이것이 Structural Identity이다.

SwiftUI는 API 전체에서 Structural Identity를 활용한다.
전형적인 예시로 View 코드 내에서 if 문과 기타 조건부 논리를 사용하는 경우다.
조건문의 구조는 각 뷰를 식별하는 명확한 방법을 제공한다.
첫 번째 뷰는 조선이 true일때만 표시되고 두번째 뷰는 false일때만 표시된다.
하지만 이 경우 SwiftUI가 볼때 이 뷰가 현재 위치에 유지되고 위치가 바뀌지 않음을 정적으로 보장할 경우에만 작동한다.
SwiftUI는 뷰 계층 구조의 type structure를 살펴봄으로써 이를 수행한다.

SwiftUI가 View를 볼 때 generic type을 본다.
이 경우 if문은 참 거짓 콘텐츠에 대해 ConditionalContent View 뷰로 변환된다.
이러한 번역은 Swift의 result builder 타입의 ViewBuilder에 의해 제공된다.
View 프로토콜은 속성의 논리 문에서 단일 일반 뷰를 구성하는 ViewBuilder의 body 속성을 암시적으로 래핑한다.
body 속성의 some View 반환 유형은 이 정적 복합 유형을 나타내는 자리 표시자로서, 코드를 복잡하게 만들지 않도록 숨겨준다.
이 generic type을 사용해 SwiftUI는 실제 View가 항상 AdoptionDirectory이고 False뷰가 항상 DogList임을 보장하여 암시적으로 안정적인 Id가 할당될 수 있게 한다.
some view
- “정확한 타입 이름은 숨길게 하지만 내부적으로는 항상 하나의 구체 타입을 반환해야 해”
- @ViewBuilder가 if else를 _ConditionalContent<TrueView, FalseView>라는 하나의 View 타입으로 감싸주기 때문에 some View가 성립한다.
전략 1: SwiftUI에서 조건 분기마다 고유한 id를 가진다
앞서 살펴본 코드이다. 상단 코드에는 각 조건 분기에 대해 서로 다른 뷰를 정의하는 if문이 있다.
SwiftUI의 if문의 각 분기가 고유한 id를 가진 다른 뷰를 나타낸다는 것을 이해하기 때문에 이로 인해 뷰가 전환된다.
전략 2: Identity를 유지하고 유연한 전환을 제공(Apple 권장)

또는 동일한 뷰에 대해 레이아웃과 색상만 변경하는 단일 뷰를 가질 수도 있다.
다른 상태로 전환되면 뷰는 다음 위치로 부드럽게 미끄러진다. 이는 일관된 id로 단일 뷰를 수정하기 때문이다.
이 두 전략 모두 작동할 수 있자만 Apple는 두 번째 접근 방식을 권장한다.
기본적으로 Identity를 유지하고 유연한 전환을 제공하도록 노력해라고 권장한다.
이유는 뷰의 라이프사이클과 상태를 보존하는 데 도움이 된다고 한다.
AnyView

앞서 우리는 AdoptionDirectory와 DogList 사이를 전환하기 위해 이 if문을 작성했었다. SwiftUI가 이 코드를 보면 오른쪽에 제네릭 타입 구조가 보인다.

이 코드는 개의 품종을 나타내는 뷰를 얻기 위한 헬퍼 함수다. 함수의 각 조건부 분기는 서로 다른 종류의 View를 반환하므로 Swift에서는 전체 함수에 대해 단일 반환 유형이 필요하므로 이를 모두 AnyView에 래핑한다.
불행이도 이는 SwiftUI가 내 코드의 조건부 구조를 볼 수 없다는 의미이기도 하다. 대신 AnyView를 함수의 반환 유형으로 간주한다. 이는 AnyView가 유형 삭제 래퍼 유형(type-erasing wrapper type) 라고 불리기 때문이다. 즉 generic signature에서 래핑하는 뷰 유형을 숨긴다. 그러나 더 중요한 것은 이 코드가 사람이 읽기에도 어렵다는 것이다.

이 오류를 피하려면 View프로토콜이 암시적으로 ViewBuilder를 래핑하기 때문에 뷰의 body 속성은 특별하다는 점을 기억해야 한다.
이는 속성의 논리를 단일 일반 뷰 구조로 변환해줄 수 있다.

이제 AnyView가 제거되어 코드의 가독성이 늘어났다. 그리고 결과 타입 시그니처를 보면 조건부 콘텐츠 트리를 사용해 함수의 조건부 논리를 정확히 복제하여 해당 구성 요소의 ID를 제공한다.

참고로 조건문의 synatic sugar인 switch를 사용해도 동일한 view’s type signature(뷰의 유형 서명)이 정확히 유지된다.
AnyView정리
- 코드에서 유형 정보를 삭제할 수 있다.
- view builder를 활용해 불필요한 AnyView를 제거할 수 있다.
- AnyView는 컴파일러에서 정적 유형 정보를 숨기기 때문에 유용한 진단 오류 및 경고가 코드에 표시되지 않는 경우가 있다.
- 필요하지 않을 때 AnyView를 사용하면 성능이 저하될 수 있다.(16:11)
- 가능하다면 코드 주변에 AnyView를 전달하는 대신 제네릭을 사용해 정적 유형 정보를 보존해라.