VIPER 아키텍처 - Team-HGD/SniffMEET GitHub Wiki

개요

View, Interactor, Presenter, Entity, Router의 구성 요소로 이루어진 아키텍쳐 패턴

장점

  • 모듈 간의 명확한 역할 분리
  • 테스트 용이성이 높다.

단점

  • 초기 설계 및 구현 시 코드가 많아지며 복잡해진다.
  • 간단한 화면에 VIPER를 적용할 경우 오히려 과도한 설계가 된다.

구조

View

사용자 인터페이스 요소, Presenter와만 통신한다.

protocol ExampleViewProtocol: AnyObject {
    func displayData(_ data: String)
}

class ExampleViewController: UIViewController {
    var presenter: ExamplePresenterProtocol!

    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.viewDidLoad()
    }
    
    // 버튼 액션 메서드
    @IBAction func buttonTapped(_ sender: UIButton) {
        // 버튼 클릭 이벤트를 Presenter에 전달
        presenter.handleButtonTapped()
    }
}

// ExampleViewProtocol을 구현한 익스텐션
extension ExampleViewController: ExampleViewProtocol {
    func displayData(_ data: String) {
        // Presenter로부터 데이터를 받아서 UI 업데이트
        label.text = data
    }
}

Presenter

View와 Interactor 사이의 데이터 흐름을 관리하는 컴포넌트, 비즈니스 로직을 처리하지 않는다.

역할

  • UI업데이트 요청: Interactor로부터 받은 데이터를 View에 전달하여 UI를 업데이트 한다.
  • 사용자 이벤트 처리: View에서 발생한 이벤트를 Interactor에 보내서 처리하도록 한다.
// Presenter의 역할을 담당하는 프로토콜
protocol ExamplePresenterProtocol: AnyObject {
    func viewDidLoad() // View가 로드되었을 때 호출되는 메서드
    func handleButtonTapped() // 버튼 클릭 이벤트 처리
}

class ExamplePresenter: ExamplePresenterProtocol {
    weak var view: ExampleViewProtocol?  // View에 대한 약한 참조
    var interactor: ExampleInteractorProtocol  // 비즈니스 로직 처리
    var router: ExampleRouterProtocol  // 화면 전환 관리

    // 의존성 주입 (Presenter는 View, Interactor, Router를 의존성으로 가짐)
    init(view: ExampleViewProtocol, interactor: ExampleInteractorProtocol, router: ExampleRouterProtocol) {
        self.view = view
        self.interactor = interactor
        self.router = router
    }

    // View가 로드되었을 때 호출되는 메서드
    func viewDidLoad() {
        // Interactor에 데이터 요청 (비즈니스 로직은 Interactor에서 처리)
        interactor.fetchData()
    }

    // 버튼 클릭 이벤트 처리
    func handleButtonTapped() {
        // 버튼 클릭 시 Interactor에게 작업을 요청
        interactor.performAction()
    }
}

// Interactor에서 데이터를 성공적으로 받아왔을 때 호출되는 메서드
extension ExamplePresenter: ExampleInteractorOutputProtocol {
    func dataFetchedSuccessfully(data: String) {
        // 데이터를 View에 전달하여 UI를 업데이트
        view?.displayData(data)
    }

    // 데이터 로드 실패 시 호출되는 메서드
    func dataFetchFailed(withError error: Error) {
        // 에러 메시지를 View에 전달
        view?.displayError(error.localizedDescription)
    }
}

특징

  • Interactor, View, Router를 의존성으로 가지고 있다.
  • Presenter는 View를 weak 참조한다(순환 참조 방지).
  • View에서 발생한 사용자 액션에 대응하는 메소드를 통해 액션을 Interactor에 전달한다.

Interactor

비즈니스 로직을 담당하는 컴포넌트, 데이터와 관련된 작업을 수행한다.

역할

  • 비즈니스 로직 처리
  • 데이터 요청: 데이터 베이스나 네트워크에서 필요한 데이터를 요청하거나 저장함
  • 결과 전달: Presenter에게 데이터 처리 결과를 전달하여 UI를 업데이트 할 수 있도록 한다.
protocol ExampleInteractorProtocol {
    func fetchData()  // 데이터를 가져오는 메서드
    func performAction()  // 특정 작업을 수행하는 메서드
}

