[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/SwiftUI

[SwiftUI] SwiftUI의 신비를 풀어보자 (Demystify SwiftUI)

inu 2023. 9. 16. 16:17
반응형

본 포스팅은 WWDC21 : Demystify SwiftUI 세션을 기반으로 작성되었습니다. (Demystify = 신비를 풀다)

 

https://developer.apple.com/videos/play/wwdc2021/10022/


SwiftUI가 우리 코드를 바라볼 때 이들을 어떤 것을 중점적으로 살펴보고 있을까? 답은 3개이다.

  1. Identity : 어떻게 특정 요소를 동일한 것 혹은 다른 것으로 파악하는지
  2. Lifetime : 어떻게 뷰와 데이터의 존재 여부를 시간에 따라 추적하는지
  3. Dependency : 어떻게 인터페이스를 업데이트할 타이밍과 그 이유를 이해하는지

SwiftUI에서는 이 세가지 개념을 통해 “언제, 무엇을, 어떻게” 변화시켜야하는지 파악한다.

Identity

먼저 '어떻게 특정 요소를 동일한 것 혹은 다른 것으로 판단할 것인가?'에 대한 부분을 살펴보자.

 

SwfitUI에서는 view identity의 구분에 따라 View의 Transition을 다르게 수행하기 때문에 이는 매우 중요하다. (ex. 같은 View라면 자연스러운 이동 애니메이션, 다른 View라면 fade in/out)

 

Identity에는 두가지 타입이 있다.

  1. Explicit Identity : custom 혹은 데이터기반 identifier
  2. Structural Identity : 뷰 hierarchy에서의 위치 및 타입에 따른 identifier

Explicit Identity

먼저 Explicit Identity는 일종의 이름표를 부여하는 것이라고 생각하면 된다. 강력하고 유연하지만 항상 이름을 추적하고 있어야한다.

 

UIKit에서 사용하던 pointer identity가 대표적인 예시이다.

UIView는 클래스기 때문에 메모리 할당에 따른 고유한 포인터가 존재한다. 이를 기반으로 만약 특정한 View 두개가 동일한 포인터를 공유한다면 동일한 뷰라고 말할수 있는 것이다.

 

그런데 SwiftUI에서는 view가 struct로 구성된다. 즉, 포인터가 존재하지 않는다.

따라서 SwiftUI에서는 다른 방식을 사용한다. List를 생성할 때 각 view를 식별하기 위해 id를 사용하는 것도 일종의 Explicit Identity이다. id를 기반으로 변경점을 파악하고 올바른 애니메이션을 생성하는 것이다.

id(_:) modifier를 통해 id를 부여해 identifier를 설정해줄 수도 있다. 위 이미지의 예시에서는 id를 기반으로 위치를 찾아가 scroll을 수행한다.

 

여기서 알 수 있는 것은 SwiftUI에서 모든 View가 explicit identifier를 가지고 있지 않다는 것이다.

Structural identity

그래서 사용하는 것이 Structural Identity이다. 이는 View hierarchy를 이용해 view에 대한 identity를 결정한다.

쉽게 설명하자면, 이름을 모른다면 위치로 구분하자는 접근방식이다.

 

SwiftUI에서는 API 전반에 걸쳐 Structural identity를 사용한다. 가장 일반적인 예는 View 코드에서 if 와 같은 조건부 로직을 사용하는 것이다.

위 View는 “True” View, 아래 View는 “False” View로 구분한다. 하지만 이는 SwiftUI가 이런 view이 그 자리에 그대로 있고 Swap되지 않는다는 보장하에서만 동작한다. SwiftUI는 이를 극복하기 위해 view hierarchy 상에 존재하는 type structure를 살펴본다.

 

SwiftUI가 View들을 볼 때는 그들의 generic type을 본다. 왼쪽의 코드는 오른쪽과 같이 번역되는데, 이는 resultBuilder의 일종인 @ViewBuilder 를 통해 번역된다. view protocol은 암시적으로 body property를 wrapping하여 우리의 로직으로부터 단일 generic view를 생성한다.

 

some View로 복잡한 정적 타입을 추상화하여 나타낼 수 있다. 내부의 _ConditionalContent 덕분에 우리는 True일 땐 AdoptionDirectory, False일 땐 Dog List를 보여준다는 사실을 ‘보장’할 수 있게 된다. 이를 기반으로 암시적이지만 안정적인 identity를 형성할 수 있게 되는 것이다. (각각은 다른 View임을 확인)

 

7

이렇게 코드를 작성하면 각각의 View를 다른 View로 인식한다.

 

반면 이렇게 코드를 작성하면 하나의 View로 인식한다. 일반적으로는 이런 방식을 권장한다. View의 Lifetime과 State를 보존하는데 도움을 주기 때문이다.

AnyView

이런 로직을 부숴버리는 나쁜 친구가 있는데 그것이 AnyView이다.

아주 안좋은 예시. some View를 리턴할 수는 있겠지만 절차적으로 작성되어 있어서 내부 로직을 알 수가 없다. 그렇게되면 아까와는 다르게 _ConditionalContent와 같은 것을 활용해서 번역도 못하고 그냥 오른쪽같이 AnyView 하나의 정보를 들고 있는 것이다. 즉, Structural Identity를 알 수 없다.

 

어찌어찌 SwiftUI식으로 변경해보려하니까 에러가 발생한다. 불투명(Oqaque) 타입 some은 반환 타입이 같아야하는데, 현재는 모두 다르고, 리턴 처리도 제대로 해주고 있지 않다. 오른쪽처럼 기존에는 ViewBuilder가 든든하게 있어줘서 이를 하나의 View로 변경하는 것이 가능했는데 이건 없으니까 모두 각각의 View로 인식되고 이를 각각 리턴해줘야 한다. 

 

명시적으로 추가하니 구조가 깔끔하게 처리되었다. AnyView는 이처럼 코드에 악영향을 주니까 쓰지말자.

  • 코드 읽기 어려워짐
  • 걍 제어문 + ViewBuilder 조합을 쓰셈
  • AnyView는 정적 타입의 정보를 그냥 숨겨버려서 오류나 경고를 틀어막는거나 마찬가지
  • 성능적으로도 안좋음

Lifetime

위 고양이의 이름은 "Theseus"이다. 이 고양이의 상태가 변하더라도 이 고양이는 항상 “Theseus”라는 이름을 갖는다. 즉, Identity가 시간에 따른 값 변화를 가능하도록 만들어주는 셈이다. → Identity가 연속성(continuity)을 가지도록 도와준다.

 

Identity & Lifetime

view도 마찬가지이다. 시간에 따라 다른 값을 가질 수 있다. 각각의 값은 view 입장에서는 모두 독립적으로 ‘다른 값’이다. 하지만 이런 ‘다른 값’들을 연결해주는 것이 앞서 살펴본 Identity 개념이다.

 

SwiftUI에서는 view의 데이터 사본을 보유하고 있다가 특정 값의 변화가 발생할 경우 이 사실을 알아차린다. 그 다음에는 기존에 보유하고 있던 사본을 삭제한다.

 

여기서 알아야하는 것이 view의 value는 결코 view의 identity가 아니라는 것이다. view의 value는 일시적이며 이는 view의 lifetime과도 관련이 없다.

 

view가 생성되고 나타나면 SwiftUI는 앞장에서 다뤘던 테크닉을 기반으로 identity를 할당한다. 시간이 지나면서 이들의 '값' 자체는 변할 수 있지만, identity는 변하지 않는다. 만약 변한다면 그것은 lifetime이 끝났음을 의미한다. → view의 lifetime은 곧 identity의 유효기간과도 같다.

 

State와 StateObject를 활용한 예시를 보면 view의 “값 자체”는 새롭게 생성되지만, SwiftUI는 이들을 같은 view로 인식하고 State 및 StateObject의 변화를 받아들일 수 있게 된다.

 

Data- driven constructs

이제 조금 다른 예시를 보자. SwiftUI에서는 위와 같이 데이터의 identity를 활용하는 Data-driven construct들이 존재한다. 가장 직관적인 예시는 ForEach이다.

 

ForEach를 기반으로 view를 생성할 때 RescueCat의 collection을 활용한다. 이 때, tagID라는 UUID를 identity 삼아 생성하고 있다. 만약 위의 예시처럼 객체가 identifiable protocol을 채택하고 있다면 좀 더 쉽게 처리가 가능하다. 내부적으로 protocol을 통해 제공된 정보를 기반으로 identity를 형성하는 것이다.

 

cf. Swift에서  “특정 상황에 대한 해결방법에 대한 제약조건”을 타입으로서 표현할 수 있는것은 매우 유용한 기능이다. (identity를 표현해주기 위해 id를 정의해야하는 제약조건을 주는 protocol type)

 

생성자 모양을 보면 ForEach는 data의 collection을 view의 collection으로 만들고 싶어함을 직관적으로 알 수 있다. 하지만 무엇보다 흥미로운 것은 Identifiable로 제한한 것이다. 이렇게해서 각 view가 identity를 형성함으로써 SwiftUI가 lifetime동안 data를 traking할 수 있는 것이다.

 

identifier를 어떻게 선정하느냐에 따라 내가 만들 view와 data의 lifetime이 달라질 수 있음. → identifier 선정은 매우 중요! (위의 예시에서 만약 name을 id로 설정하면 이름이 같은 객체는 같은 객체로 인식해버리는 문제가 발생한다.)

 

정리

  • view의 값자체는 일시적이다.
  • 하지만 identity를 기반으로 특정한 lifetime을 가지게된다.
  • 내부 상태의 지속성은 lifetime과 함께 묶여있다.
  • SwiftUI 내부에서는 Data-driven components같이 Identity를 활용하는 경우가 많으므로, 안정적 identifier를 선정하는 것이 중요하다.

Dependency

View에서 가지고 있는 상태들을 dependency라고 표현할 수 있다. 이러한 view의 dependency에는 Binding, Environment, State, StateObject, ObservedObject, EnvironmentObject과 같은 observable한 object들이 있다. 만약 dependency가 변경되면 view는 새로운 body를 만들것을 요구받는다. 이 때 dependency를 변경하는 것은 action이다.

 

이는 간단한 케이스라 구조가 트리처럼 깔끔하게 형성된다.

 

하지만 dependency는 훨씬 복잡하게 구성될 수 있다. 단순 트리 구조가 아닌 형태까지 이른다.

 

다시 정리하면 이런 느낌이다. 이건 그래프지 트리가 아님. (이것을 ‘Dependency graph’라고 부름)

 

만약 특정한 dependency가 변경되면, 기존의 해당 dependency를 사용하던 view의 body는 무효화되고 새로운 body를 생성하게 된다.

이렇게 dependency로 인해 변경이 일어나면 자연스럽게 그들과 이어지는 다른 view에도 변경이 일어날 수 있다. 하지만 항상 그런 것은 아니다. view들이 value type이기 때문에 SwiftUI는 효율적으로 이들을 비교해 필요한 subset만 업데이트할 수 있다.

 

view의 값들 자체는 단지 비교를 위해 사용될 뿐이지만, view 자신은 독립적인 lifetime을 가진다. 이게 가운데에 있는 저 view(빨간점으로 표시된 view 사이에 위치한 view)같은 view들이 새롭게 생성하는 것을 피하는 방법이다. 

Improve use identity

앞서 언급했듯이 적절하고 안정적인 identifier를 선택하는 것은 중요하다.  왜냐하면

  • lifetime에 직접적인 영향을 준다.
  • view를 효율적으로 업데이트하여 성능에 도움이 된다.
  • Depedency로 인한 영향 최소화해준다.
  • 상태값 유지하도록 해준다.

 

안정적일 뿐 아니라 유니크해야한다.

  • 애니메이션에 영향을 주고
  • 성능적으로 유니크한게 유리하고
  • dependency도 효율적으로 반영되기 때문이다.

name을 사용하면 같은 이름일 경우 제대로 동작하지 않게 된다.

 

serialNumber같이 유니크한 값을 사용해야한다.

 

각 내부 Cell View에 Custom ViewModifier를 적용한다고 해보자. 

이 때 이런식으로 Custom ViewModifier를 구성하면 두개의 다른 identitiy를 가지게 된다. (If문으로 View 브랜치 형성)

이렇게 해야 단일 identity를 보유할 수 있다. 굳이 새로운 매번 새로운 identity의 view를 사용할 필요가 없다. 이와 같이 우리가 여러 view를 만들어야하는 상황인지, 다른 상태를 가지는 같은 view가 필요한 상황인지 잘 판단해야한다.

 

세션이 끝나니 선글라이스를 쓰고 인사하는 엔지니어...

-끝-

반응형