SSG의 뻔하지 않은 SwiftUI 도입 고민

Sunny Jeon
SSG TECH BLOG

--

안녕하세요? 저는 App개발팀의 iOS 개발자 전선수입니다.

저희 파트에서 SwiftUI 도입을 고민하면서 느꼈던 점을 소개하려고 합니다. 본 내용은 제가 2022 summer let us: Go! With SSG 에서 발표했던 “SSG의 뻔하지 않은 SwiftUI 도입 고민”의 아티클 버전이오니, 해당 발표를 직접 듣고 싶으신 분은 아래의 링크 하단의 “영상 다시보기” 2번 영상을 확인해주세요. 🤭

https://let-us-go-2022-summer.vercel.app

주의 ⚠️

이 아티클은 영상의 중반부 내용까지만 출판하며, 후반부에 해당하는 WWDC22의 특정 Swift 5.7의 문법(some, any의 개선), Opaque Type, Generic, Protocol 등은 다음 아티클을 기대해주세요. 🫣

기본적으로 SwiftUI를 한 번이라도 써봤거나 유경험자, 경험은 없더라도 저희처럼 UIKit 위주의 구조가 있는데 SwiftUI 도입을 검토하는 분들을 대상으로 합니다. 따라서 관련 지식이 아예 없는 편이라면 아래 Apple 공식 SwiftUI 소개 링크를 읽고 오시는 것도 도움이 될 것입니다. 😆
https://developer.apple.com/xcode/swiftui/

SwiftUI 도입 검토 배경

아래와 같은 3가지 배경을 근거로 도입을 고민하게 되었습니다.

  1. 빠르면 2022년 하반기 중 SSG 앱 최소 지원버전이 iOS 13으로 상향할 예정에 있음
  2. SSG의 매장이 모듈화가 잘 되어있는 화면들로 구성되어 있음
  3. 각 모듈의 UI가 대부분 XIB + AutoLayout 구성을 따르고 있음

iOS 13을 최소 지원버전으로 올리게 된다면, 검토가 필요한 기술들이 몇 개 있는데, 대표적인 것이 SwiftUI, Swift Concurrency(async, await 등), Combine으로 정리할 수 있습니다. 저희 파트(두병님, 혜연님이 추가로 도움을 주셨습니다.)에서는 그중에서 연초에 SwiftUI 리서치 및 적용했을 경우의 실용성과 장단점을 미리 확인해보았습니다. 그 과정에서 탄생한 게 이 아티클의 재료라 할 수 있겠습니다. 🙃

또한, 저희 SSG 매장은 모듈화가 잘 되어있는 화면들로 구성돼있는데, 이는 각 모듈 유닛 전부를 바꾸지 못하더라도 1개부터 해서 소규모 단위로 SwiftUI 전환이 가능함을 의미합니다. 또한, XIB + AutoLayout 구성에서 바꿀 때 유리한 점은 이 설계방식은 “frame 위주의 코드에 비해 성능은 포기하는 대신 높은 유지보수성을 확보”하는 것에 있는데, 이 역할을 SwiftUI의 Preview가 대신 수행할 수 있어 유기적인 전환이 가능하다는 것입니다.

iOS 13 + SwiftUI 조합의 희망과 절망편 😇😈

사실 iOS 13 버전에서의 SwiftUI는 처음 출시된 버전인 만큼, 이 버전의 조합으로 할 수 있는 것은 너무 한정적입니다. 그렇다면, iOS 14와 iOS 13의 버전 별 SwiftUI 지원 기능의 차이를 알고있어야 합니다.

왜냐하면, iOS 14에서 어차피 지원되는 기능이라면, iOS 13에서 우회하여 개발한다고 하더라도 결국 최소 지원버전 상향 시 이를 교체해야 합니다. 이는 기술 부채로 남게되고, 기껏 전환을 위해 개발한 공수마저 허사가 될 수도 있습니다. 그럼에도 긍정적인 방향으로 최소 지원 버전의 상향과 상관 없는(기술 부채를 발생시키지 않을) 신기능들을 정리해보았습니다. 🫡

  • TextEditor
    UIKit의 UITextView에 대응되나, SSG의 매장 내 사용 영역이 없습니다.
  • ProgressView
    UIKit의 UIActivityIndicatorView, UIProgressView에 대응되나, SSG는 독자적인 로딩바를 사용하므로 교체 니즈가 없습니다.
  • Map
    MapKit의 MKMapView에 대응되는 기능이나, SSG 내 네이티브 지도 사용 영역이 없습니다.
  • DatePicker
    UIKit의 UIDatePicker에 대응되나, 마찬가지로 SSG의 매장 내 사용 영역이 없습니다.