protocol ExampleInteractorOutputProtocol: AnyObject {
    func dataFetchedSuccessfully(data: String)  // 데이터 로드 성공 시 호출
    func dataFetchFailed(withError error: Error)  // 데이터 로드 실패 시 호출
}

class ExampleInteractor: ExampleInteractorProtocol {
    weak var presenter: ExampleInteractorOutputProtocol?  // Presenter와의 약한 연결

    // 데이터를 가져오는 메서드
    func fetchData() {
        // 데이터 로직 처리 (예: 네트워크 요청 또는 데이터베이스 접근)
        // 성공 시:
        presenter?.dataFetchedSuccessfully(data: "데이터가 성공적으로 로드되었습니다.")
        
        // 실패 시:
        // presenter?.dataFetchFailed(withError: someError)
    }

    // 특정 작업을 수행하는 메서드
    func performAction() {
        // 비즈니스 로직 처리
        // 결과에 따라 Presenter에 알림
        presenter?.dataFetchedSuccessfully(data: "작업이 성공적으로 완료되었습니다.")
    }
}

특징

  • Presenter를 weak참조한다.
  • 데이터를 처리하고, 그 처리 결과를 Presenter의 메소드를 호출하여 Presenter에 전달

Entity

데이터 모델을 정의하는 부분, 앱의 주요 데이터 구조를 나타낸다.

역할

  • Interactor가 비즈니스 로직을 처리할 때 필요한 데이터 구조나 모델을 정의함
  • 비즈니스 로직의 데이터 구조만 정의하며, 로직을 처리하지 않음
// 예시 사용자(Entity) 모델 정의
struct User: Codable {
    let id: Int
    let name: String
    let email: String
    let age: Int
}

// 예시 제품(Entity) 모델 정의
struct Product: Codable {
    let id: Int
    let name: String
    let price: Double
    let stock: Int
}

설계 기준

  • Simple and Focused: 데이터만 표현하고 로직을 포함하지 않음

데이터 흐름

  • Interactor는 네트워크 요청을 통해 받아온 데이터를 Entity 구조로 변경시킨다.
  • Presenter는 Entity 데이터를 View에 맞는 형식으로 가공해서 전달한다.

Router

화면 전환이나 모듈 간의 이동을 처리한다.

역할

  • 특정화면에서 다른 화면으로 이동하거나 팝업을 띄우는 등의 작업을 수행한다.
  • 화면을 이동할 때, 필요한 데이터를 다음 화면으로 전달하는 작업을 수행한다.
  • 각 모듈을 초기화하고 필요한 의존성을 주입하여 각 컴포넌트를 연결한다.
protocol ExampleRouterProtocol {
    func navigateToDetail(from view: ExampleViewProtocol, with data: String)  // 상세 화면으로 이동
}

class ExampleRouter: ExampleRouterProtocol {
    
    // Detail 화면으로의 네비게이션 메서드 구현
    func navigateToDetail(from view: ExampleViewProtocol, with data: String) {
        // 현재 View를 UIViewController로 변환
        guard let viewController = view as? UIViewController else { return }
        
        // DetailViewController 인스턴스 생성 및 데이터 설정
        let detailViewController = DetailViewController()
        detailViewController.data = data
        
        // 현재 ViewController에서 DetailViewController로 푸시 네비게이션
        viewController.navigationController?.pushViewController(detailViewController, animated: true)
    }
    
    // 모듈 초기화 메서드 - VIPER 각 구성 요소 설정 및 연결
    static func createModule() -> UIViewController {
        let view = ExampleViewController()
        let interactor = ExampleInteractor()
        let router = ExampleRouter()
        
        let presenter = ExamplePresenter(view: view, interactor: interactor, router: router)
        view.presenter = presenter
        interactor.presenter = presenter
        
        return view
    }
}

동작 흐름

  • Presenter에서 화면 전환 요청이 발생하면, Router의 화면 이동 메서드 호출
  • Router는 ViewController를 참조하여 내비게이션 작업 수행, 필요한 데이터를 다음 화면에 전달
  • 모듈을 생성하여, 독립적인 화면 초기화

의존성 주입