[iOS 앱 만들기 004] 뷰 컨트롤러 아웃렛과 액션

뷰 컨트롤러의 주된 역할인 뷰의 제어에 대해 알아보려면 먼저 MVC 패턴에 대해 살짝 이야기하고 넘어갈 필요가 있을 것 같다. MVC 패턴은 객체 지향 프로그래밍의 디자인 패턴이지만, 코코아터치 프레임워크 곳곳에 적용되어 있으며, iOS 앱을 만들 때에도 필연적으로 가장 많이 사용될 수 밖에 없는 패턴이다.

MVC

MVC는 말 그대로 모델컨트롤러의 각 머리글자를 딴 이름이다. 프로그램을 쪼개고 쪼개어 단위 기능별로 쪼갠다고 할 때, 각각의 단위기능은 “사용자로부터 입력을 받아, 이 데이터를 정해진 규칙에 따라 가공하고, 다시 그 결과를 사용자에게 전달하는” 것으로 압축되는데, 이 작업을 수행하는데 필요한 객체들을 역할에 따라 나누는 것이다. 이 때 뷰는 사용자에게 시각적으로 정보를 전달하거나, 혹은 정보를 입력받기 위해 UI를 표시하는 역할을 담당한다. (UIView가 주로 맡게 된다) 모델은 데이터 모델 객체로 정해진 규칙에 따라 데이터를 가공하는 알고리듬을 내장하고 있다. 그리고 이 두 객체는 컨트롤러가 그 사이를 잇는 가교 역할을 하게 된다.

예를 들어 3가지 버튼 중에서 한 가지 버튼을 누르면 누른 버튼에 따라 정해진 문구가 화면에 출력되는 프로그램이 있다고치면, 이 프로그램은 다음과 같이 MVC의 역할이 정해진다. 먼저 모델 객체는 특정한 정수값을 입력 받아, 그에 맞는 문구를 반환하는 기능을 포함한다. 뷰는 버튼들과 텍스트를 출력할 레이블을 포함한다. 컨트롤러는 이 둘을 연결한다. 사용자가 버튼을 누르면(터치하면) 뷰는 컨트롤러의 정해진 액션을 호출한다. 컨트롤러는 이 액션을 통해 버튼의 고유 숫자값을 모델에 전달한다. 모델은 숫자값에 따른 텍스트를 반환하는데, 다시 컨트롤러가 이를 뷰를 통해 표시하도록 뷰를 컨트롤한다.

얼핏보면 뷰에서 터치가 일어나면 그 즉시 모델에게 값이 전달되어 모델이 다시 뷰에게 화면을 업데이트하도록 할 수도 있는데 굳이 한다리를 걸쳐야 하나? 싶은 생각이 들 때도 있다. 만약 단위 기능이 이처럼 간단하다면 이 때는 모델 객체가 컨트롤러의 기능을 포함해버려도 상관은 없을 것이다. 아니 심지어 뷰 자체가 이를 바로 처리해도 상관은 없다.

어쩌면 작성하는 코드가 가장 적을 수 있을테니 세 번째 방법이 더 좋지 않겠냐고 할 수도 있겠는데, 이처럼 간단한 기능은 그럴 수 있다. 하지만 프로그램의 기능이 확장되거나 변경될 가능성은 언제나 존재하기 때문에 적어도 뷰와 모델은 확실히 분리되어야 한다. 또한 어떤 뷰가 표현하는 데이터의 데이터 모델이 변경되는 것도 충분히 가능하고 혹은 같은 뷰를 다른 데이터 모델에 연결해야 할 수도 있기 때문에 뷰와 모델을 직접 연결하는 것도 확장과 재사용에 관해서는 그리 좋지 못할 수 있다.

