Search
🔮

SwiftUI는 UIKit를 대체할 수 있을까?

생성일
2024/12/14 04:33
태그

SwiftUI는 UIKit을 대체할 수 있을까?

3줄 요약

SwiftUI와 TCA(The Composable Architecture)의 조합은 Swift의 Concurrency를 가장 아름답게 사용할 수 있는 구조다.
선언적 UI라는 특징이 너무나 강력한 장점이 되기도 하지만 단점이 되기도 한다.
SwiftUI로만 서비스를 개발하는 것은 불가능에 가까우며 UIKit을 함께 활용할 수 있는 개발자가 되어야 한다.

 SwiftUI는 UIKit을 대체할 수 있을까?

저는 2023년 8월부터 NHN FashionGo Korea에서 SwiftUI를 이용해 서비스를 개발하고 있습니다. 기존 UIKit 프레임워크 기반의 앱에서 신규 기능은 SwiftUI로 개발하고 있으며, 기존의 UIKit 기반 화면도 SwiftUI로 점차 전환해 나가고 있습니다.
SwiftUI로의 전환은 최신 애플 기술 스택을 채택하고, 코드의 단순화와 유지보수성을 개선하기 위한 전략적 선택이었습니다. 하지만 이 과정에서 저는 SwiftUI에 대해 감탄과 좌절, 절망을 느끼며, UIKit과의 차이점에 대해서 깊이 이해하는 기회가 되었습니다.
SwiftUI를 도입하면서 직면했던 구체적인 어려움과 이를 극복하기 위해 고민했던 점들, 그리고 결국 SwiftUI가 UIKit을 대체할 수 있는지에 대한 저의 결론을 공유하고자 합니다. SwiftUI와 UIKit 사이에서 고민하는 개발자들에게 도움이 되는 글이 되길 바랍니다.

 SwiftUI의 장점

1. 빠른 생산성

SwiftUI를 이용하며 느낀 가장 큰 장점 중 하나는 빠른 생산성입니다. UIKit과 비교했을 때 코드 분량 자체가 절대적으로 감소하고, UI를 선언하는 과정이 곧 레이아웃 그 자체로 이어집니다. 이는 SwiftUI의 선언형 문법 덕분입니다. 오토레이아웃 관련 코드가 SwiftUI에서는 필요 없기 때문에 개발 시간이 크게 단축됩니다.
간단히 버튼 3개를 배치하는 코드를 SwiftUI와 UIKit으로 작성해봤습니다. SwiftUI에서는 다음과 같은 코드로 UI와 레이아웃을 동시에 정의할 수 있습니다:
struct ContentView: View { var body: some View { VStack(spacing: 16) { Button("Button 1") .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) Button("Button 2") .background(Color.green) .foregroundColor(.white) .cornerRadius(8) Button("Button 3") .background(Color.red) .foregroundColor(.white) .cornerRadius(8) } .frame(maxWidth: .infinity, maxHeight: .infinity) } }
Swift
복사
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.gray.withAlphaComponent(0.2) // 버튼 생성 let button1 = createButton(title: "Button 1", backgroundColor: .blue) let button2 = createButton(title: "Button 2", backgroundColor: .green) let button3 = createButton(title: "Button 3", backgroundColor: .red) // 버튼 스택 뷰 생성 let stackView = UIStackView(arrangedSubviews: [button1, button2, button3]) stackView.axis = .vertical stackView.spacing = 16 stackView.translatesAutoresizingMaskIntoConstraints = false // 스택 뷰 추가 view.addSubview(stackView) // 스택 뷰 레이아웃 NSLayoutConstraint.activate([ stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor), stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) } private func createButton(title: String, backgroundColor: UIColor) -> UIButton { let button = UIButton(type: .system) button.setTitle(title, for: .normal) button.backgroundColor = backgroundColor button.setTitleColor(.white, for: .normal) button.layer.cornerRadius = 8 button.translatesAutoresizingMaskIntoConstraints = false button.heightAnchor.constraint(equalToConstant: 44).isActive = true return button } }
Swift
복사

2. Swift의 Concurrency를 가장 아름답게 사용할 수 있는 구조

