LODY/정리

iOS 개발 필수 기본기 / 앱은 어디서 시작하는가

앱은 어디서 시작하는가

🟣 실행 버튼을 눌렀을 때 무슨 일이 일어나나

C 프로그램에는 main 함수가 있고 거기서 실행이 시작된다는 걸 학부에서 배웠다. 그런데 처음 만든 iOS 프로젝트를 아무리 뒤져도 main이라 부를 만한 곳이 없었다. AppDelegate가 있고 SceneDelegate가 있는데, 둘 중 무엇이 먼저 도는지, 애초에 이 코드를 누가 부르는지가 불투명했다.

결론부터 말하면 iOS 앱에도 시작점은 있다. 다만 그 시작점을 우리가 직접 쓰지 않고 프레임워크가 대신 만들어 준다. 이 장은 실행 버튼을 누른 뒤 첫 화면이 뜨기까지의 경로를 따라간다. 이 경로를 알면 "앱이 켜질 때 딱 한 번 하고 싶은 일"을 어디에 둬야 하는지, 화면 초기화 코드를 왜 특정 위치에 써야 하는지가 자연스럽게 풀린다.


🟣 숨어 있는 main과 @main

UIKit 앱의 진짜 시작점은 UIApplicationMain이라는 함수다. 이 함수가 UIApplication 객체를 만들고, 이벤트 루프를 돌리고, 앱을 살아 있게 유지한다. 우리가 이 함수를 직접 부르지 않는 이유는 예전 프로젝트에 있던 main.swift가 사라졌기 때문이다.

Objective-C 시절이나 초기 Swift 프로젝트에는 AppDelegate 위에 @UIApplicationMain이라는 표시가 붙어 있었다. 이 표시가 컴파일러에게 "이 클래스를 델리게이트로 삼아 숨은 main을 만들어 달라"고 알리는 역할을 했다. Swift 5.3부터는 더 일반적인 @main으로 바뀌었다. @main은 특정 타입을 프로그램 진입점으로 지정하는 언어 차원의 표시이고, UIApplicationController가 그 규약에 맞는 main 정적 메서드를 제공한다.

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    // 컴파일러가 이 클래스로 숨은 main을 만들어 준다
}

둘의 차이는 대개 신경 쓸 일이 없지만, 시작점을 손수 제어하고 싶을 때 갈린다. 예를 들어 앱이 뜨기 전에 특정 설정을 먼저 하고 싶다면 @main을 지우고 main.swift를 직접 만들어 UIApplicationMain(...)을 호출하면 된다. 평소엔 프레임워크에 맡기고, 필요할 때만 열어 볼 수 있는 문이 있다는 정도로 알아 두면 충분하다.

@main(숨은 main)UIApplication(이벤트 루프)AppDelegate(앱 생명주기)SceneDelegate(화면 생명주기)UIWindowrootViewController객체 생성앱 시작 알림화면 연결 위임창 구성

🟣 AppDelegate와 SceneDelegate의 분업

iOS 13에서 SceneDelegate가 등장하면서 앱 시작 코드가 두 파일로 나뉘었다. 처음엔 왜 굳이 쪼갰는지 이해가 안 갔는데, 아이패드에서 같은 앱을 창 두 개로 띄우는 상황을 떠올리면 바로 납득이 된다.

AppDelegate앱 전체에 딱 하나뿐인 관심사를 맡는다. 앱이 처음 실행됐을 때 한 번 하는 초기화, 푸시 알림 등록, 백그라운드 진입 같은 프로세스 단위의 일이다. 반면 SceneDelegate화면(scene) 하나하나의 생명주기를 맡는다. 창을 만들고 첫 화면을 붙이고, 그 창이 앞으로 나오거나 뒤로 숨는 순간을 다룬다. 아이패드에서 창을 두 개 열면 AppDelegate는 여전히 하나지만 SceneDelegate는 창마다 따로 생긴다.

이 분업을 알고 나면 초기화 코드를 어디 둘지가 명확해진다. 로그 수집 도구를 켜거나 데이터베이스를 준비하는 것처럼 앱에 한 번만 하면 되는 일은 AppDelegatedidFinishLaunching에 둔다. 첫 화면을 무엇으로 할지 정하는 것은 창에 딸린 일이므로 SceneDelegate에 둔다.

🚀 스토리보드 없이 첫 화면 붙이기

Xcode 새 프로젝트는 Main.storyboard를 첫 화면으로 물려 준다. 스토리보드는 화면을 눈으로 보며 배치할 수 있어 편하지만, 팀으로 일할 때 충돌이 잦고 XML이라 코드 리뷰가 어렵다. 그래서 스토리보드를 걷어 내고 코드로 첫 화면을 붙이는 방식을 익혀 두면 두고두고 쓸모가 있다.

스토리보드가 자동으로 해 주던 일은 사실 몇 줄이다. 창을 만들고, 그 창의 최상위 화면을 지정하고, 창을 화면에 띄우는 것이다. 이 세 가지를 SceneDelegate에서 손으로 하면 된다.

func scene(_ scene: UIScene,
           willConnectTo session: UISceneSession,
           options: UIScene.ConnectionOptions) {
    guard let windowScene = scene as? UIWindowScene else { return }
    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = HomeViewController()
    self.window = window
    window.makeKeyAndVisible()
}

마지막으로 프로젝트 설정에서 스토리보드 이름을 지우고, Info.plist의 씬 구성에서도 스토리보드 참조를 없앤다. 이 과정을 직접 해 보면 앞에서 본 진입 경로가 손에 잡힌다. rootViewController를 바꾸는 한 줄이 곧 앱의 첫 화면을 바꾸는 스위치라는 게 눈에 들어온다.


🟣 정리

  • iOS 앱에도 시작점은 있다. @main이 지정한 델리게이트로 컴파일러가 숨은 main을 만들고, 그 안에서 UIApplicationMain이 앱을 돌린다.
  • @UIApplicationMain은 옛 표기이고 지금은 언어 차원의 @main을 쓴다. 평소엔 신경 쓸 일이 없지만, 시작점을 직접 제어하려면 이 문을 열 수 있다.
  • AppDelegate는 앱 전체에 하나뿐인 일을, SceneDelegate는 창마다의 화면 생명주기를 맡는다. 초기화 코드를 어디 둘지는 이 분업으로 갈린다.
  • 스토리보드 없이 시작하려면 SceneDelegate에서 창을 만들고 rootViewController를 지정한 뒤 makeKeyAndVisible을 부르면 된다.
  • 면접에서 "앱이 실행되면 어떤 순서로 코드가 도는가", "AppDelegateSceneDelegate의 차이"는 단골 질문이다. 이 진입 경로를 그림으로 그릴 수 있으면 대부분 이어서 답이 나온다.

시작점을 잡고 첫 화면을 띄웠으면, 그다음은 그 화면 안을 채우는 코드를 어떻게 나눌지의 문제다. 화면 하나에 모든 코드를 몰아넣다 보면 곧 손댈 수 없이 커진다. 다음 장은 그 코드를 나누는 방법, 곧 아키텍처 패턴을 다룬다.