이처럼 MVC는 데이터와 그 표현(presentation)을 완전히 분리하는 것을 중점으로 한다. 물론 모든 경우에 이렇게 3개의 역할로 딱 떨어지는 건 아니고 경우에 따라서는 4개 이상의 역할로 나눌 수도 있고 그 보다 적을 수도 있다. 뷰 컨트롤러는 일반적으로 컨트롤러의 역할을 담당할 수 있는데, 좀 복잡도가 높은 앱을 작성하는 경우에는 뷰 컨트롤러와 모델 컨트롤러가 별도로 구분되기도 한다. 뷰 컨트롤러는 이런 경우 그 특성상, 보다 뷰와 밀접하게 연관되는 역할을 담당하게 된다.

아웃렛과 타깃액션

아웃렛

뷰 컨트롤러가 뷰 혹은 그 뷰의 서브 뷰를 직접적으로 제어하고자 한다면 해당 객체에 대한 링크 혹은 참조가 있어야 한다. 이런 방법에는 몇 가지가 있는데, 하나는 컨트롤러의 인스턴스 변수를 만들고 거기에 해당 뷰의 포인터를 저장하는 방법이 있고, 또한 해당 객체의 참조를 얻어내는 메소드를 만든 다음, 이 메소드가 리턴해주는 객체를 사용하는 방법이 있다. 물론 후자의 경우는 대부분 암시적으로 전자의 방법을 포함한다. 즉, 인스턴스 변수를 하나 만들고, 메소드를 통해 그 인스턴스 변수를 반환한 값을 사용하는 것이다.

이 방식은 우리가 흔히 접하는 프로퍼티(Declared Property)와 조금도 다르지 않다. 물론 이렇게 프로퍼티를 사용하는 주된 이유는 객체의 외부에서 해당 속성 (혹은 인스턴스 변수)에 접근할 수 있는 길을 열어주기 위한 것인데, 그 외에도 프로퍼티로 선언하는 속성은 다양한 장점을 지닌다.

  1. 작성하기 따라서는 충분히 인터페이스를 외부에 노출하지 않을 수 있다. 카테고리를 통한 클래스 확장을 이용하면, 헤더 파일에 의해 객체 외부로 드러나지 않는 내부 인터페이스로서의 프로퍼티를 만들 수 있다.
  2. getter 프로퍼티는 단순히 인스턴스 변수를 리턴하는 것 외에도 초기화 되지 않은 인스턴스 변수를 초기화 하는 과정을 포함할 수 있다. 이는 lazy-loading을 구현한 것으로, 만약 이 프로퍼티를 앱의 라이프 사이클 내에서 사용하지 않았다면, 해당 속성은 할당이나 초기화가 되지 않았으므로 메모리를 절약할 수 있는 방법이 된다. UIViewControllerview 프로퍼티 역시 이러한 방식으로 설계되어 있다. (view가 nil일 때 스토리보드를 열거나, 새로운 UIView 객체를 할당한다.)

이렇게 뷰 컨트롤러가 직접 제어할 UI요소에 대해서 Xcode는 IBOutlet이라는 특별한 기능을 제공한다. 아, 물론 IB를 전혀 사용하지 않고 앱을 작성하는 사람이라면, 굳이 이 챕터(내지는 이 포스팅 전체)를 읽을 필요는 없다.

아웃렛은 인터페이스 빌더에서 컨트롤러 객체와 뷰 계층 구조 상의 특정한 객체를 연결할 수 있는 연결고리의 걸쇠(eyelet)를 만들어주는 효과가 있는데, 실제 컴파일 시에는 아무런 기능도 수행하지 않는다. UIViewController를 커스터마이징하는 서브 클래스를 작성하고 있고, 이 뷰 컨트롤러는 수시로 현재 상태를 나타내는 텍스트를 표시할 레이블을 하나 가지고 있다고 가정해보자. 뷰 컨트롤러는 이 UILabel객체를 프로퍼티로 사용할 것이며, nib 파일로부터 로드될 것이라는 점도 추가로 가정한다.

    @interface MYViewController : UIViewController
    @property (nonatomic, retain) IBOutlet UILabel *statusField;
    @end