SwiftUI를 사용하기 전, 저는 MVVM + RxSwift 조합의 코드베이스를 선호했습니다. RxSwift는 비동기 작업을 스트림 형태로 처리하며, 이벤트의 흐름과 타이밍을 Observable 연산자로 제어합니다. 그러나 MVVM + RxSwift 조합에서 애플의 Concurrency를 도입하는 것은 적합하지 않습니다. 이미 RxSwift(Combine 포함)에서 ObserveOnSubscribeOn 연산자로 비동기 실행 컨텍스트를 제어하고 있기 때문입니다. Concurrency를 함께 사용하면 이러한 작업이 중복되어 복잡성만 증가합니다.
SwiftUI에서 가장 널리 채택되고 있는(사실상 다른 선택의 여지가 없는) 설계는 TCA(The Composable Architecture)입니다. 처음에는 애플 공식 프레임워크가 아닌 Third-party 라이브러리에 앱 설계를 의존한다는 점이 부담스러웠습니다. 하지만 TCA는 SwiftUI의 State-Driven UI와 애플 Concurrency를 가장 이상적으로 결합한 프레임워크라고 생각합니다.
TCA의 Reducer는 Task를 사용하여 다양한 비즈니스 로직을 처리합니다. 예를 들어, API 호출을 통해 데이터를 가져오거나 복잡한 비동기 작업을 처리할 때도 클로저 중첩 없이 간결하게 코드를 작성할 수 있습니다. await/async를 활용해 비동기 작업을 명확하게 표현할 수 있으며, 로직과 상태 변화가 Reducer 내에 명확히 정의되어 있어 테스트가 용이합니다.
특징
MVVM + RxSwift
SwiftUI + TCA
코드 복잡도
RxSwift 오퍼레이터로 인해 복잡도 증가
Reducer로 명확히 로직 분리
비동기 처리
flatMapLatest 등 오퍼레이터 체인이 필요
Task, await/async 활용
상태 관리
상태 변화 추적이 어려움
명시적인 State와 Action으로 관리
UI 업데이트
Observable을 View와 바인딩해야 함
State와 SwiftUI 뷰가 자동으로 동기화
reduce함수에서 Action을 처리하는 방식과 PHPhotoLibrary에서 권한을 async를 사용해 받아오는 예제
func reduce(into state: inout State, action: Action) -> Effect<Action> { switch action { case .requestPermission: return requestPhotoPermission() } } func requestPhotoPermission() -> Effect<Action> { return Effect.run { send in let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite) await send(.permissionGranted(status == .authorized || status == .limited)) } }
Swift
복사
이처럼 TCA는 비동기 작업을 Task로 명확히 정의하고, 상태 변화와 비즈니스 로직을 분리하여 Swift의 Concurrency를 가장 아름답게 사용할 수 있는 구조를 제공합니다. 클로저 중첩으로 인한 가독성 문제에서 벗어나, 선언형 UI와 함께 직관적이고 유지보수하기 쉬운 코드를 작성할 수 있었습니다.

 실무에서 겪은 SwiftUI의 한계 사례

1. UIKit도 아닌 SwiftUI도 아닌 어떤 것

SwiftUI는 UIKit과 완전히 분리된 새로운 프레임워크처럼 보이지만, 실제로는 많은 SwiftUI View가 UIKit을 래핑한 구조로 구현되어 있습니다. 예를 들어, SwiftUI의 List는 iOS 버전에 따라 내부적으로 사용되는 UIKit 컴포넌트가 달라집니다. iOS 14/15에서는 UITableView를 기반으로 동작하고, iOS 16 이상에서는 UICollectionView로 동작합니다. 이는 SwiftUI View의 동작이 iOS 버전에 따라 달라질 수 있음을 의미합니다.
실제로 이 부분이 문제가 되기도 했습니다. iOS 15에서는 UITableViewUICollectionViewsectionHeadorTopPadding 속성과 기본값이 추가되었습니다. 이로 인해 헤더를 사용하지 않는 경우에도 제거할 수 없는 빈 여백이 발생했습니다. List 마찬가지로 개발자가 설정하지 않은 의문의 여백을 마주하게됐고 이 여백을 지울 수 있는 방법을 찾아야했습니다. 하지만 SwiftUI의 ListsectionHeadorTopPadding속성을 설정할 수 있는 옵션을 제공하지 않기 때문에, 이 문제를 해결하려면 UIKit 수준으로 내려가야 했습니다.
List {} .introspect(.list, on: .iOS(.v15)) { tableView in tableView.sectionHeaderTopPadding = 0 } .introspect(.list, on: .iOS(.v16, .v17, .v18)) { collectionView in collectionView.sectionHeaderTopPadding = 0 }
Swift
복사

