본문 바로가기

iOS/UIKit

UIViewController의 존재 의의, UIView의 관점에서

 UIKit에서 화면을 다루기 위해서 꼭 필요한 존재 ViewController에 대해서 이해해보려고 합니다. 특히 오늘은 View 관점에서 이 ViewController에 대해 자세히 알아보겠습니다. 설명의 편의를 위해 UI라는 프레임워크 접두어를 일부 생략하였습니다.

 

Apple이 말하는 ViewController

Apple의 공식 문서에서는 아래와 같은 책임을 가지고 있다고 설명하고 있습니다.

  1. UI요소 배치 및 크기 조정
  2. 사용자와의 상호작용에 반응하기
  3. UI를 데이터 기반으로 업데이트 해주기
  4. 다른 뷰컨트롤러를 비롯한 여러 Objects와 소통하며 앱의 이벤트들을 조정하기 등

결국 ViewController는 View에서 일어나는 일을 전체적으로 관장하는 객체라고 정리해볼 수 있겠네요.

 

ViewController와 View의 관계

Root View와 강한 결합

 ViewController는 하나의 root view를 직접 소유하고 있습니다.(view라는 프로퍼티를 통해서) 이 root view는 하위에 여러 view들을 가질 수 있는데요, 버튼, 레이블을 비롯한 UI 요소를 비롯해 빈 view또한 하위 뷰로 추가할 수 있다는 이야기입니다.

 View는 결국 사용자에게 무언가를 보여주는 역할이 본질입니다. 그러나 UI/UX라는 것이 벽에 붙은 포스터처럼 정적이고 단방향으로 보여주기만 하는 것이 아닌, 사용자와 상호작용 할 수 있는 수단이죠? 그래서 Apple은 UIResponder라는 class를 상속해 responder chain의 일부가 됨으로써 View에 이벤트에 반응(Response)할 수 있도록 처리해놓았습니다.

만약 버튼이 직접 이벤트를 핸들링한다면?

 예를 들어, 버튼을 눌렀을 때 다른 레이블의 내용을 바꿔주거나 색을 변경하는 역할을 ViewController를 통해 부여하는 것이 아니라, 버튼 객체에게 직접 부여한다고 생각해봅시다. 이런 단순한 동작이 하나만 있을 때는 문제가 없어보일 수 있지만, 상태와 로직이 복잡하게 얽혀져 있다면 아래와 같은 여러 소프트웨어공학적인 문제가 생길 수 있습니다.

  • 강한 결합 문제
  • 책임 분리 위반
  • 유지보수 문제
  • 디버깅 어려움

ViewController의 책임

 그래서 rootView와 그 하위 View에서 일어나는 모든 이벤트들을 취합해 앱 화면의 상태를 핸들링하는 존재가 필요했는데, 그것이 바로 ViewController입니다. ViewController의 입장은 이런 것이죠.

"버튼1아 너 눌렸다고? 그럼 라벨1 너는 이거 글자 바꿔야지. 다음 화면 불러와야 한다고? 알았어 뷰컨2야 다음 화면은 너에게 맡긴다~" 같이 지시를 내려주고 view 간의 동작을 중재하는 팀장님 같은 존재가 바로 ViewController라고 볼 수 있겠습니다. delegate pattern이나 notification 같은 방식으로 여러 객체와 소통도 하면서 말이죠. 그치만... 이 ViewController의 방식은 뭐랄까... 마이크로 매니징하는 팀장님 같은 느낌이랄까요? 조금만 코드 분리를 느슨하게 해도 ViewController에 모든 로직이 집중이 될 수 밖에 없는 구조인 것 같습니다.

view 프로퍼티의 비밀(?)

 ViewController는 기본적으로 view프로퍼티를 가지고 있다고 했었죠. 앞서 말했던 rootView입니다. ViewController가 초기화 될 때 view가 같이 초기화되는 것이 아니라, 그 이후 view에 접근하는 시점에 비로소 메모리에 올라가게 됩니다. (lazyily 하게 동작한다고 공식문서에 등장하기도 하는데, view가 lazy 프로퍼티라는 뜻은 아니고, 프레임워크의 내부 구현이 lazy하게 되어있다고 보면 될 것 같습니다).

class MyViewController: UIViewController {     
    override func viewDidLoad() {        
        super.viewDidLoad()        
        print("viewDidLoad called")    
    }
} 

let vc = MyViewController() // 아직 viewDidLoad가 호출되지 않음
print(vc.view) // view가 생성되고 viewDidLoad가 호출되는 시점

 그래서 이 부분이 바로 ViewController의 생명주기와도 연관되는데, 바로 loadView()viewDidLoad()가 위 시점에서 호출되는 친구들입니다. view 프로퍼티에 접근하는 시점에 그 값이 nil이라면 loadView()가 호출되고, viewDidLoad()는 view가 메모리에 올라갔을 때 호출되는 것이죠. 추상적으로 생각했던 지점이었는데, 한번 이렇게 정리해보겠습니다.

    • loadView(): ViewController에서 view 프로퍼티에 최초로 접근했을 때 nil인지 확인하고, nil이라면 호출된다.
    • viewDidLoad(): loadView()가 실행이 끝나고, 메모리에 view가 로드된 시점. 스토리보드로 만들어진 뷰라면 IBOutlet과 IBAction도 연결되어있는 시점
    • 그렇기 때문에 ViewController 객체를 생성하고, view 프로퍼티에 먼저 접근을 하면, viewDidLoad()가 실행되는 시점이 예상한 시점보다 빨라질 수도 있다는 사실 또한 추론할 수 있겠습니다.

 

느낀점

 ViewController는 View와 강한 결합을 가지며 View들 간의 상호작용을 중재하고 앱의 전체적인 흐름을 제어하는 중요한 객체라는 사실을 알아보았습니다. 일부 생명주기 메서드들이 실제로 언제 호출되는지도 간단하게 확인해볼 수 있었습니다.

공식문서조차 ViewController자체가 갖는 결합도가 높다고 명시하고 있으니, 그로부터 비롯되는 View와 ViewController 자체를 하나의 View 계층으로 보는 시각으로 보는 아키텍처 패턴들이 등장한 것도 자연스럽게 이해가 됩니다. 마음만 먹으면 ViewController에 앱의 거의 모든 로직을 넣을 수 있으니... 가능하면 정적인 View 속성이나 상태 속성은 View 계층에서 처리해 ViewController의 책임을 덜어내주면 좋겠습니다. 다음에는 ViewController 생명주기, View의 Drawing Cycle, ViewController가 가진 책임을 분리하는 패턴에 대해 알아보도록 하겠습니다. 감사합니다.