Suspended — iOS 애플리케이션 라이프사이클의 일시 중단 상태로, 앱이 메모리에서 정지되지만 코드를 실행하지 않습니다. Suspended의 작동 방식, 백그라운드에서 앱 정지가 가져오는 위험, iOS가 Suspended 앱의 언로드를 관리하는 방법, 그리고 Suspended에서 돌아온 후 원활한 복구를 위한 state restoration 구현 방법을 설명합니다.
핵심 사항
Suspended는 iOS 애플리케이션 라이프사이클의 상태로, 앱이 기기의 RAM에 존재하지만 코드를 전혀 실행하지 않습니다. 이것은 완전 종료 전의 최종 상태입니다. 앱은 모든 백그라운드 작업을 완료하거나 타임아웃이 만료된 후 Background에서 Suspended로 전환됩니다. Suspended에서 애플리케이션은 완전히 정지됩니다 — 모든 스레드가 일시 중단되고, 타이머가 작동하지 않으며, 네트워크 활동이 없습니다.
Suspended는 iOS의 고유한 기능으로, 표준 Android 라이프사이클에는 없습니다. 그 이유는 프로세스 관리 아키텍처의 차이에 있습니다. iOS는 앱의 이미지를 메모리에 보존하여(데스크톱 최대 절전 모드와 유사) 사용자가 돌아올 때 콜드 스타트 없이 인터페이스를 즉시 복원할 수 있도록 합니다. Android에는 Suspended가 없습니다 — 프로세스는 존재하여 코드를 실행할 수 있거나(Background) 종료됩니다(Not Running). 다만 Android는 LMK를 통해 스레드 실행을 일시 중단할 수 있습니다.
사용자에게 Suspended는 즉시 복원처럼 보입니다. App Switcher를 통해 앱 간에 전환하면 각 앱이 중단한 위치에서 열립니다. 이는 모든 앱이 동시에 실행되고 있는 듯한 착각을 만듭니다. 실제로는 대부분이 Suspended에서 정지되어 있습니다. Suspended에서의 핫 스타트는 Not Running에서의 콜드 스타트보다 몇 배 더 빠릅니다. 코드가 이미 메모리에 로드되어 있기 때문입니다.
iOS는 모든 애플리케이션의 상태를 추적하고 사용 가능한 메모리에 따라 Suspended 앱의 언로드를 결정합니다. 메모리가 부족하면 시스템은 Suspended 앱 언로드를 시작하며, 이 상태에 가장 오래 있었던 앱부터 시작합니다. 메모리가 여전히 부족하면 시스템은 Background 및 Inactive의 앱을 Suspended로 전환한 후 언로드합니다. 이 프로세스는 사용자에게 완전히 투명합니다 — App Switcher에서 앱 아이콘을 보기만 하면 되며, 탭하면 콜드 스타트가 시작됩니다.
| 특성 | Suspended (iOS) | Background (iOS) | Background (Android) |
|---|---|---|---|
| 코드 실행 | 아니오 | 예 (제한적) | 예 (제한적) |
| 메모리 내 | 예 | 예 | 예 |
| CPU 소비 | 0% | 낮음 | 낮음 |
| 핫 스타트 | 예 — 즉시 복원 | 예 — Inactive를 통해 | 아니오 — 프로세스가 종료될 수 있음 |
| 타임아웃 | 아니오 — 메모리에 시간 동안 있을 수 있음 | ~30초 (beginBackgroundTask 후) | API 버전에 따라 다름 |
| 시스템 언로드 | 메모리 부족 시 | 메모리 심각 부족 시 | LMK (Low Memory Killer) |
| 작업 복귀 | App Switcher에서 — 즉시 | App Switcher에서 — Inactive를 통해 | 콜드 스타트 |
| State Restoration | 권장 | 필요 없음 | SavedStateHandle |
iOS에서 모든 백그라운드 작업이 완료된 후 Suspended가 자동으로 도달됩니다. 시스템이 applicationDidEnterBackground를 호출하고, beginBackgroundTask를 실행할 시간(약 30초)을 준 후, 강제로 모든 스레드를 일시 중단하고 앱을 Suspended로 전환합니다. 메모리의 객체는 보존되지만 코드는 실행되지 않습니다 — 애플리케이션은 현재 상태에서 정지됩니다.
매우 중요한 포인트: applicationDidEnterBackground는 Suspended 전에 반드시 호출되는 마지막 메서드입니다. 그 후 앱은 메모리 언로드에 대한 알림을 받지 않습니다. 사용자나 시스템이 Suspended 상태의 앱을 종료하면 applicationWillTerminate나 applicationDidEnterBackground가 다시 호출되지 않습니다. 따라서 모든 데이터 저장은 applicationDidEnterBackground에서 이루어져야 하며, applicationWillTerminate가 아닙니다.
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
// Suspended 전 마지막 보장된 호출
func applicationDidEnterBackground(_ application: UIApplication) {
// 메모리 언로드에서 살아남아야 하는 모든 것을 저장
savePersistentState()
saveNavigationStack()
// 필요하면 추가 시간 요청
let task = application.beginBackgroundTask {
application.endBackgroundTask(task)
}
}
// Suspended에서 복귀 — 핫 스타트
func applicationWillEnterForeground(_ application: UIApplication) {
// 앱이 Suspended 상태였음, 작업 재개
print("Suspended 또는 Background에서 복귀")
}
// 메모리 언로드 후 전체 복원
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Suspended 언로드 후 콜드 스타트인 경우 —
// state restoration 복원
return true
}
private func savePersistentState() {
UserDefaults.standard.set(Date(), forKey: "lastActiveDate")
}
private func saveNavigationStack() {
guard let rootVC = window?.rootViewController else { return }
// 현재 탐색 스택 저장
if let navController = rootVC as? UINavigationController {
let vcClasses = navController.viewControllers.map { type(of: $0) }
UserDefaults.standard.set(vcClasses.map { NSStringFromClass($0) }, forKey: "navStack")
}
}
}코드는 iOS에서 Suspended의 매우 중요한 처리를 보여줍니다. applicationDidEnterBackground는 마지막으로 보장된 호출입니다. 모든 데이터 저장은 여기서 이루어져야 합니다: 사용자 상태, 탐색 스택, 초안, 타이머. applicationWillEnterForeground는 Suspended 또는 Background에서 돌아올 때 호출됩니다. didFinishLaunchingWithOptions — 앱이 Suspended 후 메모리에서 언로드된 경우 콜드 스타트 시에만 호출됩니다.
Android에는 iOS Suspended의 직접적인 유사 기능이 없습니다. Android는 실행 컨텍스트를 보존하면서 앱을 메모리에서 정지하지 않습니다. 대신 Android는 프로세스를 백그라운드에 유지하거나 종료합니다. 그러나 Android 11+(API 30)에서는 App Freezer라는 메커니즘이 도입되어 SIGSTOP 신호를 사용하여 백그라운드 프로세스 실행을 일시 중단합니다. 이것은 기능적으로 Suspended와 유사하지만 중요한 차이점이 있습니다.
App Freezer는 Android 메모리 관리 시스템의 일부입니다. 앱이 활성 알림 없이 오랫동안 백그라운드에 있으면 시스템이 SIGSTOP을 보내 모든 스레드를 일시 중단합니다. 앱이 포그라운드로 돌아오면 SIGCONT가 전송되고 실행이 재개됩니다. iOS와의 주요 차이점: App Freezer는 상태 보존을 보장하지 않습니다 — 정지 중 프로세스가 종료되면 메모리의 데이터가 손실될 수 있습니다.
Android에서는 프로세스 종료 시 자동 상태 보존을 위해 ViewModel에서 SavedStateHandle 사용이 권장됩니다. SavedStateHandle은 onSaveInstanceState를 통해 Bundle에 데이터를 저장하며, App Freezer와 Process Death 모두에서 살아남습니다. iOS에서 Suspended 언로드가 예외적인 상황인 반면, Android에서 Process Death는 항상 예상되어야 하는 정상적인 동작입니다.
// SavedStateHandle — Android에서 Process Death로부터의 구원
class CheckoutViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// App Freezer 후에도 프로세스를 살아남는 상태
var currentStep: MutableLiveData<Int> =
savedStateHandle.getLiveData("checkout_step", 1)
var cartItems: MutableLiveData<List<CartItem>> =
savedStateHandle.getLiveData("cart_items", emptyList())
fun proceedToNextStep() {
currentStep.value = (currentStep.value ?: 0) + 1
}
fun addToCart(item: CartItem) {
val updatedList = (cartItems.value ?: emptyList()) + item
cartItems.value = updatedList
savedStateHandle["cart_items"] = updatedList
}
}
// App Freezer에 대비해 onStop에서 저장
class MainActivity : AppCompatActivity() {
override fun onStop() {
super.onStop()
// 정지에서 살아남아야 하는 데이터 저장
saveDraftData()
// 정지 상태에서 필요 없는 리소스 해제
releaseHeavyResources()
// 앱이 정지될 것이라고 경고
// (디버깅용 로깅)
Log.d("Lifecycle", "Activity stopped — App Freeze 가능")
}
}코드는 Android에서 Suspended 유사 기능을 처리하는 접근 방식을 보여줍니다. ViewModel의 SavedStateHandle은 Process Death 중에 데이터를 자동으로 저장하고 복원합니다. onStop은 App Freezer 또는 프로세스 종료 가능성 전의 마지막 보장된 이벤트입니다. 체크아웃 양식 상태, 장바구니 항목 목록 — 이 모든 데이터는 SavedStateHandle 덕분에 정지에서 살아남습니다. 무거운 리소스(비트맵, DB 커서)의 경우 onStop이 메모리를 해제하는 장소입니다.
State Restoration은 앱이 메모리에서 언로드된 후 UI 상태를 저장하고 복원하기 위한 iOS 내장 메커니즘입니다. 앱이 Suspended 상태였고 시스템이 언로드한 경우, 다음 콜드 스타트 시 state restoration이 탐색 스택, 스크롤 위치, 양식 상태 및 기타 UI 요소를 복원합니다. 사용자는 중단했던 동일한 화면으로 돌아갑니다.
State Restoration은 UIViewControllerRestoration 및 UIStateRestoring 프로토콜을 통해 작동합니다. 개발자는 복원하려는 각 ViewController 및 View에 restorationIdentifier를 할당합니다. 백그라운드로 이동하면 iOS가 이러한 객체의 상태를 인코딩합니다. 언로드 후 돌아오면 iOS가 새 객체를 생성하고 저장된 상태를 디코딩합니다. State restoration이 없으면 사용자는 콜드 스타트 후 중단했던 위치 대신 빈 화면을 보게 됩니다.
import UIKit
class DetailViewController: UIViewController {
var itemID: String = ""
var scrollPosition: CGPoint = .zero
override func viewDidLoad() {
super.viewDidLoad()
restorationIdentifier = "DetailViewController"
restorationClass = type(of: self)
}
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(itemID, forKey: "itemID")
coder.encode(scrollPosition, forKey: "scrollPosition")
}
override func decodeRestorableState(with coder: NSCoder) {
super.decodeRestorableState(with: coder)
if let savedID = coder.decodeObject(forKey: "itemID") as? String {
itemID = savedID
loadItem()
}
if let savedPosition = coder.decodeCGPoint(forKey: "scrollPosition") {
scrollPosition = savedPosition
// 데이터 로드 후 위치 복원
}
}
}
// AppDelegate — State Restoration 활성화
func application(
_ application: UIApplication,
shouldSaveSecureApplicationState coder: NSCoder
) -> Bool {
return true
}
func application(
_ application: UIApplication,
shouldRestoreSecureApplicationState coder: NSCoder
) -> Bool {
return true
}코드는 iOS에서 State Restoration의 구현을 보여줍니다. 복원 가능한 각 ViewController에는 restorationIdentifier와 restorationClass가 필요합니다. encodeRestorableState/decodeRestorableState는 NSCoder를 통해 데이터를 저장하고 로드합니다. AppDelegate에서 shouldSaveSecureApplicationState와 shouldRestoreSecureApplicationState는 암호화된 상태 보존을 활성화합니다. iOS 12+에서는 데이터 보호를 위해 보안 인코딩(NSSecureCoding)을 사용하는 것이 좋습니다.
첫 번째 규칙 — 앱이 Suspended에서 돌아올 것이라고 가정하지 마십시오. 시스템은 언제든지 앱을 언로드할 수 있습니다. 모든 매우 중요한 데이터는 Suspended로 전환되기 전에 영구 저장소에 저장되어야 합니다 — 즉, applicationDidEnterBackground 또는 onStop에서 저장합니다. UserDefaults, Core Data, File Manager가 적절한 저장소 옵션입니다. 메모리(변수, 속성)는 Suspended에서 살아남아야 하는 데이터에 신뢰할 수 없는 저장소입니다.
두 번째 규칙 — Suspended 전에 리소스를 해제하십시오. 파일 디스크립터를 닫고, GPU 메모리(Metal, Core Graphics)를 해제하고, 네트워크 연결을 닫습니다. 앱이 Suspended에서 CPU를 소비하지는 않지만, 점유된 리소스는 다른 애플리케이션에서 차단됩니다. iOS에서는 Suspended에서 소켓을 열린 상태로 유지할 수 없습니다 — Suspended에서 돌아올 때 작동하지 않아 오류가 발생할 수 있습니다.
세 번째 규칙 — Suspended에서 돌아올 것을 기대하는 시간 의존 로직을 배치하지 마십시오. 타이머, 콜백 및 네트워크 활동은 Suspended에서 중단됩니다. 앱이 몇 시간 동안 Suspended 상태였던 경우, 돌아올 때 타이머가 잘못 작동할 수 있습니다. 돌아올 때 데이터 유효성을 확인하십시오 — 캐시가 오래되었거나 인증 토큰이 만료되었을 수 있습니다.
네 번째 규칙 — 모든 화면, 특히 입력 양식, 스크롤 가능한 목록 및 상세 화면에 State Restoration을 사용하십시오. State restoration이 없으면 언로드된 Suspended에서 돌아온 후 사용자는 중단한 위치 대신 앱의 초기 화면을 보게 됩니다. 이는 사용자 경험을 저하시키며 사용자가 작업을 반복하도록 강제합니다.
import UIKit
// 확인: 앱이 메모리에서 언로드되었습니까?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 저장된 상태가 있는지 확인
if UserDefaults.standard.object(forKey: "navStack") != nil {
// 앱이 Suspended에서 언로드되었습니다
// 상태를 복원해야 합니다
restoreNavigationStack()
} else {
// Not Running에서 깨끗한 콜드 스타트
showOnboardingIfNeeded()
}
return true
}
private func restoreNavigationStack() {
guard let savedStack = UserDefaults.standard.array(forKey: "navStack") as? [String],
let navController = window?.rootViewController as? UINavigationController
else { return }
for vcClassName in savedStack {
if let vcClass = NSClassFromString(vcClassName) as? UIViewController.Type {
let vc = vcClass.init()
navController.pushViewController(vc, animated: false)
}
}
}코드는 앱이 Suspended에서 언로드되었는지 확인하는 방법을 보여줍니다. 저장된 탐색 스택의 존재 여부에 대한 UserDefaults 확인은 언로드 후 콜드 스타트와 깨끗한 콜드 스타트를 구분할 수 있게 합니다. 첫 번째 경우 탐색 스택이 복원되고, 두 번째 경우 온보딩 또는 메인 화면이 표시됩니다. 이 접근 방식은 NSCoder가 부족한 경우 내장 State Restoration을 보완합니다.
자주 묻는 질문
무제한입니다 — 몇 초에서 며칠까지. iOS에는 Suspended에 대한 타임아웃이 없습니다. 앱은 시스템이 리소스 부족으로 언로드하기로 결정할 때까지 메모리에 남아 있습니다. 실제로는 앱은 기기의 RAM과 활성 애플리케이션 수에 따라 15분에서 몇 시간 동안 Suspended 상태로 유지됩니다.
아니요. Suspended에서 앱을 언로드할 때 applicationWillTerminate가 호출되지 않습니다. 시스템은 앱에 알리지 않고 단순히 메모리를 해제합니다. 이것이 모든 데이터 저장이 applicationDidEnterBackground에서 이루어져야 하는 또 다른 이유입니다. applicationWillTerminate는 사용자가 App Switcher에서 앱을 스와이프하여 수동으로 종료할 때만 호출됩니다.
직접적인 유사 기능은 없습니다. Android 11+에서는 App Freezer가 도입되어 SIGSTOP을 통해 백그라운드 프로세스를 일시 중단합니다 — 이것은 기능적으로 Suspended와 유사합니다. 그러나 Android 앱은 언제든지 Process Death가 발생할 수 있다고 가정하고 설계해야 합니다. App Freezer와 Process Death 모두에서 살아남을 상태를 저장하려면 ViewModel에서 SavedStateHandle과 onSaveInstanceState를 사용하십시오.
iOS에는 확인할 직접적인 API가 없습니다. 간접적인 방법: didFinishLaunchingWithOptions에서 저장된 상태의 존재 여부에 대해 UserDefaults를 확인합니다. 상태가 있으면 — 앱이 Suspended에서 언로드되었고 콜드 스타트 중입니다. 상태가 없으면 — 깨끗한 콜드 스타트입니다. SwiftUI에서는 scenePhase.background에 플래그를 저장하고 다음 실행 시 확인할 수 있습니다.
Suspended로 전환할 때 iOS는 스냅샷 — 현재 앱 UI의 스크린샷을 찍습니다. 이 스크린샷은 App Switcher와 앱으로 돌아올 때("해동" 애니메이션으로) 표시됩니다. 앱에 기밀 데이터가 포함된 경우 스냅샷이 이를 노출할 수 있습니다. 보호를 위해 UIApplication.shouldSnapshotSecureApp(iOS 16+)을 사용하거나 applicationDidEnterBackground에서 블러 오버레이를 적용하십시오.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.