2. ScrollView + 동적 컨텐츠

SwiftUI는 선언형 UI의 특성상 동적으로 콘텐츠가 바뀌는 상황에 굉장이 취약합니다. 특히, 스크롤뷰 + 동적 셀 + 페이징이 필요한 시나리오에서 이러한 한계가 두드러집니다.
페이징의 한계
페이징은 부족한 데이터를 추가로 가져와 기존 데이터의 앞이나 뒤에 이어붙이는 작업입니다. UIKit의 UITableView를 사용하면 DataSource가 데이터 추가를 처리하고, 필요한 셀만 업데이트하기 때문에 스크롤 위치를 유지한 채 자연스럽게 UI가 업데이트됩니다. 예를 들어, 새 데이터를 앞에 붙이는(prev 페이징) 경우에도 현재 보고 있던 내용이 유지됩니다.
그러나 SwiftUI의 List에서는 데이터가 바인딩된 상태가 변경되면, 이는 전체 List의 다시 렌더링을 유발합니다. 이로 인해 prev 페이징의 경우 기존에 보고 있던 스크롤 위치가 변경되거나 초기화되는 현상이 발생합니다. 이 현상은 UX에 굉장히 치명적입니다.
문제 사례
UIKit에서의 이전 컨텐츠 페이징
SwiftUI에서의 이전 컨텐츠 페이징
사용자가 스크롤을 위로 올리면서 페이징이 발생했을 때 데이터가 91 다음 90으로 자연스럽게 이어지는게 아닌 91 다음 81이 보이는 굉장히 어색한 현상이 발생합니다.
ScrollViewReader { proxy in ScrollView { LazyVStack { ForEach(items, id: \.self) { item in Text("Item \(item)") .padding() .id(item) } } } .onChange(of: items) { _ in if let lastViewedItem = lastViewedItemId { proxy.scrollTo(lastViewedItem, anchor: .top) } } }
Swift
복사
위 현상을 제거하기 위해 ScrollReader를 이용해 각 셀의 id를 추적하고, 신규 데이터 추가 직후에 scrollTo함수를 통해 마지막으로 보고 있던 셀의 id로 이동하면 그나마 비슷하게 동작을 구현할 수 있으나 이 부분마저 자연스럽지 못하게 동작하며 iOS17에서는 동작하지만 iOS18에서는 동작하지도 않습니다.

3. SwiftUI의 버그

1.
setNavigationHidden
SwiftUI 자체 버그도 많습니다. 한가지 예시로 SwiftUI에서는 View의 modifier로 setNavigationHidden()이라는 함수를 제공합니다. 이 함수를 통해 NavigationBar를 숨길 수 있습니다.
iOS15에서만 간헐적으로 NavigationBar가 숨겨지지 않는 이슈가 있었습니다. 이 문제를 해결하기 위해 HostingViewController 수준의 lifeCycle에서 추가 작업을 진행했던 기억이 있습니다.
2.
CollectionView
iOS17에서는 List에서 Column 갯수가 2인 리스트를 표시할때 컨텐츠가 1개인 경우(우측이 비어있는 경우) 스크롤이 혼자 통통 튀는 이슈가 발생했습니다. iOS17에서만 컨텐츠가 1개인 경우 1개의 보이지 않는 더미Cell을 추가하는 방식으로 해결해야만 했습니다.
3.
UITableView, UICollectionView의 Reorder 기능
UIKit의 UITableView, UICollectionView는 EditMode에서 컨텐츠의 순서 변경 기능을 제공합니다. SwiftUI에서도 Edit모드, 컨텐츠 순서 재정렬 기능을 함수로 제공하고 있지만 iOS버전마다 다른 버그가 발생했습니다.
데이터 변경은 반영되나 UI에서는 바뀌지 않는 문제
가장 마지막 아이템은 드래그 되지 않는 문제
일부 셀에만 reorder 기능을 막아도 reorder 동작하는 문제
iOS버전마다 다 다른버그들이 나타나 절망에 빠지기도 했습니다.
4.
그 외 온갖 버전을 타는 이슈들
위에서 작성한 3가지 이슈들 외에도 정말 다양한 이슈에 부딪혔습니다. 가장 어려웠던건 iOS버전마다 나타나는 버그가 달랐기 때문에 QA과정에서 발견도 힘들었고, 발견한다해도 납득할만한 해결책을 찾기도 어려웠습니다.

 SwiftUI로만 개발 할 수 없는 이유