이렇게 억텐으로 SwiftUI로 전환해도 되는 이유들을 짜내보았는데, 최소 버전이 iOS 14여야 하는 이유들도 그만큼 많았습니다. 😭

  • LazyVStack, LazyHStack
    VStack, HStack 사용 시 대량의 데이터를 lazy loading 가능한 장점이 있습니다. 따라서 퍼포먼스 이슈가 해결됩니다.
  • LazyHGrid, LazyVGrid
    Grid 형태의 화면을 그릴 때 사용하며 마찬가지로 lazy loading 가능한 장점이 있습니다. 특히, SSG의 매장 내 유닛 중 가로스크롤이 가능한 유닛 일부에 사용하면 좋을 것 같다는 생각이 들었습니다.
  • ScrollViewReader, ScrollViewProxy
    코드로 ScrollView의 포지션을 제어할 수 있습니다. 하지만, SSG의 적지 않은 매장에서 스크롤 지점의 컨트롤이 필요하므로 이를 나중에 적용한다면 기존 로직이 기술 부채가 될 것이 뻔합니다.
  • onChange modifier
    main thread 단에서 side effect을 트리거할 수 있으며, Combine 없이 이밴트 캐치가 용이하다는 장점이 있습니다.

위의 희망과 절망은 SwiftUI의 기능들을 딥하게 파고들면, 더 많지만(사실은 절망만 더 많아지지만) 대표적으로 4가지씩만 짚어보았습니다.

희망 편 1, 빠른 전환의 장점이 이 모든 단점을 커버할 수 있나요?

위의 기술 부채를 감안하더라도 SwiftUI 도입 장점이 더 크다면, 충분히 도입할 만한 근거가 될 수 있습니다. 실제로 SSG의 모듈매장 구조는 재사용 가능한 UI로 유닛 단위 개발이 잘 되어있어, 각 구성원이 SwiftUI를 사용하는 데 능숙하다면 SwiftUI로 금방 전환이 가능합니다.

위의 이미지에서 보듯이 똑같은 SSG 홈 매장 내의 유닛이지만, 이 2개의 유닛은 다른 유닛이 아닌 재사용 가능한 타이틀유닛으로 매장 기획자가 언제든 원하는 위치에 꽂을 수 있습니다.

저희 화면의 타이틀 모듈유닛을 예제로 SwiftUI 전환을 위한 Step-up 해보겠습니다.

1️⃣ ObservableObject에 사용될 ViewModel을 준비합니다. 예를 들면, TitleUnitPresentable(타이틀 유닛 UI 표시 가능한)을 adopt하는 샘플 클래스를 정의합니다.
2️⃣ UI대로 그립니다. 내부 구성 요소가 2개 있는 구조이며, 예를 들어 그 구성요소가 2px 간격이라고 하면, 이는 VStack(alignment: .leading, spacing: 2)의 코드로 대응할 수 있습니다.
3️⃣ 두 UILabel은 Text로 정의하여 font, foregroundColor 등을 설정합니다. 두 타이틀은 최대 2줄이므로 lineLimit을 2로 적용하면 되겠죠?
4️⃣ 이 모듈유닛의 여백을 상단 45 좌우 15, 하단 0이라 하면, 가장 외곽에 padding을 이 수치로 적용합니다.

이 사고방식과 설계를 장착해서 속도만 붙는다면, 수십 개의 셀을 금방 만들어낼 수 있겠죠? 🤥

희망 편 2, SwiftUI + Preview 조합이 XIB + AutoLayout 조합을 대체할 수 있나요?

