iOS_Swift 앱개발👍

[iOS_Swift] VIPER 디자인 패턴 _ 44

개발하는윤기사 2023. 12. 27. 09:25
728x90
반응형

 

안녕하세요! 윤기사입니다!

 

이번 포스팅에서는 MVVM, MVC 디자인 패턴 이외에 VIPER 디자인 패턴에 대해 정리해보려고 합니다!

 

 

VIPER(View, Interactor, Presenter, Entity, Router)

 

VIPER 패턴은 각각 (View, Interactor, Presenter, Entity, Router)의 앞 글자를 따서 이름이 명명되었습니다!

 

SOLID 5원칙 중 단일 책임 원칙(SRP)을 기반으로 한 아키텍처입니다! 

 

단일 책임 원칙이라고 하면,  모든 클래스는 각각 하나의 책임만 가져야 한다. 클래스는 그 책임을 완전히 캡슐화해야 함을 뜻 합니다.

 

간단하게 아래 표를 보시죠!

  1. View(ViewController) : 주로 UIViewController, UIView들이 이 역할을 하게 됩니다. UI와 관련된 처리만 하며, Presenter에 대한 의존성을 가지고 있고, 앱 생명 주기에 따른, 아니면 사용자 입력 동작에 대한 처리를 Presenter에게 위임하고, Presenter의 요청을 받아 UI를 update 하는 역할.
  2. Presenter : View, Router, Interactor에 대한 의존성을 가지고 있습니다. View로부터 Event를 전달받아 Interactor를 통해 처리하고, View에 data와 함께 UI Update 요청을 보내거나, Router를 통한 화면 이동을 처리합니다.
  3. Interactor : 데이터 또는 네트워크와 관련된 비즈니스 로직을 가지고 있는 단위입니다. 주로 API 호출, data 관련된 로직을 가지고 있는 단위의 모듈이며, API 통신 등의 네트워킹이나 Entity에 대한 처리를 하고 결과를 Presenter에 전달합니다. (뷰와는 완전 독립적)
  4. Entity : 일반적인 데이터 객체들입니다. 단순하게, 데이터 모델이라고 생각하면 될 것 같습니다.
  5. Router : 화면이 언제 표시되어야 하는지 어떤 화면을 띄울지 등 Navigatoin 로직을 가지고 있습니다. (뷰 전환 담당)

 

이런 식으로 응집도는 높고, 결합도는 낮은 상태로, 역할 단위의 구분이 명확하지만, 양날의 검이라고 생각이 듭니다 ^_^

 

(장점이 될 수도, 단점이 될 수도 있다는 생각...)

 

 

 

VIPER의 장점과 단점

좋은 아키텍처의 3가지 특징인 Distribution, Testability, Easy of use를 기반으로 VIPER의 장점과 단점 정리!

  • Distribution - 각각의 역할 구분이 정말 명확하기 때문에 책임 분배 면에서는 정말 큰 장점을 가지고 있습니다.
  • Testability - 책임 분배의 명확성(독립성)은 더 좋은 testability를 만듭니다.
  • Easy of Use - 역할 단위의 구분이 명확!! 작은 역할을 가지는 클래스들을 위해 엄청나게 많은 인터페이스를 작성해야 한다는 것이 단점입니다. 

 

 

VIPER 구현 코드

- API Call을 통하여 아래와 같은 형식의 JSON 데이터를 파싱 해서 name을 TableView에 표시하는 예제를 한 번 해볼 겁니다!

[
  {
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": {
      "street": "Kulas Light",
      "suite": "Apt. 556",
      "city": "Gwenborough",
      "zipcode": "92998-3874",
      "geo": {
        "lat": "-37.3159",
        "lng": "81.1496"
      }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
      "name": "Romaguera-Crona",
      "catchPhrase": "Multi-layered client-server neural-net",
      "bs": "harness real-time e-markets"
    }
  },
  
  ...
 
]

 

View.swift

- ViewController를 의미하는 View 부분입니다! Presenter에 대한 의존성을 가지고 있으며, 앱 LifeCycle에 따른, 아니면 사용자 동작에 대한 처리를 Presenter에게 위임하고, Presenter의 요청을 받아 UI를 update 하는 역할을 합니다!

- View의 기본적인 구조는 Protocol, Object로 나뉩니다!

import UIKit

protocol AnyView {
    var presenter: AnyPresenter? { get set }
    
    func update(with users: [User])
    func update(with error: String)
}

class UserViewController: UIViewController, AnyView, UITableViewDelegate, UITableViewDataSource {
    
    var presenter: AnyPresenter?
    
    private let tableView: UITableView = {
       let table = UITableView()
        table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        table.isHidden = true
        return table
    }()
    
    private let label: UILabel = {
        let lbl = UILabel()
        lbl.textAlignment = .center
        lbl.isHidden = true
        return lbl
    }()
    
