채널 iOS에 Redux를 적용하게 된 7가지 이유.

친숙한 MVC 패턴

개발자라면 누구에게나 친숙한 MVC (모델 - 뷰 - 컨트롤러) 패턴은 꽤 오랜 시간 동안 사용됐고 아직까지 많은 개발자들에게 사랑받고 있는 패턴이다. 그 이유로는 이 패턴이 일단 진입장벽이 낮기도 하지만 코드 재사용성, 동시 개발의 용이성 때문이다. 만약 당신이 초보 iOS 개발자라면 높은 확률로 MVC 패턴을 쓰게될 것인데 그 이유는

  • 대부분의 예제 및 튜토리얼이 MVC 패턴을 쓰고 있고
  • iOS의 IDE인 Xcode에서 (Swift 는 예외지만) 클래스를 생성할때 기본으로 이름에 ViewController 라고 들어간다.

위와 같은 이유로 많은 iOS 개발자에 영향을 주리라 생각된다. (2011년도부터 iOS 세계에 빠진 저자도 사실 iOS에서는 software architectural design pattern으로는 MVC가 넘사벽이라고 생가하고 있었기에) 문제는 상대적으로 복잡도가 높아지거나 코드의 양이 많은 제품의 개발에서는 생산성이나 가독성에 그다지 도움을 주지 못하는 데 있다고 생각한다. 예를 들어, 한 페이지의 복잡도가 높아지면 ViewController 파일 한 개의 코드 라인이 기하급수적으로 증가한다. 또 (코드 관리에 매우 신경을 쓰지 않는 이상) 객체 간의 통신 및 데이터의 통일성이 없어져서 가독성이 떨어지기 쉽고, 기능을 추가할 때 생산성이 점점 떨어지게 된다.

왜 MVC 패턴은 이렇게 문제가 생기는걸까

라는 질문에서부터 시작해보자.

MVC 패턴, 도대체 뭐가 문제인가?!

그림 1. 보편적인 MVC 패턴의 구조

보편적으로, MVC 패턴의 구조는 위의 그림과 같다. 그림을 간단히 설명하자면:

  • 뷰에서 이벤트가 발생하면 컨트롤러에 알린다
  • 컨트롤러는 그것을 처리하고 모델에 업데이트를 하라고 전달한다.
  • 모델은 업데이트를 하고 컨트롤러에 다시 알린다
  • 컨트롤러는 모델이 업데이트되었다는 것을 뷰에 알린다
  • 뷰는 모델의 업데이트된 값에 따라 다시 뷰를 그린다

그림 1과 위의 설명만 놓고 보면 각각의 역할이 명확하다고 생각한다. 구조가 복잡하지 않기 때문에 초보자들도 쉽게 이해하고 적용 가능하다는 것이 장점이다. 하지만 MVC 패턴은 객체 간에 어떤 방향으로 커뮤니케이션 해야 하는지에 대해서는 강제하지 않기 때문에 파생된 패턴들이 많이 있다. 실제로 구글에서 “MVC pattern”이라고 검색을 하면 위 그림과 다른 MVC 패턴 이미지들을 볼 수 있다. 그 한 가지 예가 밑에 그림 2이다.

그림 2. 또 다른 MVC 패턴의 구조

그림 2를 보면 그림 1과는 다른 커뮤니케이션 방향을 나타내고 있다. 바꿔 말하면 개발자가 원하면 언제든지 세 가지 구조 안에서 방향을 유동적으로 바꿔 써도 무방하다는 것이 된다 (그것이 원하는 MVC 패턴이든 아니든지 간에). MVC의 변형으로써는 여러 가지가 있지만, 대표적인 것들은 아래의 그림과 같이 MVP, MVVM 같은 것들이 있다.

그림 3. MVC, MVP, MVVM 패턴의 비교

실제 저자도 MVC 패턴이 커뮤니케이션 방향을 강제하지 않는 것과 관련해 문제를 겪은 경험이 여러 번 있었던 것을 기억한다. 한가지 예를 들어보자.