1. SwiftUI로 제공되지 않는 기능

아직 개발을 안한건지 못한건지, 아니면 의도한 것인지는 알 수 없으나 아직 WKWebView(WebKit), Photo 라이브러리는 SwiftUI에서 사용할 수 없습니다.
WebView와 Photo 특성상 UI와 사용자의 상호작용이 복잡해서 선언적UI 보다 명령형 프로그래밍이 더 적합하다고 생각이 들긴 합니다.

2. 계속 추가/변경되는 SwiftUI View (최소지원버전 이슈)

SwiftUI는 아직 완전하지 않습니다. 나온지 N년이 지났지만 새로운 기능들이 계속 추가되고 있습니다. 사실 왜 그 기능들이 아직까지 없었는지 의문이 듭니다. Pull To Refresh가 iOS15 부터 등장했다는게 충격이죠.
또한 iOS앱에서 핵심적인 Navigation 로직도 iOS16 부터 사용할 수 있습니다.
단순 소소한 기능이 아닌 서비스 앱 개발의 핵심이 될 수 있는 부분에 대한 변경사항도 아직 활발히 일어나고 있습니다. 당연히 신규 추가되는 기능들의 최소지원 버전은 최신 iOS버전으로 설정됩니다.
우리는 보통 이미 서비스를 라이브하고 있기 때문에 기존 사용자들을 버릴 수 없습니다. 따라서 신규 기능들을 지켜봐야만 하는 상황에 있습니다.

 SwiftUI + UIKit을 함께 사용한다는 것

처음부터 SwiftUI를 활용해 신규 프로젝트를 진행하는 것이 아닌 이상, 보통 UIKit프레임워크의 서비스에서 SwiftUI로 전환을 시도하는게 일반적일 것이라고 생각됩니다.
애플에서도 이 과정을 지원하기 위해 UIViewRepresentable, UIViewControllerRepresentable, HostingViewController를 제공합니다.
UIViewRepresentable, UIViewControllerRepresentable를 이용해 UIKit의 UIView, UIViewController를 SwiftUI에서 사용할 수 있는 View로 래핑해주며, HostingViewController를 통해서는 SwiftUI의 View를 UIKit 환경에서 사용할 수 있도록 해줍니다. 가능하다고 했지 완벽하다곤… 못합니다.
상태 관리의 차이
SwiftUI는 상태 기반 업데이트를 사용하는 반면, UIKit은 명령형 업데이트 방식을 사용합니다. 이로 인해 두 프레임워크 간 데이터 동기화가 어려울 수 있습니다.
뷰 업데이트 타이밍 차이
UIKit과 SwiftUI는 화면을 업데이트하는 시점이 다를 수 있어 의도치 않은 버그가 발생할 수 있습니다.
추가 복잡성
두 프레임워크를 병행하면 코드의 복잡성이 증가하고, 유지보수가 어려워질 수 있습니다.

 Objective-C처럼 UIKit도 점점 사라지게 되지 않을까?

애플이 앞으로 어떤 결정을 내릴지 정확히 예측할 수는 없지만, 개인적으로는 UIKit이 앞으로도 오랜 기간 살아남을 것이라고 생각합니다. SwiftUI는 많은 가능성을 열어준 프레임워크이지만, UIKit처럼 안정성과 유연성을 갖추기에는 아직 부족한 점이 많기 때문입니다.

1. 과거의 사례: Storyboard vs 코드 기반 UI