    var users: [User] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        view.addSubview(tableView)
        view.addSubview(label)
        tableView.delegate = self
        tableView.dataSource = self
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.frame = view.bounds
        label.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
        label.center = view.center
    }
    
    func update(with users: [User]) {
        print("got users")
        DispatchQueue.main.async {
            self.users = users
            self.tableView.reloadData()
            self.tableView.isHidden = false
        }
    }
    
    func update(with error: String) {
        print(error)
        DispatchQueue.main.async {
            self.users = []
            self.label.text = error
            self.tableView.isHidden = true
            self.label.isHidden = false
        }
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return users.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = users[indexPath.row].name
        return cell
    }
    
    
}

 

Interactor.swift

- 비즈니스 로직을 담당하는 Interactor 부분입니다!

- API 통신, 네트워킹이나 Entity에 대한 처리를 하고 결과를 Presenter에 전달하는 역할을 합니다. 이 부분에서는 URLSession을 이용한 API response 값을 presenter에 전달해주고 있죠!

import Foundation

protocol AnyInteractor {
    var presenter: AnyPresenter? { get set }
    
    func getUsers()
}

class UserInteractor: AnyInteractor {
    var presenter: AnyPresenter?
    
    func getUsers() {
        print("Start Fetching")
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else { return }
        let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
            guard let data = data, error == nil else {
                self?.presenter?.interactorDidFetchUsers(with: .failure(FetchError.failed))
                return
            }
            
            do {
                let entities = try JSONDecoder().decode([User].self, from: data)
                self?.presenter?.interactorDidFetchUsers(with: .success(entities))
            }
            catch {
                self?.presenter?.interactorDidFetchUsers(with: .failure(error))
            }
        }
        task.resume()
    }
}

 

 

Presenter.swift

- 가장 많은 의존성을 가지고 있는 Presenter입니다! View, Router, Interactor에 대한 의존성을 가지고 있습니다!
- View로부터 Event를 전달받아 Interactor를 통해 처리하고, View에 data와 함께 UI Update 요청을 보내거나, Router를 통한 화면 이동을 처리하는 가장 의존성이 높은 녀석입니다 ^_^

import Foundation

enum FetchError: Error {
    case failed
}

protocol AnyPresenter {
    var router: AnyRouter? { get set }
    var interactor: AnyInteractor? { get set }
    var view: AnyView? { get set }
    
    func interactorDidFetchUsers(with result: Result<[User], Error>)
}

class UserPresenter: AnyPresenter {
    var router: AnyRouter?
    
    var interactor: AnyInteractor? {
        didSet {
            interactor?.getUsers()
        }
    }
    
    var view: AnyView?
    
    func interactorDidFetchUsers(with result: Result<[User], Error>) {
        switch result {
        case .success(let users):
            view?.update(with: users)
        case .failure:
            view?.update(with: "Something Wrong")
        }
    }
}

 

 

Entity.swift

- 단순하게 Data Model을 의미합니다 ^_^

import Foundation

struct User: Codable {
    let name: String
}

 

 

Router.swift

-  화면 전환과 각 구성요소에 의존성 주입을 처리하는 역할을 하는 Router입니다! 뷰 전환을 담당하고 있어요.

import UIKit

typealias EntryPoint = AnyView & UIViewController

protocol AnyRouter {
    var entry: EntryPoint? { get }
    
    static func start() -> AnyRouter
}

class UserRouter: AnyRouter {
    var entry: EntryPoint?
    
    static func start() -> AnyRouter {
        let router = UserRouter()
        
        //Assign VIP
        var view: AnyView = UserViewController()
        var presenter: AnyPresenter = UserPresenter()
        var interactor: AnyInteractor = UserInteractor()
        
        view.presenter = presenter
        
        interactor.presenter = presenter
        
        presenter.router = router
        presenter.view = view
        presenter.interactor = interactor
        
        router.entry = view as? EntryPoint
        
        return router
    }
}

 

 

SceneDelegate.swift

- SceneDelegate에서는 Router에 대한 entryPoint를 정해줍니다! 이렇게 함으로써 처음 앱이 실행될 때 보이는 initial ViewController를 설정할 수가 있어요!

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    guard let windowScene = (scene as? UIWindowScene) else { return }

    let userRouter = UserRouter.start()
    let initialVC = userRouter.entry

    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = initialVC
    self.window = window
    window.makeKeyAndVisible()
}

 

 

 

 

이상으로 이번 VIPER 패턴에 관한 포스팅을 마치겠습니다 ^_^

 

 

 

 

 

 

 

 

💎 참고 블로그

 

iOS 아키텍처 패턴 VIPER

iOS 아키텍처 패턴, 디자인 패턴에 대해 알아보던 중 VIPER 패턴에 대해 공부할 일이 있어서 겸사겸사 블로그에 정리해보려고 합니다. Apple's MVC 좌측이 Classic MVC 우측이 apple's MVC인데, iOS 앱 개발 관

bugle.tistory.com

 

 

[Architecture] VIPER 패턴

안녕하세요, 제인입니다! 최근 시작한 아키텍처 및 디자인 스터디에서 첫번째로 주제로 다루었던 VIPER 아키텍처 패턴에 대해 정리해보려 합니다. 바로 시작하겠습니다😊 VIPER Pattern이란? View, Int

janechoi.tistory.com

 

 

 

💎 참고 영상

https://youtu.be/hFLdbWEE3_Y?si=XVJQKwnZCy_QRbkc

728x90
반응형