UIKit ↔ SwiftUI 혼용 시 Life-Cycle 이해하기
UIKit ↔ SwiftUI 혼용 시 Life-Cycle 이해하기
1. 들어가며
iOS 개발에서는 UIKit과 SwiftUI를 함께 사용하는 “혼합 아키텍처”가 빠르게 표준이 되고 있습니다.
특히 실무에서는 화면 대부분은 UIKit으로 이루어져 있지만 특정 화면만 SwiftUI로 교체하거나, 반대로 SwiftUI 프로젝트 내에서 특정 커스텀 기능을 UIKit으로 구현하는 경우가 많습니다.
그러나 두 프레임워크는 렌더링 방식, 라이프사이클, 이벤트 전파 방식이 완전히 다릅니다.
그 차이를 이해하지 않으면 다음과 같은 문제가 발생합니다.
- 화면 전환 시 SwiftUI View가 사라지지 않음
- onAppear/onDisappear가 예상과 다르게 여러 번 호출
- UIHostingController가 재할당되며 메모리 증가
- 키보드 포커스 시 전체 뷰가 튐
- Custom transition 시 제스처 충돌
- NavigationBar, SafeArea, inset이 깨짐
이 챕터에서는 UIKit ↔ SwiftUI 혼용 시 반드시 알아야 할 모든 실전 개념을 정리했습니다.
2. UIKit 라이프사이클 복습
UIKit은 명확하고 전통적인 라이프사이클을 가집니다.
viewDidLoad()
viewWillAppear()
viewDidAppear()
viewWillLayoutSubviews()
viewDidLayoutSubviews()
viewWillDisappear()
viewDidDisappear()
deinit
특징
- 한 번 로드되면 메모리가 유지되는 경우가 많음
- 화면 전환 시 생명주기가 정확함
- 개발자 입장에서 예측하기 매우 쉬움
3. SwiftUI 라이프사이클 구조 이해
SwiftUI의 핵심은 “상태 기반 렌더링”입니다.
View의 구조적 특징
- struct이며 값 타입
- 상태 변화마다 body가 재호출
- View는 재생성되는 것이 기본 행동
- onAppear/onDisappear는 렌더링 이벤트
중요한 개념
SwiftUI의 View는 화면의 실체가 아니고, 그리는 규칙(설명서)에 가깝습니다.
즉, “View는 필요할 때 그려지며, 사라질 필요가 없으면 재사용되지 않는다.”
4. UIKit ↔ SwiftUI 혼합 시 발생하는 대표 현상
4.1 onAppear가 여러 번 호출된다
SwiftUI View는 body 재렌더링만 되어도 onAppear가 호출되는 경우가 있습니다.
4.2 NavigationView 내부 push/present 시 라이프사이클이 꼬임
UIKit NavigationController와 SwiftUI NavigationStack의 구조적 차이 때문입니다.
4.3 Sheet와 fullScreenCover가 UIKit present와 동작 방식이 다름
4.4 HostingController가 예상치 못하게 재생성
특히 아래 상황에서 발생합니다.
- State 변화
- Parent View 업데이트
- UIHostingController 내부에서 뷰가 바뀌는 경우
5. UIHostingController의 동작 구조
UIKit에 SwiftUI View를 넣을 때는 UIHostingController를 사용합니다.
let vc = UIHostingController(rootView: MySwiftUIView())
present(vc, animated: true)
하지만 많은 개발자가 놓치는 점이 있습니다.
UIHostingController는 View를 “보관하지 않는다”
rootView는 상태 기반으로 렌더링되며 필요한 시점에 다시 만들어집니다.
- rootView는 매번 새로운 struct
- 실제 렌더링된 UIView 계층만 UIKit이 관리
HostingController의 중요한 메서드
override func viewDidLayoutSubviews()
override func viewDidAppear(_ animated: Bool)
override func viewDidDisappear(_ animated: Bool)
SwiftUI의 body가 재호출되더라도 UIKit의 life-cycle은 유지됩니다.
6. 실무에서 흔히 발생하는 문제 & 해결법
6.1 문제: SwiftUI View가 계속 새로 그려져서 상태가 초기화됨
원인
- View struct는 값 타입이라 상태를 저장하지 않음
- @State 또는 ObservableObject가 외부에서 관리되지 않음
해결
View에 종속되지 않는 StateObject/ObservedObject/EnvironmentObject로 상태 유지
struct MyView: View {
@StateObject var viewModel = MyViewModel()
}
또는 UIKit에서 생성 후 주입
UIHostingController(rootView: MyView(viewModel: injectedVM))
6.2 문제: 키보드 포커스 시 전체 화면이 튀거나 밀림
원인
- UIKit Keyboard Notification
- SwiftUI safe area 동작 차이
- GeometryReader의 frame 계산 문제
해결
UIKit에서 KeyboardAvoidance를 비활성화
hostingController.view.insetsLayoutMarginsFromSafeArea = false
또는 ScrollView에서 keyboard avoidance를 직접 처리
(iOS 15+에서 ScrollDismissesKeyboard 옵션 사용)
6.3 문제: NavigationBar 숨김 처리 안됨
SwiftUI:
.navigationBarHidden(true)
UIKit:
navigationController?.setNavigationBarHidden(true, animated: false)
혼합할 경우 SwiftUI의 navigation bar 설정이 UIKit 설정을 “역전”시키는 상황 발생.
해결
UIKit의 NavigationController에서 우선값을 강제 적용
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: false)
}
6.4 문제: Custom transition 시 제스처 충돌
SwiftUI의 swipeDismissGesture와 UIKit의 interactiveTransitionGesture가 중복되는 것이 원인.
해결
HostingController 기반으로 transition delegate 지정 후,
SwiftUI의 gesture를 disable 처리.
.hostingController
.environment(\.dismissDisabled, true)
7. 실무에서 가장 좋은 혼합 아키텍처 패턴
패턴 1 — UIKit main, SwiftUI 화면 부분 교체
실무에서 가장 많이 쓰는 패턴.
- 기존 UIKit 프로젝트 유지
- 특정 화면만 SwiftUI로 전환
- 점진적 전환 가능
패턴 2 — SwiftUI main, 커스텀 기능만 UIKit
- SwiftUI로 앱 구성
- 지도, 카메라, 영상, 애니메이션 등 특정 부분만 UIKit으로 구현
- UIViewRepresentable 기반
패턴 3 — MVVM + Clean Architecture 혼합
- View는 SwiftUI
- ViewModel은 ObservableObject
- UseCase/Repository는 UIKit 프로젝트 동일 구조로 유지
8. SwiftUI ↔ UIKit 연결 시 가장 안정적인 코드 패턴
SwiftUI → UIKit
struct MyView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> SomeViewController {
return SomeViewController()
}
func updateUIViewController(_ vc: SomeViewController, context: Context) { }
}
UIKit → SwiftUI
let swiftUIView = SomeSwiftUIView()
let hosting = UIHostingController(rootView: swiftUIView)
navigationController?.pushViewController(hosting, animated: true)
9. 체크리스트
- onAppear는 life-cycle이 아니라 “렌더링 이벤트”다
- UIHostingController는 rootView를 보관하지 않는다
- SwiftUI의 body는 ‘렌더링 규칙’, 실체가 아니다
- 상태(StateObject)가 없으면 SwiftUI는 초기화된다
- NavigationBar/SafeArea는 UIKit 우선순위가 높다
- 커스텀 프레젠테이션은 UIKit에서 처리하면 안정적
- Keyboard Avoidance는 UIKit 규칙을 따르는 것이 좋다
10. 결론
SwiftUI와 UIKit 혼용은 점점 더 일반화되고 있으며, 실무에서 반드시 필요한 스킬입니다.
두 프레임워크의 본질적인 동작 차이를 이해하면 복잡한 UI에서도 안정성과 유지보수성이 크게 향상됩니다.