본문 바로가기

iOS

[WWDC 21] Discover concurrency in swiftUI

Swift 5.5는 비동기 처리와 관련 Breaking change가 있지요,

async, await을 지원하며 최신 언어들과 결을 같이 했고,

이와 함께 Actor 개념을 도입하여 Race condition에 대한 좋은 방안을 제안해 주었습니다. 

 

async 개념이 도입되며 상당히 많은 부분에서 개선이 있었는데요, SwiftUI를 중점적으로 다뤄볼 예정입니다.

asyncImage, task, main Actor에 대해서 다루고 글을 작성해 보겠습니다.

 

기본 SwiftUI와 concurrency에 대한 지식이 조금 필요합니다. 나중에 추가 글을 작성하게 되면 아래에 링크하겠습니다.

 

 

위 세션은 `SpacePhoto` 모델으로 시작합니다. 
작업 내용을 간략히 하면, SpacePhoto를 보여주는 List를 만들 것입니다.  SpacePhoto는 이미지를 가지고,

모델 내에 해당 url을 가지고 있습니다. 

위를 이용해 Data Driven View를 만들 것입니다.

 

이 SpacePhoto를 fetching하고, holding할 수 있는 모델을 만들고, 이름은 `Photos`

ObservableObject를 따르므로, 이건 내부 변수 items가 변할때 마다, 객체의 변화에 따라 view도 바뀌게 될 것입니다.

 

그러니까 이런 뷰를 만든단 뜻

 

위 뷰 이름은 CatalogView, 그리고 각각의 뷰 (UIKit 식으로는 cell)는 PhotoView 입니다.

 

그러니까, ForEach를 통해 리스트를 그릴거고, 이대로 그리면

요런식으로 되겠죠 😀

그럼 이제 PhotoView를 구현하러 갑니다.

 

그 전에, Observable Object가 어떻게 뷰를 그리는지에 대해서 짧은 설명을 이어합니다.

* SwiftUI의 View Update LifeCycle이 있고, 올해 Swift가 5.5 버전이 되며 main Actor 개념이 도입되었습니다.

run loop가 있지요, UIKit과 개념이 다르지 않고, 뷰를 그리는 한 사이클이라고 생각하면 좋습니다

model이 update되면, view를 다시 update하는 메커니즘에 대한 짧은 설명이에요.

 

아까 SpacePhoto를 받아오고 저장하는 Photos 모델이지요?
여기에서 updateItem은 main run loop에서 돌고, 그만큼을 오른쪽에 파란 블록으로 표시한 것입니다.

 

items = fetched 으로 고

@Published 어노테이션이 적용된 변수에 값 할당을 하면, 

objectWillChange event를 트리거합니다.

 

그럼, items에 새로운 값이 들어갔지요?

그리고 SwiftUI가 objectWillChange를 관측하게 되면, 

 

기존 item의 Snapshot을 찍어서, 바뀐 모델과의 변화를 계산하고, snapShot을 찍은 run loop의 다음 tick (다음 바퀴)에

바뀐 값과, 기존 값의 변화를 비교하고 Photos에 대해 뷰 변화를 실행하게 됩니다.

 

(1) items에 새로운 값이 들어감 -> objectWillChange가 trigger

(2) SwiftUI가 objectWillChange를 관측하면 이전 값을 SnapShot

(3) SnapShot을 찍은 다음 tick에 SnapShot의 값과 변화된 값을 비교하고 뷰 변경

 

자, 그럼 이게 왜 문제가 되는 코드인지를 보겠습니다.

 

첫번째 문제, fetchPhotos()는 값이 비싼 함수입니다. API를 통해 가져오므로 얼마나 걸리는지 알 수 없습니다.

따라서 main run loop를 점유하게 되고, 화면이 멈추게 되는걸 유저가 볼 수 있겠죠?

 

이를 개선하고자 이런 코드를 작성할 수 있습니다. 

바로바로 main Queue를 점유하지 않도록 특정 Queue에서 작업을 수행하게 하면 됩니다.

자, 그런데 여기서도 미묘한 문제가 있을 수 있습니다.

 

그럼, objectWillChange, items 대입이 mainActor에서 벗어난 곳에서 수행되게 되겠지요?

 

그럼, SnapShot을 찍은 곳 다음 roop에 비교가 들어가야 하는데, 

통제되지 않은 시점에 의해서 바뀌기 전에 값과 비교가 될수 있단 말이에요. 말이 어려웠...나..요?

 

그림을 보면 이해가 쉽습니다, 이전값은 빈 배열이고, run loop tick때의 값도 빈 배열이 됩니다.

이유는, items에 아직 대입이 되기 이전이기 때문이에요.

 

그렇다면 SwiftUI는 모델이 바뀌지 않은 것으로 인식하고 뷰 업데이트를 하지 않는 문제가 있을 수 있겠죠?

-> 통제 가능한 범위에 objectWillChange를 트리거한다가 핵심이 되겠습니다.

 

정리하면,

1. objectWillChange,

2. state 변경 (값 변경)

3. 그 다음 run loop tick에 비교하는 순서 보장.

 

그럼 어떻게 보장하냐? 이제 새로운 API를..! 도입하며..! 개선할 수 있어요.

이름하야 await. 두둥.

자, 그럼 main actor가 어떻게 양보가 되는 것인지 살펴봅시다.

 

이런 코드를 쓸겁니다.

비용이 비싼 fetchPhotos가 async 함수가 되었습니다.

이 경우, fetchPhotos 함수는 main Actor에 대한 통제권을 양보합니다. 그래서 to yield the main actor.

 

그림으로 보겠습니다.

빨간색 오래 걸리는 fetchPhoto 함수가 main Run loop에서 멀찍이 떨어져 있는걸 볼 수 있으십니다.

 

자, 그럼 궁금한 것이, 그럼 이건 어떻게 objectWillChange와 값 변경의 순서를 보장할 수 있느냐?

이건 이제 Swift compiler 단에서 보장하고, 아래 방법을 제안합니다.

 

바로바로, @MainActor Annotation을 지정해주는 방법.

이 어노테이션이 지정하는 바는, 여기에 저장된 변수는 모두 main Actor에서만 접근이 가능함을 보장하는 것입니다.

 

요렇게 정리 가능.

 

1. 이제 global Queue 쓰지 마셈. tick 안에서 업데이트 되는것을 보장할 수 없음. 사실 이건 기존에도 그랬음

2. 대신 await 쓰셈. await 쓰기 시작하면 사실 async 밭이 되긴 하는데, 그건 어쩔수 없음. 시대의 흐름인 듯

 

길었지만 요렇게 두줄 요약이 가능해집니다.

async, await은 이렇게 사용합니다. 패러다임의 변화가 생긴다는걸 이번 주제로 예측할 수 있겠습니다.