XIB + AutoLayout 방식의 장단점을 간단하게 쓰자면, frame 코드 작성 방식보다 낮은 퍼포먼스를 보여주는 대신, 코드 가독성이 훨씬 좋고 유지보수에 용이하다는 것입니다. TMI스럽지만 더 구체적으로 설명해보자면, 🧐

  • XIB는 필연적으로 파일 시스템을 거쳐 XIB을 메모리로 불러오는 방식입니다.
    frame 방식은 이 과정이 필요 없으며, XIB 파일을 찾아서 메모리로 로드하는 과정이 매우 오래 걸립니다.
  • AutoLayout은 XIB에서 사용했을 때, GUI 사용성이 좋습니다.
    물론 코드로만 AutoLayout을 설정하고 사용할 수도 있습니다. 하지만, 그렇게 사용한다면 AutoLayout의 각 Constraint을 코드로 직접 생성하고 관리해야 합니다. 이를 단순화하여 사용하기 위해 Visual Format Language를 고려할 수도 있으나, 이 역시 XIB + AutoLayout 조합에 비해 가독성이 떨어집니다. 즉, XIB + AutoLayout 조합으로 썼을 때의 소스코드가 훨씬 간결함을 알 수 있습니다.
  • frame 방식은 물리적으로 속도가 가장 빠른 방법입니다.
    그러나, 개발자가 수정을 원하는 뷰, 영향을 받는 뷰를 모두 알고 수정해야 합니다. 따라서 개발자의 생산성이 매우 저해되고 유지보수성이 낮다고 할 수 있습니다.

극단적으로 예를 들면, 제품의 “어떤 설계도”를 운반한다고 가정해보겠습니다.
frame으로 사용하는 메모리 로드가 서울 서초구에서 강남구를 자가용으로 움직이는 시간이라 쳤을 때, XIB 로드에 해당하는 Disk I/O는 부산에서 서울 강남구까지 오는 시간으로 비유할 수 있습니다.
분명히 전자가 후자에 비해 빠름에도 불구하고, 전자의 설계도를 독해하는 데 3일 이상 걸린다면? 후자의 설계도는 독해하는 데 3시간도 채 걸리지 않는다면?
종합적으로 후자가 더 빠른 방식이고, 개발을 진행함에 있어 더 합리적인 개발 및 유지보수 방법이라 할 수 있습니다. 극한의 성능을 끌어내야 하는 것이 아니라면!

따라서 저희 SSG의 주요 화면은 성능이 아주 중요한 화면이 아니라면(ex: 채팅) 유지보수성이 높은 XIB + AutoLayout 방식을 주로 사용해왔습니다.

실전 적용 — XIB + Autolayout

실제로 타이틀 모듈유닛을 개발하는 것을 즉석으로 라이브 코딩해보았습니다.

이제, 저희 SSG 앱에서의 사용 의의와 개선이 필요한 포인트를 정리해보겠습니다.

  • 간단한 디자인의 변경을 XIB 파일 내 AutoLayout 추가 및 수정으로 모두 처리할 수 있습니다.
    이로 인해 개발-기획-디자인 간 업무 생산성이 높아집니다. 별도의 디자인 검수 일정이 불필요하며, 개발 속도도 보장됩니다.
  • 많이 사용될 때 성능이 정말 느릴까요?
    SSG의 모듈 유닛들은 원자적 단위로 하나의 일만, 하나의 책임만 맡게 설계하는 것이 원칙입니다. 따라서 고객이 체감할 때 생각보다 좋은 퍼포먼스를 보여주고 있습니다.

두 개 이상의 파일을 열어서 수정 부분을 같이 봐야 하는 경우가 많습니다.
간단한 디자인 수정이라면 XIB만 봐도 되겠지만, 추가되는 것이 있다면 필연적으로 대응되는 swift 파일을 열어보아야 합니다. 하나의 파일만 열어서 간단하게 수정 작업을 한다면 생산성이 더 높아질 수도 있겠다는 생각이 드네요. 😅

실전 적용 — frame

이번에는 타이틀 모듈유닛을 frame 방식으로 개발하는 것을 즉석으로 라이브 코딩해보았습니다.

이번에도 코딩을 해보면서 느꼈던 점을 정리해보겠습니다.

  • 코드가 굉장히 길어져 가독성이 매우 떨어집니다.
  • 디자인 변경 시마다 코드를 분석해야 합니다.
    특히 본인이 이 코드의 원작자가 아닌 경우, 이 분석 시간은 배로 걸릴 수 있습니다.
  • 또한 하나를 수정하더라도 이에 영향을 주는 다른 뷰도 봐야 합니다.
    이 과정에서 객체의 책임을 잘 생각해야 하며, 필요에 따라 모든 구성요소가 영향을 받을 수 있고, 간단한 수정인데도 전부 수정을 해야 할 수도 있다는 점을 명심해야 합니다.
    AutoLayout은 객체 간의 의존성을 자동으로 해결해주지만, 객체 간 의존성 문제를 전부 코드로 해결해야 하는 단점이 보입니다.

