앱델리게이트 이해하기 (iOS앱 만들기 – 01)

iOS앱이 시작되는 과정

iOS앱은 Objective-C로 만들어지고, Objective-C는 C언어가 확장된 형태이며, 기본적인 구조는 C와 동일하다.  따라서 iOS앱 역시 예외없이 main 함수가 프로그램 전체의 본체가 되며, iOS 앱 프로젝트의 메인이 되는 파일인 main.m에 이 함수가 정의되어 있다. 다만, 99%의 iOS앱 프로젝트가 앱의 론칭 방식까지 변경할 이유는 없기 때문에 우리는 보통 앱이 실행되고 나서 커스텀 코드를 만나게 되는 첫 지점인 앱 델리게이트 파일부터 편집하게 된다. 하지만 프로젝트 네비게이터를 잘 뒤져보면 main.m 파일이 있을 것이다.

iOS앱은 사실 UIApplication이라는 클래스의 객체이다. 프로젝트의 main 함수는 기본적으로 UIApplication 클래스의 인스턴스를 만들어서 GUI를 사용하기 위한 런루프를 돌려주는 작업을 수행한다. 그리고 그 이후에 앱 내에서 일어나는 모든 처리는 UIApplication 객체가 관리1하게 된다.

  1. main 함수가 실행된다.
  2. main 함수는 다시 UIApplicationMain 함수를 호출한다.
  3. 이 함수는 앱의 본체에 해당하는 객체인 UIApplication 객체를 생성한다.
  4. Info.plist 파일을 읽어들여 파일에 기록된 정보를 참고하여 그외에 필요한 데이터를 로드한다.
  5. 메인 nib 파일을 사용하는 경우 4.의 과정에서 로드된다.
  6. 메인 nib 파일이 없거나, 그 속에 앱 델리게이트가 없는 경우, 앱 델리게이트[^델리게이트] 객체를 만들고 앱 객체와 연결한다.
  7. 런루프를 만드는 등 실행에 필요한 준비를 마무리해 간다.
  8. 실행 완료를 앞두고 앱 객체가 앱 델리게이트에게 application:didFinishLaunchingWithOptions: 메시지를 보낸다.

 

참고로 글이 쓰여진 시절이 시절인만큼 Swift로 코드가 소개되니 참고하시라.

UIApplicationMain 함수

위에서 언급한 앱의 시작 절차는 애플에 의해 디자인되어 있고, Xcode 프로젝트 역시 앱이 이러한 절차를 통해서 초기화된다는 점을 염두에 두고 설계되어 있다.

따라서 “실제로 Objective-C가 C라면 main함수부터 시작해야 하는데, 왜 Xcode 프로젝트는 앱 델리게이트부터 시작하라는거지?”라는 이상한 질문을 떠올리는 초보 개발자가 아닌 이상, 아무도 신경쓰지 않을 부분일지 모른다. 이 함수의 원형을 파악하고 있는 것은 iOS앱이 어떤 식으로 로드되고 시작되는지, Xcode 상의 여러 객체들, nib 파일은 도대체 어디서 객체로 바뀌어서 짠하고 나타나는지 등을 풀어나가는 열쇠가 될 수 있다. 또, 가끔 인터페이스 빌더 없이 모든 UI를 코드로만 구성하는 팀도 있는데, 이런 경우에는 main.m 파일을 편집해야 하므로, 앱의 구조에 대해서 잘 알아둘 필요가 있다.

코코아 앱도 그렇지만 iOS앱 역시 GUI 기반의 앱이다. 이러한 앱은 그간 간단하게 연습삼아 만들어봤던 명령줄 프로그램과는 다르다. 프로그램은 실행된 이후에 (초기화와 관련된 몇 가지 자체 구성을 제외하면) 아무런 일도 하지 않으며, 사용자가 직접 끝내지 않는 이상 종료되지도 않는다. 그러니까, main() 함수를 호출해서 0을 리턴하고 딱 끝나는 C 프로그램하고는 뭔가 다른 것이다.