ViewA.swift (뷰)

protocol ViewADelegate {  
    func updateA()
}

class ViewA : UIView {  
     var delegate: ViewADelegate?

     //update through protocol
     func didClickOnA() {
         self.delegate?.updateA()
    }

    //update through notification
    //maybe same kind of update can happen in other views 
    func didClickOnAA() {
        NotificationCenter.default.post(
            name: NSNotification.Name(rawValue: “updateFromA”), 
            object: nil
        )
    }

    func render(_ model: product) {
        //update based on model 
    }
}

ViewController.swift (컨트롤러)

class ViewController : UIViewController, ViewADelegate {  
    Var viewA: ViewA?
    Var product = Product()
    func viewDidLoad() {
        self.viewA = ViewA()
        self.viewA.delegate = self
        // ... 
        self.view.addSubview(self.viewA)
    }

    func updateA() {
        self.product.update(name: “aa”, version: “123”)
        self.viewA.update(self.product)
        //re-render viewA
    }
}

Product.swift (모델)

class Product {  
    var name = “”
    var version = “”
    init() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(self.doSomething),
            name: “updateFromA”, object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    func update(name: String, version: String) {
        self.name = name
        self.version = version
    }

    func doSomething() {
         //do something…
         //notify viewA or any objects through notification
    }
}

조금 극단적인 예처럼 보이긴 하지만 실제 개발을 하다 보면 충분히 일어날 수 있는 상황이다. 코드에 대해 간략하게 설명하자면:

  • ViewA에서는 delegate와 notification으로 각각 ViewControllerProduct에 이벤트를 날리고 있고
  • ViewController에서는 delegate method를 구현해서 Product를 업데이트 후, 다시 ViewA를 그리라는 로직을 가지고 있다.
  • Product 에서는 객체를 업데이트 할 수 있는 메소드가 있고 notification을 통한 업데이트를 하고 있다.

이건 아주 간단한 예이지만 프로젝트가 커진다면 특정 이벤트에 대해 데이터가 업데이트되는 경로가 달라질 수 있다. ViewA -> Product -> SubProduct -> Product -> ViewA 의 경로라던가, ViewA -> Controller -> Product -> SubProduct -> Controller -> ViewA 의 경로 등이 가능하다. 이처럼 특정 이벤트에 대해 여러 가지 체인형식으로 업데이트가 이루어질 경우 그 경로를 일일이 추적하는데 시간이 걸릴 수밖에 없는 구조를 가지고 있는 것을 볼 수 있다.

(프로젝트의 크기가 어느정도 커지게 된다면 이렇게 될지도 ㅎㅎ)

이런 케이스가 발생하는 근본적인 이유는 결국 MVC 패턴의 장점이라고도 말할 수 있는 유연성과 양방향 커뮤니케이션 때문이다. 이 패턴 자체가 문제가 있는 것은 아니지만 결국 코드는 사람이 작성하는 것이기에 생산성과 가독성을 떨어뜨리는 결과를 초래할 가능성이 높다. 여기에서 우리는 기존 웹 개발에서 쓰이고 있던 Redux 도입을 생각하게 된 것이다.

Redux는 무엇인가?

Redux 로고

Redux는 Facebook의 Flux 를 모태로 삼고 있고 예측 가능한 상태를 자바스크립트 프로그램에서 구현하기 위한 애플리케이션 아키텍쳐이다. Redux는 본래 자바스크립트에서 시작한 오픈소스 프로젝트이지만 다른 개발자들에게 영감을 주었고 2015년 말쯤 iOS 플랫폼에서는 ReSwift(Redux + Swift)가 생겨났다. ReSwift는 결국 Redux랑 크게 다르지 않고 Redux의 세 가지 법칙을 따른다.

  • Single source of truth — 애플리케이션의 전체 상태(State, 또는 데이터)는 트리 형태의 하나의 저장소(Store)로 저장된다.

  • Changes with pure functions — 상태 트리를 변경하는 리듀서(Reducer)는 순수 함수(pure function)이어야한다.

  • Read-only states — 상태는 오직 액션(Action, 어떤 일이 일어날 것인지 설명할 수 있는 객체)으로만 변화가 가능하다.