그럼에도 성능 상의 이득은 분명히 있습니다.
저희 파트 내 사용 영역에서 이렇게 개발된 영역이 있는데, 채팅처럼 가변의 데이터가 대량으로 실시간으로 있고, 스크롤 시 부드럽고 빠르게 되어야 하는 상황이어서 이 방법을 채택했습니다.
즉, 성능 상 이득을 보는 대비 생산성(유지보수성)이 극도로 떨어지는 것이 문제라 할 수 있습니다. 😱

실전 적용 — SwiftUI + Preview

이번에는 본 아티클의 목적에 맞게, SwiftUI + Preview 조합이 XIB + AutoLayout 을 완벽하게 대체할 수 있는지 라이브 코딩해보았습니다.

저는 라이브 코딩을 진행하면서 아래와 같은 점들을 느꼈습니다.

  • 이전 frame 방식과 다르게 다른 요소와의 의존성이 적습니다.
  • 하나의 디자인 수정에 하나만 집중할 수 있습니다.
  • XIB를 로드하는 시간이 없어 Run Time 성능이 뛰어납니다.
    하지만, Compile Time은 훨씬 많이 소요됩니다.
  • RxCocoa 등 외부 오픈소스의 니즈가 거의 없습니다.
    (실제로 SSG 매장 내 영역에서 사용하지 않고 있어, 크게 체감되는 점은 아니긴 합니다.)
  • XIB, swift 파일이 분리되어 있지 않아 한 파일에서 작업이 가능합니다.
    하지만, 생각보다 Preview로 표시되기까지 시간이 오래 걸리는 편입니다.

완벽?까진 아니지만, 유지보수성이 높은 장점은 이어나갈 수 있다는 결론을 내릴 수 있었습니다. 또한, 저희 회사에 해당되는 점은 아니지만 Flutter, React Native로 입문한 개발자에게는 SwiftUI가 금방 익숙해질 수 있겠으나, 반대로 AutoLayout 방식에 너무 익숙한 상태라면, 이 사고의 방식을 전환하는 데 러닝 커브가 존재한다는 점을 더 생각해보아야 합니다. 😨

절망 편, UICollectionView 까지 대체할 수 있나요?

저는 단일 셀들을 SwiftUI로 전환하는 데 성공했으나, 가장 큰 난관에 봉착하고 말았습니다. 이 재사용 가능한 셀을 가져다 쓰는 곳(UICollectionView)의 영역까지 SwiftUI 가 대체 가능한지를 알아보는 것이었습니다. 이를 위해 5가지 방법을 리서치해보고 직접 적용해보았습니다.

1. ScrollView + VStack

이렇게 코딩은 할 수 있으나, 실제로 Reuse가 전혀 되지 않는다는 치명적인 단점이 있어 대체 불가능합니다. 이는 LazyVStack 으로 iOS 14를 최소 지원버전으로 감안하더라도 해결되지 않는 점입니다. 🥵

2. List + HStack

ForEach 를 이중으로 선언하여 UICollectionViewFlowLayout을 구현하는 것처럼 하나의 line이 하나의 HStack이며, Reuse도 가능합니다. (이 예제에서는 1줄이 꽉 차는 타이틀 유닛만 있으므로 ForEach 하나를 생략하였습니다.)
그러나 iOS 13, 14, 15 버전별로 separator를 없애야 하는 처리가 다르고 핏이 UITableView와 유사한 List를 그대로 쓸 수 없다는 점이 문제였습니다. 😵‍💫

3. UIViewRepresentable

기존 UICollectionView를 embed하는 방식이나, 실제 저희 SSG 앱에서는 사용이 불가능한 방법입니다.
저희는 UICollectionView의 dataSource와 delegate를 한 곳에서 처리하는 Adapter 패턴을 사용 중으로, 이 방식은 Adapter의 data만 바인딩하는 구조로, 위의 이미지처럼 특정 delegate를 구현하는 방법을 사용할 수 없습니다. 또한, 전체 껍데기를 SwiftUI만 쓴다면, 아까 했던 라이브 코딩으로 각 모듈유닛을 SwiftUI로 만드는 것이 무의미해집니다. (기존의 cell을 그대로 쓸 수 있는데, 뭐하러 힘들게 SwiftUI로 변환했을까요? 🤬)

