apple/DesignPattern, Architecture

iOS VIPER 아키텍처 정리

lgvv 2024. 12. 3. 02:29

iOS VIPER 아키텍처 정리

 

VIPER Architecture는 Jeff Gilbert와 Conrad Stoll이 클린 아키텍처를 iOS 앱 개발에 특별하게 적용하기 위해서 개발하고, 대중화

해당 포스팅에서는 조금 더 최신화 형태로 구성

 

예제 파일

ArchitectureExample.zip
0.24MB

VIRER란?

  • View
    • ViewController로 xib/storyboard
  • Interactor:
    • 데이터, 네트워킹 및 비즈니스 로직을 담당.
  • Presenter
    • View와 Interactor 사이에 데이터를 전달하고, 사용자 이벤트를 처리하여 라우터를 호출.
    • VIPER의 다른 모든 객체들과 통신하는 유일한 객체
  • Entity
    • 데이터 객체이며 데이터 접근은 Interactor에서 가능
    • VIPER 모듈 사이에서 화면 전환을 담당Router
  • Builder (해당 포스팅에서 구현에 추가된 개념)
    • RIBs 처럼 하나의 VIPER 모듈을 만드는 Builder 객체
    • 의존성 문제 해결, 빌드 성능 개선 등을 위해 만들어짐.

VIPER 이미지

 

 

 

VIRER의 폴더 구조

VIPER 폴더 구조는 아래처럼 구성되며, 의존성이 적은 순서대로 아래 코드 예제를 진행.

위의 이미지와 다르게 Builder를 통해 객체를 생성

 

VIPER 폴더 구조

 

 

 

Entity 코드

간단한 객체로 인터렉터에 의해서 데이터를 가공

// MARK: - Entity

public struct HomeEntity {
    public let title: String
    public let description: String
    
    public init(title: String, description: String) {
        self.title = title
        self.description = description
    }
}

 

Interactor 코드

Interactor의 인터페이스는 Input과 Output으로 구분하여 Interactor가 비대해지지 않도록 구성

  • Input: 인터렉터로 들어오는 이벤트 (인터렉터를 호출)
  • Output: 인터렉터가 비즈니스 로직 등의 처리를 끝내고 Presenter로 전달
// MARK: - Interactor
import Model

protocol HomeInteractorInputInterface {
    func fetch()
}

protocol HomeInteractorOutputInterface: AnyObject {
    func updateHomeLabel(title: String, description: String)
}

class HomeInteractor: HomeInteractorInputInterface {
    weak var output: HomeInteractorOutputInterface?
    
    func fetch() {
        let data = HomeEntity(title: "Viper", description: "Home")
        output?.updateHomeLabel(title: data.title, description: data.description)
    }
}

 

Router 코드

기본적으로 화면전환을 담당

  • 해당 화면이 현재 UINavigationContoller로 되어 있어서 UINavigationController를 들고 있음
  • RIBs의 didBecomeActive처럼 해당 예제에서는 ViewController의 ViewDidLoad에서 navgiationContoller를 넣어줌.
import UIKit
import HomeDetail

protocol HomeRouterInterface {
    var navigationContoller: UINavigationController? { get }
    
    func pushToDetail()
}

class HomeRouter: HomeRouterInterface {
    var navigationContoller: UINavigationController?
    
    init() {}
    
    func pushToDetail() {
        let view = HomeDetailBuilder().build()
        
        navigationContoller?.pushViewController(view, animated: true)
    }
}

 

View 코드

ViewController로 View를 담당

  • presenter로 사용자에게 들어온 이벤트를 전송
  • HomeViewInterface를 통해 View가 이벤트를 수신
import UIKit
// MARK: - View

protocol HomeViewInterface: AnyObject {
    func updateHomeLabel(title: String, description: String)
}

final class HomeViewController: UIViewController {
    
    var presenter: HomePresenterInterface

    override func viewDidLoad() {
        super.viewDidLoad()

        presenter.viewDidLoad(self)
        
        configureUI()
    }
    