쉽게 말하자면 “Redux는 한 개의 상태 저장소를 가지고 있고 그 안에 있는 데이터만이 신뢰할 수 있으며 저장소의 상태는 오직 순수 함수인 리듀서를 통해서만 변화가 가능하다” 라고 축약 할 수 있다.

그림 4 Redux 패턴의 구조

위의 그림 4을 보면 충분히 프로그램의 흐름이 어떤 식으로 흐르는지 감이 왔으리라 생각한다.

  1. 이벤트가 뷰에서 생성되면 그에 해당이 되는 액션을 통해 알린다.
  2. 액션은 특정 리듀서에서 처리한다.
  3. 리듀서는 액션에 따라 저장소를 업데이트한다.
  4. 저장소에 변화가 오면 구독(Subscribe)을 하고 있는 모든 객체에 알린다.

이것이 Redux의 커뮤니케이션 사이클이다. Redux만으로도 충분히 여러가지 블로그 주제가 나올 정도로 할 이야기가 많지만 여기까지만 하고, 좀 더 자세한 디테일을 알기 원한다면 옆의 링크를 클릭하시면 된다. :) -> 리덕스 공식 링크

Redux vs. MVC

MVC와 Redux에 대해 소개를 했으니 간단히 비교해 보자.

  • The Flow — Redux는 데이터 및 애플리케이션의 흐름을 강제한다. 저장소의 변화는 오직 액션을 통해서만 가능하기 때문이다. 이와 다르게 MVC는 강제성이 없기 때문에 여러가지 파생 패턴이 생길 수 있다.

  • Unidirectional flow — Redux에서 흐름은 액션으로만 변화가 일어나기 때문에 오직 한쪽으로만 흐른다. MVC에서는 양방향이 될 수도 있고 한 방향이 될 수도 있지만 보통 양방향이다.

  • Stores — Redux에서는 상태 및 데이터가 하나의 저장소에 있기 때문에 관리하기가 쉬운 반면, MVC에서는 여러 군데에 상태가 분리되어 있기 때문에 동기화에 신경을 써야 한다. (로컬 데이터 스토리지를 쓴다면 문제가 해결되기는 하지만 패턴 이외에 추가적인 노력이 필요함)

그 이외에 여러가지 다른 점이 있겠지만, 위의 3가지가 가장 다른 점이라고 저자는 생각한다.

채널 데스크 iOS에 Redux를 적용하게 된 이유

이제 MVC와 Redux의 차이점을 알게 되셨으리라 생각한다. 우리 팀이 채널 데스크 iOS에 Redux를 적용한 이유를 소개하려고 한다. 아직 모든 부분에 완벽히 적용한 상태는 아니지만 (부분적으로 Notification, Delegate 그리고 Reactive를 쓰고 있다) 그럼에도 Redux를 적용함으로써 얻는 이점이 많다고 느끼고 있다.

Explicit data flow — 새로운 개발자가 왔을 때나 여러 명이 작업을 할때 애플리케이션의 전체 흐름을 파악하고 이해하기 쉽다.

Unidirectional flow — 데이터 관련 부분을 전부 Redux로 대체하니 모든 데이터 흐름이 한 방향으로 강제되었다. 덕분에 데이터가 어디에서 왔고 어디로 가는지를 파악하기 매우 쉽다.

Single storage — 한 곳에서만 데이터를 관리하기 때문에 데이터에 관한 부분은 리듀서만 잘 짜 놓으면 관리하기 쉬워진 점이 있다. Redux를 적용하기 전에 CoreData를 데이터 저장소로 쓰고 있었는데, 어느 시점에 어떻게 저장되는지 눈에 들어오지 않아 불편한 점을 Redux를 사용함으로써 해결할 수 있었다.