4. UIHostingController

Cell 내 XIB를 없애고 SwiftUI로 대체하는 방법으로 껍데기만 UICollectionView를 쓰는 위의 문제도 해결되며, 이전의 디자인 패턴 문제도 깔끔하게 해결됩니다만, 여전히 UIHostingController의 view.frame 으로 cell 자신의 잘 계산된(개발자가 미리 계산해서 layout까지 불러야겠죠?) bounds 를 주고 있습니다.

그러나, 이렇게 SwiftUI를 쓰게 되면, UICollectionView가 외부의 정보를 전달해야 합니다. 그러나 Apple의 SwiftUI 설계 방향을 생각해보면 이는 적절하지 않음을 알 수 있는데, SwiftUI는 SwiftUI의 요소들로부터 외부에서 내부의 컨텐츠를 채워나가는 방향이 이상적입니다. 즉, 단독으로 SwiftUI만 사용한다면, UIKit에서 사용하는 요소 없이 스스로 자신의 크기를 결정할 수 있는 것이 가장 좋습니다.

심지어 저희 SSG에서는 성능을 위해 모듈유닛의 물리적인 사이즈를 캐싱하고 있으므로, UICollectionView가 각 item의 size를 구하도록 하는 이벤트를 불러야 합니다. 이렇게 사이즈만 구하는 곳을 따로 구현한다면, 관리의 포인트가 두 곳이 되는 것이 가장 큰 문제입니다.

디자이너가 디자인 수정을 요구했는데, 사이즈 구하는 곳도 고치고, 내부의 SwiftUI도 고쳐야 하는 불합리한 상황에 직면하게 되겠죠? 🤮

5. PreferenceKey + GeometryReader

위의 방식처럼 GeometryReader의 SizePreferenceKey를 추가하면 동적인 사이즈를 꼼수로! 제공받을 수 있습니다. 구체적인 동작 원리는 View의 background color를 강제로 세팅하여 세팅한 뷰의 크기를 Key-Value로 Observing하는 곳에 Value로 넘기는 것이죠. 따라서 위에서 상술한 사이즈를 외부에서 계산해서 주는 문제도 해결이 될 것입니다.

그런데 이렇게 넘기는 사이즈로 각 모듈유닛의 사이즈를 캐시할 수는 있겠으나, 이 역시 Apple이 권장하는 방법은 아닙니다. 결국은 Apple이 의도하는 대로 SwiftUI를 사용하는 것이 아니라 전통적인 방식으로 UIKit을 사용하는 것처럼 회귀하게 되어 SwiftUI 를 적용하는 의미가 없게 되는 것이죠. 🙄

결론

일단 북산엔딩이 나와서 죄송하다는 말을 전하며,

저희는 SwiftUI를 Apple의 의도대로 사용하는 것이 중요하다고 생각했습니다.
그러나, SSG 의 매장 내 모듈유닛은 Apple의 이상대로 SwiftUI에 맞춰줄 수는 없었습니다. 적어도 최소 지원버전이 iOS 13인 상태에서는 말이죠. 🥶

하지만 이번 WWDC22 를 보며, 다른 희망도 볼 수 있었으니..

최소 지원버전이 iOS 16이라면 UICollectionViewCell 내부에 SwiftUI 코드를 내장할 수 있습니다.

즉, SwiftUI로 미리미리 재사용 가능한 모듈화된 Custom View를 구성해놓는다면, 별도의 처리 없이 XIB + AutoLayout을 쓰는 경우 효과적인 소프트 랜딩이 가능해짐을 알 수 있죠.

이 아티클은 여기에서 끝나지만, 저는 이번 6월의 WWDC22 를 지켜보며 제 나름대로 SwiftUI 를 통해 보여주는 Apple의 숨은 메시지를 찾아냈습니다. 기회가 되면 다른 아티클에서 저의 생각(추상화, 캡슐화 등)을 전해보도록 하겠습니다.

정말 정말 끝으로!! 이 아티클이 SwiftUI 도입을 검토하는 다른 회사들에도 도움이 되는 글이기를 바라며, 저는 9월 추석 연휴의 마지막 클라이밍 🧗🏻을 하기 위해 글을 여기서 마치도록 하겠습니다. 읽어주셔서 감사합니다. 🙏🏻

--

--