    init(presenter: HomePresenterInterface) {
        self.presenter = presenter
        
        super.init(nibName: nil, bundle: nil)
        configureUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - UIComponents
    
    private lazy var button: UIButton = {
        $0.setTitle("디테일 화면 이동 버튼", for: .normal)
        $0.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
        return $0
    }(UIButton())
    
    @objc
    func didTapButton() {
        presenter.didTapButton()
    }
    
    private let label: UILabel = {
        $0.lineBreakMode = .byWordWrapping
        $0.numberOfLines = 0
        $0.textAlignment = .center
        $0.textColor = .black
        $0.font = .systemFont(ofSize: 24, weight: .semibold)
        return $0
    }(UILabel())
    
    private func configureUI() {
        view.backgroundColor = .blue
        
        view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
        
        view.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: button.bottomAnchor, constant: 20),
            label.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            label.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }
}

extension HomeViewController: HomeViewInterface {
    func updateHomeLabel(title: String, description: String) {
        label.text = "\(title)\n\(description)"
    }
}


Presenter 코드

VIPER 모듈의 다른 객체를 모두 알고 있음

  • 이벤트 간의 중재자 역할을 수행
import UIKit
// MARK: - Presenter

protocol HomePresenterInterface: AnyObject {
    func viewDidLoad(_ viewControllable :HomeViewInterface)
    func didTapButton()
}

class HomePresenter: HomePresenterInterface {
    weak var view: HomeViewInterface?
    
    var interactor: HomeInteractorInputInterface
    var router: HomeRouterInterface

    init(interactor: HomeInteractorInputInterface, router: HomeRouterInterface) {
        self.interactor = interactor
        self.router = router
    }
    
    func viewDidLoad(_ viewControllable: HomeViewInterface) {
        self.view = viewControllable
        interactor.fetch()
    }
    
    func didTapButton() {
        router.pushToDetail()
    }
}

extension HomePresenter: HomeInteractorOutputInterface {
    func updateHomeLabel(title: String, description: String) {
        view?.updateHomeLabel(title: title, description: description)
    }
}

 

Builder 코드

VIPER 모듈의 한 객체를 생성하는 역할을 담당

  • 빌드시간 개선을 과정에서 의존성 관리를 위해 필요
  • BuilderInterface에 의존
import UIKit

public protocol HomeBuilderInterface {
    func build() -> UINavigationController
}

public struct HomeBuilder: HomeBuilderInterface {
    
    public init() {}
    
    public func build() -> UINavigationController {
        let router = HomeRouter()
        let interactor = HomeInteractor()
        let presenter = HomePresenter(
            interactor: interactor,
            router: router
        )
        interactor.output = presenter
        
        let navigationController = UINavigationController(
            rootViewController: HomeViewController(presenter: presenter)
        )
        router.navigationContoller = navigationController

        return navigationController
    }
}

 

 

VIPER 모듈 사용하는 방법

Builder를 통해 생성한 객체를 해당 형태로 사용하면 끝.

let builder: HomeBuilderInterface = HomeBuilder()
let rootViewController = builder.build()
        
window?.rootViewController = rootViewController

 

 

정리

VIPER 장점

  • 독립적인 모듈 및 낮은 결합도로 인한 유지보수성 증대
  • 테스트하기 좋은 코드
  • 모듈화 용이성

VIPER의 아쉬운점

  • Presenter가 로직을 주도
    • 따라서 Presenter가 없는 형태로 구성하기 여려워 완전한 모듈화의 어려움
    • 이를 보완하고자 RIBs 등장
  • RIBs는 비지니스 로직(Interactor)가 주도

 

(참고)

https://dev.to/marwan8/getting-started-with-the-viper-architecture-pattern-for-ios-application-development-2oee

 

Getting Started with the VIPER Architecture Pattern for iOS Application Development

When you are planning to build an app, one of the most important decisions is to choose how to...

dev.to