Immutability and data consistency — 변경 가능한(Mutable) 객체는 보통 iOS 개발에서나 다른 플랫폼 개발에서 장점일수도 있다. 하지만 데이터의 일관성이 깨지기 쉽다. 만약 A에서의 데이터와 B에서의 데이터가 다르면 어떤 것을 신뢰해야 하는지의 문제도 생길 수 있다. 우리는 Redux의 저장소에 있는 데이터를 모두 변경 불가능한 객체(Immutable, Swift에서는 Struct을 쓴다)로 구현하여 이 문제를 해결하였다. 이 부분은 코딩할 때 불편한 점이 조금 있지만, 그 불편함을 감수할만한 가치가 있다고 생각한다.

Predictability — 저장소는 오직 액션을 통해서만 변경할 수 있다는 점이 무엇보다 장점인 것 같다. MVC와 같이 데이터를 어디서든 변경할 수 있다면 데이터와 관련된 버그를 찾는 데 소비하는 시간이 길어지곤 한다. Redux는 어떤 액션이 어디에서 불리는지 아는 것만으로도 그 시간을 비약적으로 단축할 수 있다.

Maintainability — 저장소, 상태, 액션 그리고 리듀서로 역할과 레이어를 분리하게 되니 보통 코드 라인이 100줄을 넘지 않는다. 그만큼 유지보수 비용이 적어졌다.

Organized Code — MVC 패턴에서는 비지니스 로직이 뷰에 들어가는 경우가 있기도 했었는데 Redux의 가이드라인을 따름으로써 자연스럽게 대부분의 뷰는 그저 데이터를 받고 시각화하는 dummy 뷰의 형태가 되었다. 비즈니스 로직이 완전히 뷰와 분리됨으로써 뷰의 복잡도와 코드를 관리하기가 쉬웠다.

ReSwift 도입 시 주의할 점

ReSwift 도입을 고려하는 분들을 위해 몇 가지 주의할 점을 소개하겠다.

Performance — ReSwift에서는 저장소가 변경될 때마다 newState: 메소드가 호출이 되어 화면을 업데이트할 수 있게 되어있다. 채널 데스크 같은 경우는 실시간 애플리케이션(Real-time application)이라서 API 이벤트와 Socket 이벤트가 자주 발생해서 저장소가 변경되는데, 도입 초기 단계에 이 부분을 간과해서 화면이 거의 멈출 정도로 퍼포먼스가 나오지 않았었다. 만약 ReSwift를 적용했는데 퍼포먼스가 나오지 않는다면 newState: 함수 부분을 최적화하거나 미들웨어(middleware)를 만들어서 batch 형식으로 액션을 처리하는 방식을 고려해봐야 한다.

Not thread safe — ReSwift는 thread-safe 하지 않아서 초반에 알 수 없는 crash들이 자주 발생했었다. 저자 같은 경우는 ActionWrapper를 만들어서 액션은 항상 메인스레드에서 처리되도록 강제했다.

글을 마무리하며..

Redux는 이미 자바스크립트 개발에서는 React와 함께 많이 쓰이고 있지만 iOS에서는 아직도 생소한 아키텍쳐이다. ReSwift는 아직 2년도 되지 않은 프로젝트이고 자바스크립트에서 처럼 유용한 Redux 미들웨어도 많지 않다. 또한 인지도도 MVC, MVVM, MVP에 아직 미비한 편이다. 프로덕션에 참고할 만한 예제도 찾기 어려웠기에 초기 러닝 커브는 조금 있었던 것으로 회상한다. 그럼에도 불구하고 우리 팀은 ReSwift를 적용해 보다 깨끗하고 유지보수하기 쉬운 프로그램을 만들 수 있었고 만족하며 사용하고 있다. 기존 MVC의 불편함을 아시는 분들은 충분히 도입할 가치가 있다고 생각한다.

Wooyoung Chung

by Wooyoung Chung

If I'm knocked seven times, I come back on my eighth.