Xcode 프로젝트 내의 main.m 파일을 열어보면, main함수는 C의 그것과 완전히 똑같이 생겼고, 이 메인 함수는 단지 다음의 또다른 C 함수를 호출하는 것 외의 내용이 없다.

int UIApplicationMain(
    int argc, 
    char *argv[], 
    NSString *principalClassName, 
    NSString * appDelegateClassName
);

UIApplicationMain 함수는 코코아 터치 프레임워크에서 앱의 라이프 사이클을 시작하는 함수이다. 이 함수는 UIApplication 객체의 인스턴스를 만들고, 해당 객체의 앱으로서 기능하기 위한 제반 초치를 취한다. 코코아터치에서 이 과정은 “앱 로딩 프로세스”라 불린다. 앱 로딩 프로세스는 앱 객체가 생성되는 것을 시작으로, 앱 구동에 필요한 델리게이트, 윈도, 뷰, 뷰 컨트로러들을 로딩하고 초기화하여 이들의 유기적인 관계를 재구성하며 GUI 동작에 필요한 런루프를 만드는 등의 많은 일을 포함한다. (그리고 이 많은 일 중의 거의 대부분은 프로그래머가 일일이 설정하거나 세팅해줄 필요가 없게 끔 되어 있다. 앱 로딩 과정에서 앱이 참조해야 하는 수많은 정보들은 소스코드가 아니라 info-plist라는 프로퍼티 리스트 파일에 정의된다. (맞다, 프로젝트에서 앱이름-Info.plist라고 되어 있는 그 파일이다.)

UIApplicationMain 함수는 main의 고정 파라미터인 argc, argv를 그대로 넘겨받으면서 추가적으로 두 가지 인자를 더 받고 있다. principalClassName은 앱 객체가 될 클래스의 이름인데 이건 정말 극히 특수하게 드문 경우가 아니라면 ‘UIApplication’일 것이다. (그래서 nil을 넣으면 UIApplication으로 대체한다.) 마지막 인자는 앱 델리게이트 클래스 이름이다. 역시 많은 경우에 nib 파일 내에 해당 객체가 이미 만들어져 있기 때문에, 이 값도 넘겨줄 필요가 없다.

하지만 nib 파일을 사용하지 않거나, nib 파일 내에 앱 델리게이트가 없는 경우도 있을 수 있다. 하지만 nib 파일은 없어도 되지만, iOS앱 구조상, 앱 델리게이트는 절대 없어서 안된다. 물론, 그러니까 UIApplicationMain 함수에서 파라미터로까지 정의해놓지 않았겠나. nib 파일을 로딩했는데2 앱 델리게이트가 없다면, UIApplicationMain 함수가 인자로 받은 이름을 기반으로, 앱 델리게이트 역할을 수행할 객체를 자동으로 생성하고 앱 델리게이트로 임명하게 된다.

만약, 메인 nib 파일을 쓰지 않는 프로젝트를 만들었다면 main.m 파일은 대략 다음과 같은 코드로 생성되어 있을 것이다.

    #import <UIKit/UIKit.h>
    #import "MYAppDelegate.h"

    int main(int argc, const char ** argv)
    {
        @autoreleasepool{
            return UIApplicationMain(argc, argv, 
                     nil,   
                     // 앱 델리게이트를 만들 수 있는 힌트를 줘야 한다.
                     NSStringFromClass([MYAppDelegate class]));
        }
    }

메인 nib 파일을 사용하는 경우

메인 nib 파일의 이름은 주로 MainWindow.xib으로 되어 있는데 (.xib 이지만 관례적으로 계속 ‘닙’이라고 읽는다. 게다가 xib를 그대로 읽으려면 이거 원…암튼 영어권에서도 계속 nib 으로 읽는다.) 파일의 이름이 고정되어 있는 것은 아니고 Info.plist 파일에 디폴트로 그렇게 명시되어 있다.

이 프로퍼티 리스트 파일은 주로 <앱이름>-Info.plist라는 파일로 번들에 포함되는데 앱이 처음 론칭될 때 자동으로 읽어들여지고 이 파일에 기록된 여러 설정에 의해 앱의 초기화 방식이 결정되고 그외 다른 여러 곳에 이 값들이 사용될 수 있다.

메인 nib 파일을 사용하는 경우, 앱의 론칭 과정에서 이 nib 파일을 로드해서 그 속에 담겨있는 정보대로 윈도나 뷰 컨트롤러, 각종 뷰와 컨트롤 및 객체들을 복원해 내는데 여기에 앱 델리게이트가 포함되어 있다면 굳이 별도의 앱 델리게이트 객체를 만들지 않아도 된다. 따라서 이 경우라면 위의 main.m 파일은 다음과 같이 차이가 날 것이다.

    #import <UIKit/UIKit.h>
    #import "MYAppDelegate.h"

    int main(int argc, const char ** argv)
    {
        @autoreleasepool{
            return UIApplicationMain(argc, argv, nil, nil);
        }
    }

앱 델리게이트의 역할

앱 델리게이트는 이름 그대로 앱 객체의 대리인 역할을 한다. UIApplication은 서브 클래싱을 하는 경우도 드물고, 별로 그럴 이유도 없으며 그게 쉽지도 않다. 하지만 분명히 앱의 라이프 사이클의 여러 스테이지에서 수행되어야 하는 일은 있다. 따라서 앱 객체 클래스를 직접 서브 클래싱하지 않고 델리게이트를 통해 처리하게 된다. 이러한 패턴처럼 코코아 및 코코아 터치는 상속을 통한 커스텀 클래스를 만들어 나가면서 객체를 확장하기 보다는 다른 이런 언어의 특성들을 사용하여 가능한한 서브 클래싱을 피하는 쪽을 권장한다. (물론 경우에 따라서는 반드시 서브 클래싱 해야하는 객체들도 많이 있다.)

앱의 시작과 종료

앱이 시작되면 앱 델리게이트는 application:didFinishLaunchingWithOptions:라는 메시지를 받게 된다. 이 메소드는 앱이 실행 준비를 거의 마쳤을 때 호출되는데, 이 때 앱에 필요한 크리티컬한 데이터의 초기화를 하면 된다. 단, 아직 앱이 화면에 나타나기 이전이므로 시간이 많이 걸리는 작업을 여기서 하면 안된다. 데이터의 초기화를 위해 이 메소드는 거의 모든 경우에 오버라이드 된다.

- (BOOL)application:(UIApplication *)application 
                  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

특히 메인 nib 파일을 사용하지 않는 앱이라면, 윈도 및 메인 뷰가 없는 상태이므로, 이 메소드에서 생성하여 설치해야 한다.

그리고 앱에 종료되기 직전에는 다시 이를 알려주는 메시지가 전달된다. 앱 델리게이트에서는 잃어서는 안되는 사용자 데이터를 종료 전에 미리 저장해 둘 기회를 얻는 셈이다.

- (void)applicationWillTerminate:(UIApplication *)application

그 외에도 앱 델리게이트는 앱의 상태 변화에 대해 감지할 수 있다. 비활성화 된다거나 잠자기에 들어간다거나 저장된 상태로부터 복구된다거나 하는 것을 알아차리며, iOS 디바이스가 회전하였을 때, 방향에 따라 화면을 회전할 것인지를 결정할 수도 있다.

앱델리게이트의 주요 메소드에 대해서는 UIAppDelegateProtocol 공식 문서를 참고하면 된다.


  1. 물론 절대 직접 담당하지는 않고 앱 델리게이트에게 모조리 위임한다. 
  2. 앱 로딩 과정에서 Info.plist 내에서 메인 nib 파일 이름을 명시했다면, 자동으로 해당 nib파일을 읽어들여서 UI 관련 객체들을 초기화한다.