그렇다면 해당 프로퍼티는 아웃렛으로 선언하도록 한다. 아웃렛으로 선언하기 위해서는 프로퍼티 선언문의 중간에 IBOutlet이라는 문구를 추가해주면 된다. 이 문구는 매크로로 컴파일 시에 삭제된다. (#define IBOutlet으로 매크로가 정의되어 있다.) 따라서 컴파일시에는 아무 필요가 없는 말이다. 다만 인터페이스 빌더는 프로젝트 내 클래스들의 헤더에서 이 문구가 포함된 프로퍼티를 시각적으로 연결할 수 있는 속성이라고 인식한다. (코코아 터치 책 사면 인터페이스 빌더에서 뷰 컨트롤러와 레이블을 연결하는 바로 그 기능!)

프로퍼티의 레이지 로딩

lazy-loading은 뷰 컨트롤러를 작성할 때 높은 우선순위로 염두에 두어야 하는 특성이다. 액세스 한 적 없는 프로퍼티나 리소스는 가능한한 뒤늦게 할당하고 초기화하여 리소스 사용을 최소화하려는 성향이다. 만약 어떤 객체가 숫자들을 담아두는 배열을 프로퍼티로 가지고 있다고 하자. 물론 초기화 메소드(Designated init method)에서 객체 생성시에 필요한 프로퍼티들을 초기화 하는 방법도 있지만, 프로퍼티의 접근자를 다음과 같이 구성하는 편을 더 추천한다.

    -(NSMutableArray *)numbers
    {
        if(!_numbers) {
            _numbers = [[NSMutableArray alloc] initWithCapacity:10];
        }
        return _numbers;
    }

_numbers 인스턴스 변수는 객체의 초기화 과정에서는 전혀 생각하지 않고, 해당 객체에 대한 액세스가 일어나는 시점에 초기화하는 것이다. (물론 또 다른 한편으로는 액세스가 거의 확실히, 그것도 빈번하게 일어나는 프로퍼티라면 초기화 때 확실히 할당하는 것이 나을 수 있지 않겠냐는 의견이 있을 수 있는데… 자세한 건 다음에 기회가 되면 이야기하자.)

아웃렛으로 설정한 프로퍼티

아웃렛으로 설정한 프로퍼티에는 별다른 getter를 작성하는 일도 잘 없고, 별도의 초기화를 거치지 않는 경우도 많다. 왜냐하면 IBOutlet을 사용한다는 것은 해당 클래스를 nib 파일로부터 읽어들여 객체화(instantiate)한다는 것인데, nib 파일을 읽어들일 때 뷰 컨트롤러는 물론 그 속의 뷰와 그 하위뷰들이 뷰 계층 구조를 이루면서 모두 객체로 생성된다. 그리고 이렇게 생성된 모든 객체들은 다시 subview-superview 관계라든지, 미리 설정된 아웃렛 관계, 그리고 뒤에서 다시 설명할 액션에 대한 정보들이 모두 nib 파일을 로딩하는 시점에 실체화되어 나타나기 때문이다. 만약 -initWithNibName:bundle:을 사용해서 뷰 컨트롤러를 초기화한 경우라면 주어진 nib 파일내에 정의된 모든 객체와 객체간의 관계는 모두 초기화과 완료된 상태로 복원된다.

결론 : IB없이 iOS앱 개발하는 거 어렵지 않다. 그런데 IB 써라. 그게 정신건강에 좋을 수 있다.

타깃-액션

타깃-액션은 원래 디자인 패턴 중 하나로 특정 타깃에게 액션 메시지를 전달하는 디자인 패턴인데, iOS에서 컨트롤(버튼과 같이 사용자와 상호작용하는 UI요소)이 터치 이벤트를 받을 때 수행되는 작업이다. 즉 터치 이벤트를 받은 컨트롤은 자신의 타깃에게 미리 정의된 액션을 수행하도록 메시지를 보내게 된다. 따라서 이벤트를 받는 객체는 에 해당하지만, 그에 대한 처리를 컨트롤러로 넘기는 방식이다.

타깃은 컨트롤 요소가 동작을 수행하도록 요청하는 대상이 되는 객체이고, 액션은 타깃이 수행해야 하는 메소드를 셀렉터(@selector())의 형태로 정의한 것이다. 타깃-액션 역시 아웃렛을 연결하는 것과 동일한 방법으로 인터페이스 빌더에서 정의할 수 있다. 연결의 출발점이 UIControl 객체인 경우, 해당 연결은 액션을 의미하게 된다.

아웃렛과 마찬가지로 인터페이스 빌더는 클래스의 헤더에 정의된 메소드가 타깃-액션에 사용된다는 힌트를 받아 이를 알고 있어야 하는데, 이 기능을 수행하는 키워드가 IBAction이다. IBActionIBOutlet과 마찬가지로 매크로로 정의되어 있는데, 컴파일시에는 void로 변환되어 처리된다.

만약 인터페이스 빌더를 사용하지 않는 경우에도 타깃-액션을 지정할 수 있다. 컨트롤 객체에 대해 다음 두 메시지를 보내면 된다.

  • - (void)setTarget:(id)anObject
  • - (void)setAction:(SEL)aSelector

아웃렛과 마찬가지로 인터페이스 빌더를 통해서 이를 생성했다면 액션은 nib 파일 로딩시에 연결된 상태로 복원되며, 심지어 타깃-액션을 지정하는데 필요한 해당 객체에 대한 포인터를 따로 갖고 있지 않아도 되는 장점도 있다. (물론 동적으로 타깃과 액션을 변경해야하는 경우가 있다면 인스턴스 변수나 프로퍼티로 해당 컨트롤을 소유하고 있어야 하겠지만)

액션의 시그니처

액션메시지로 전달되는 메소드는 정해진 시그니처를 가지고 있다.

    - (void)someAction:(id)sender
    - (void)someAction
    - (void)someAction:(id)sender forEvent:(UIEvent *)event

UIKit은 위의 세 종류의 시그니처를 액션 메시지로 인식할 수 있다. 보다 자세한 내용은 UIControl 레퍼런스를 참조하자.

정리

사실 간단한 내용인데 조금 설명을 늘어놓은 감이 없잖아 있는 것 같다. 요점만 간단히 다시 한 번 정리해 보면. 1) 뷰 컨트롤러는 뷰와 상당히 밀접하게 연결된 컨트롤러이다. 2) 뷰 컨트롤러가 직접적으로 제어하려는 객체는 뷰 컨트롤러가 프로퍼티 등으로 지정하여 알고 있어야 한다. 이 때, 인터페이스 빌더상의 객체를 컨트롤러와 연결하기 위해서는 해당 객체를 IBOutlet으로 연결한다. 3) UI컨트롤 요소는 뷰 컨트롤러에게 액션 메시지를 전달하여 사용자 터치에 대한 동작을 수행할 수 있는데, 이는 액션으로 연결한다. 4) 따라서 뷰 컨트롤러를 사용하려면 인터페이스 빌더를 사용하는 것을 적극적으로 추천한다.

다음 시간에는 UIKit에서 제공하는 뷰 컨트롤러들의 종류에 대해 간략하게 살펴보고자 한다. UIKit은 기본 뷰 컨트롤러 클래스외에도 앱을 구성하는 데 유용하게 활용할 수 있는 몇 가지 표준 뷰 컨트롤러들을 제공하고 있고, 이게 상당히 다양하다. 또한 뷰 컨트롤러가 자식 뷰 컨트롤러를 가질 수 있기 때문에 네비게이션 구조를 짜는데 이러한 뷰 컨트롤러 유형들에 대해 간략하게 알고 있는 것은 큰 도움이 된다.