SwiftUI와 UIKit의 관계는 과거 Storyboard와 코드 기반 UI의 관계와 비슷하다고 생각합니다. Storyboard가 처음 도입되었을 때 많은 개발자들이 더 이상 코드를 사용해 UI를 구성하지 않아도 될 것이라 기대했지만, 오늘날 대부분의 앱 개발은 여전히 코드 기반 UI에 의존하고 있습니다.
Storyboard가 점차 사라지게 된 이유는 다음과 같습니다.
대규모 프로젝트에서 협업과 버전 관리가 굉장히 어려웠습니다.
복잡한 UI를 구성하거나 동적으로 변하는 화면을 구현하기에 제약이 있었습니다.
SwiftUI 역시 선언형 UI로 많은 혁신을 가져왔지만, UIKit의 검증된 안정성과 유연성을 완전히 대체하기에는 부족한 점이 많습니다.

2. SwiftUI가 UIKit을 완전히 대체하지 못하는 이유

SwiftUI가 UIKit을 완전히 대체하기 어려운 이유는 다음과 같습니다:
안정성 부족
SwiftUI는 아직 비교적 젊은 프레임워크로, 실무에서 많은 버그와 iOS 버전별 비일관성을 보여주고 있습니다.
특히, 기존 UIKit 기반 앱에 SwiftUI를 도입하려면 두 프레임워크를 함께 사용해야 하는 추가 복잡성이 발생합니다.
검증된 유연성 부족
UIKit은 10년 이상 검증된 프레임워크로, 거의 모든 UI 요구사항을 충족할 수 있는 강력한 유연성을 제공합니다.
반면, SwiftUI는 일부 고급 기능(WebView, Photo 라이브러리 등)을 지원하지 않으며, 실무에서 요구되는 모든 UI 시나리오를 처리하기에는 아직 부족한 점이 많습니다.
개발 생태계의 변화 속도
SwiftUI는 매년 새로운 기능이 추가되고 기존 API가 변경됩니다. 이는 장기적으로는 긍정적인 변화지만, 실무에서는 최소 지원 버전 문제를 야기하며 SwiftUI만으로 프로젝트를 진행하기 어렵게 만듭니다.

 프로젝트에 SwiftUI를 사용하고 싶다면 고려해야 할 점

기획 및 디자인 유연성
SwiftUI는 제약사항이 존재합니다. 일부 고급 UI 동작(예: 사용자 정의 레이아웃, 복잡한 애니메이션 등)은 UIKit에 비해 구현이 어렵거나 불가능할 수 있습니다. 따라서 SwiftUI를 사용하는 프로젝트라면 기획과 디자인이 SwiftUI에서 구현 가능한 형태로 조정될 수 있어야 합니다.
디자인 시스템 보유 여부
UIKit 기반의 디자인 시스템을 보유한 경우, SwiftUI로의 전환 과정에서 다음과 같은 선택지가 있습니다:
1.
SwiftUI용 디자인 시스템 재구현:
SwiftUI 전용으로 설계된 디자인 시스템은 SwiftUI의 선언형 패러다임을 온전히 활용할 수 있습니다.
하지만, 재구현 비용이 발생하고 초기 전환 작업이 복잡합니다.
2.
UIViewRepresentable로 UIKit 디자인 시스템 재활용:
UIKit의 기존 디자인 시스템을 유지하며 SwiftUI에서 활용할 수 있습니다.
초기 비용을 절감할 수 있지만, SwiftUI의 선언형 패러다임과 잘 맞지 않아 코드가 복잡해질 수 있습니다.
신규 디자인 시스템을 구현하는 경우
1.
UIKit으로 구현: UIKit 기반 시스템은 SwiftUI와 UIKit 모두에서 사용 가능하지만, SwiftUI 전환의 의미가 퇴색됩니다.
2.
SwiftUI로 구현: SwiftUI에서만 사용 가능하며, 추후 UIKit에서 재구현해야 하는 경우 추가 리소스가 필요합니다.
유지보수 용이성의 중요도
다시 한번 언급하지만 SwiftUI는 완벽하지 않습니다. 버전마다 다른 동작. 소소한 버그. 어쩌면 우리는 핫픽스를 여러번 내보내야하는 경우도 생길겁니다.
특히 Introspect를 활용해 UIKit 수준의 커스텀을 진행하는 경우 매년 새로운 iOS버전에 대응해줘야 하는 수고로움도 생깁니다.
SwiftUI를 적용하는 프로젝트가 매일 유지보수/개발하는 서비스라면 괜찮지만 일회성으로 개발하고 운영만하는 서비스라면 추천하지 않습니다.
“끝”