SwiftUI Study – scenePhase와 앱 생명주기를 활용한 자동 저장·타이머·네트워크 작업 관리 패턴
Dev Study/SwiftUI 2025. 12. 9. 11:27반응형
SwiftUI Study – scenePhase와 앱 생명주기를 활용한 자동 저장·타이머·네트워크 작업 관리 패턴
1. 왜 중요한가 (문제 배경)
SwiftUI 앱은 UIKit처럼 AppDelegate / SceneDelegate 중심이 아니라
@main App + @Environment(\.scenePhase) 조합으로 생명주기(lifecycle) 를 관리합니다.
하지만 실무에서 다음과 같은 문제가 자주 발생합니다.
- 사용자가 홈 버튼을 눌러 나갔을 때 입력 중이던 내용이 사라짐
- 백그라운드로 진입했는데 타이머·Task·네트워크가 그대로 돌아감
- 다시 돌아왔을 때 화면 상태가 오래된 값으로 남아 있음
- 푸시/딥링크로 진입했을 때 상태 동기화가 어색함
이런 문제의 공통점은:
“앱의 생명주기 변화에 맞춰
상태 저장·정리·재로딩을 설계하지 않았다.”
SwiftUI에서 scenePhase 를 올바르게 활용하면:
- 자동 저장 / 자동 복원
- 타이머·Task 중지 및 재개
- 백그라운드 진입 시 리소스 정리
- 포그라운드 복귀 시 최신 데이터 동기화
같은 패턴을 깔끔하게 구현할 수 있다.
2. 잘못된 패턴 예시
❌ 예시 1: scenePhase를 사용하지 않고 항상 동작하는 타이머
final class TimerViewModel: ObservableObject {
@Published var count = 0
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.count += 1 // ❌ 앱이 백그라운드에서도 계속 증가
}
}
}
문제점
- 사용자가 홈으로 나가도 타이머는 계속 돈다
- 배터리 낭비 + 불필요한 연산
- 다시 돌아왔을 때 count 값이 크게 튀어 있음
❌ 예시 2: 입력 폼을 scenePhase와 분리해서 설계
struct MemoView: View {
@State private var text: String = "" // ❌ 앱 종료 시 유실
var body: some View {
TextEditor(text: $text)
}
}
문제점
- 앱을 잠시 벗어났다 돌아오면 입력 내용이 사라질 수 있음
- 자동 저장 타이밍이 없기 때문에 유실 위험 존재
❌ 예시 3: 포그라운드 복귀 시 데이터 동기화 없음
struct HomeView: View {
@State private var items: [Item] = []
var body: some View {
List(items) { Text($0.title) }
.task {
items = await api.loadItems() // ❌ 앱 실행 시 한 번만
}
}
}
문제점
- 앱을 켜둔 상태에서 시간이 오래 지나도 UI는 오래된 데이터 유지
- 사용자가 다시 앱으로 돌아왔을 때, 최신 데이터가 아니라 “오래된 화면”을 보게 됨
3. 올바른 패턴 예시
✅ 예시 1: scenePhase로 타이머/Task를 제어
@main
struct MyApp: App {
@Environment(\.scenePhase) private var scenePhase
@StateObject private var timerVM = TimerViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(timerVM)
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
timerVM.start() // 포그라운드에서만 동작
case .background, .inactive:
timerVM.stop() // 백그라운드/비활성화 시 중지
@unknown default:
break
}
}
}
}
ViewModel:
final class TimerViewModel: ObservableObject {
@Published var count = 0
private var timer: Timer?
func start() {
guard timer == nil else { return }
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.count += 1
}
}
func stop() {
timer?.invalidate()
timer = nil
}
}
장점
- 앱 상태에 따라 타이머가 정확히 멈추고 재개
- 불필요한 백그라운드 연산 제거
✅ 예시 2: scenePhase와 AppStorage를 조합한 자동 임시 저장
struct MemoView: View {
@AppStorage("draft_memo") private var draft: String = ""
@Environment(\.scenePhase) private var scenePhase
var body: some View {
TextEditor(text: $draft)
.padding()
.onChange(of: scenePhase) { phase in
if phase == .background {
saveDraft()
}
}
}
private func saveDraft() {
// @AppStorage가 이미 저장을 담당하므로
// 별도 로직이 없다면 이 함수는 로깅/추가 처리 용도
print("Draft saved:", draft)
}
}
장점
- 사용자가 앱을 벗어나도 메모 내용이 유지
- 다시 들어오면 마지막 상태 그대로 복원
- “자동 임시 저장” 패턴을 매우 쉽게 구현
✅ 예시 3: 포그라운드 복귀 시 데이터 리프레시
struct HomeView: View {
@Environment(\.scenePhase) private var scenePhase
@State private var items: [Item] = []
var body: some View {
List(items) { Text($0.title) }
.task {
await reload()
}
.onChange(of: scenePhase) { phase in
if phase == .active {
Task { await reload() }
}
}
}
func reload() async {
items = (try? await api.loadItems()) ?? []
}
}
장점
- 앱 처음 실행 + 포그라운드 복귀 시 항상 최신 데이터로 유지
- 사용자가 “오래된 화면”을 보는 현상 최소화
✅ 예시 4: scenePhase를 ViewModel로 전달해 비즈니스 로직에서 처리
@MainActor
final class SessionManager: ObservableObject {
func appDidEnterBackground() {
// 세션 저장, 토큰 갱신, 로그 플러시 등
}
func appDidBecomeActive() {
// 필요 시 세션 복구, 토큰 상태 확인 등
}
}
App:
@main
struct MyApp: App {
@Environment(\.scenePhase) private var scenePhase
@StateObject private var sessionManager = SessionManager()
var body: some Scene {
WindowGroup {
RootView()
.environmentObject(sessionManager)
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
sessionManager.appDidBecomeActive()
case .background:
sessionManager.appDidEnterBackground()
case .inactive:
break
@unknown default:
break
}
}
}
}
장점
- 생명주기 이벤트를 View가 아니라 도메인/세션 레이어에서 처리
- 향후 UIKit/다른 플랫폼으로 확장할 때도 로직 재사용이 쉬움
4. 실전 적용 팁
✔ 팁 1 – 타이머·Task·스트림은 scenePhase와 함께 설계
.active 에서만 동작하고, .background / .inactive에서는 반드시 멈추게 만드는 습관.
✔ 팁 2 – 입력 폼에는 자동 임시 저장 전략을 넣는 것이 좋다
@AppStorage 또는 로컬 DB를 활용해 “사용자 입력 유실”을 막기.
✔ 팁 3 – 포그라운드 복귀 시 가벼운 리프레시를 수행
전체 리셋이 아닌, 변경 가능성이 있는 데이터만 갱신.
✔ 팁 4 – scenePhase를 그대로 View에다 흩뿌리지 말고,
가능하면 SessionManager / AppCoordinator 같은 전담 객체로 모으기.
✔ 팁 5 – 실제 디바이스에서 백그라운드 전환/복귀 패턴을 반복 테스트
시뮬레이터 + 실제 기기 양쪽에서 동작을 꼭 확인해야 한다.
5. 정리
- SwiftUI의 scenePhase는 앱 생명주기를 표현하는 핵심 도구이며,
타이머·비동기 Task·입력 폼·세션 관리와 반드시 연결되어야 한다. - 백그라운드 진입 시 “멈춰야 할 것(타이머/폴링/스트림)”과
“저장해야 할 것(입력 값/세션/로그)”을 명확히 구분해 처리해야 한다. - 포그라운드 복귀 시 “최신 상태 동기화” 전략까지 포함하면
SwiftUI 앱의 안정성과 사용자 경험이 한 단계 올라간다.
반응형

