-
VIPER 뿌셔보기 (2)VIPER 2023. 8. 7. 12:36
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 VIPER 2탄, 코드로 알아보는 VIPER를 소개하면서 학습해보겠습니다 🙌
우선, VIPER의 기본적인 개념 및 특징 그리고 구조 등을 1탄에서 다뤘기에 먼저 선행으로 보고 오시는걸 추천드립니다 😉
그럼 우리의 시간은 귀중하니까 핵심으로 바로 넘어가볼께요.
오늘 뿌셔볼 부분이 뭐라했죠?
1탄에서 알아본 구조들을 코드로 각 파트를 표현해보는 과정을 해봐야해요.
시작도 안했는데 뭔가 큰 산이 남아있을것 같은 느낌이 들지만... 바로 들어갑니다~
코드로 표현하는 VIPER
저는 코딩하면 가장 대표 예제중 하나인 할 일들을 목차로 담은 TodoList를 만들어볼께요!
해당 포스팅에서 핵심은 VIPER의 각 구성요소들에 어떤 코드들이 들어가고 어떤 흐름인지 보는것이지 앱의 기능적 완성도가 초점이 아니라 정말 러프하게 구현해보려합니다 😃
순서는 VIPER 약자 순서대로 해도 좋지만 흐름상 자연스러운 구성으로 소개할께요!
E (Entity)
가장 우선적으로 뷰에 표현할 모델 타입의 DTO를 만들어볼께요.
import Foundation struct Todo: Identifiable { let id = UUID() let title: String }
뷰에 얹어줄 모델 타입인 Todo DTO에요.
아주 간단하게 리스트로 표현해주기위해 Identifiable 프로토콜을 따르면서 id 값으로 구분합니다.
그리고 어떤 할일인지 title로 표현해줘요.
끝! 아주 간단합니다.
이제 View를 만들기전에 Presenter, Interactor, Router를 만들어볼거에요.가장 먼저 Entity를 패치하기 위해 Interactor를 구현합니다.
I (Interactor)
사실 여기서는 네트워크 통신을 통해 데이터 모델을 받아오고 그걸 뷰에서 사용될 필드들을 이용해 뷰모델 DTO를 구성하는게 어떻게 보면 현업에서 가장 적합하게 쓰일것이지만 여기 예제에서는 네트워크 통신 없이 우선 임의의 fake 구현을 해둘께요.
import Foundation protocol TodoListInteractorInput: AnyObject { func fetchTodos() -> [Todo] } class TodoListInteractor: TodoListInteractorInput { func fetchTodos() -> [Todo] { let todos = (1...10).map { Todo(title: "할 일 \($0)") } return todos } }
보시면 먼저 해당 TodoListInteractor 클래스 타입을 구현하기위해 따라아할 Input 프로토콜을 정의합니다.
서비스 단에서 Todo를 패치해오기 위해 fetchTodos라는 메서드 구현을 담은 프로토콜이에요.
그리고 해당 인터렉터 타입에서 프로토콜을 채택하고 fetchTodos를 구현해줍니다.
여기서 보통 네트워크 통신이 이뤄지지만 저는 네트워크 통신이 없으니 10개의 데이터를 직접 만들어주고 이를 반환해줍니다.
인터렉터에서는 결국 모델 DTO에 값을 설정해주기 위한 네트워킹 및 코어 작업들이 이뤄진다고 생각하면 됩니다.
그 후, Router로 넘어가볼께요!
R (Router)
import Combine class TodoListRouter: ObservableObject { @Published var selectedTodo: Todo? = nil func moveToSelectedTodoDetail(todo: Todo) { self.selectedTodo = todo } func closeSelectedTodoDetail() { self.selectedTodo = nil } }
라우터에서는 화면 전환을 관장한다고 했습니다.
제 예제에서는 각 Todo 항목들을 클릭하면 해당 Todo 정보를 담은 DetailView로 화면 전환을 구성할 예정이라 화면 전환을 위한 라우터 구성이 필요합니다.
사실 Presenter에서 이를 처리해줘도 되지만 VIPER를 모두 따르면서 한번 해볼께요!
라우터에서 선택된 Todo 타입의 프로퍼티를 가집니다.
선택되어 이동되기 위해 moveToSelectedTodoDetail 메서드에서 해당 프로퍼티를 변경해줍니다.
그리고 어떤 특정 버튼 혹은 디테일 뷰에서 나갔을때 다시 이 selectedTodo 값을 초기화 시켜 주기 위한 메서드를 구성합니다.
그럼 라우터 끝!
그리고 뷰를 구현하기전에 최종적으로 가장 필수인 Presenter를 구현해보시죠 🕺🏻
P (Presenter)
import SwiftUI class TodoListPresenter: ObservableObject { @Published var todos: [Todo] = [] @ObservedObject var router: TodoListRouter private let interactor: TodoListInteractorInput init( interactor: TodoListInteractorInput, router: TodoListRouter ) { self.interactor = interactor self.router = router fetchTodos() } private func fetchTodos() { todos = interactor.fetchTodos() } func didSelectedTodo(_ todo: Todo) { router.moveToSelectedTodoDetail(todo: todo) } func didFinishDisplayTodoDetail() { router.closeSelectedTodoDetail() } }
Presenter는 뷰와 상호작용을 하기 위한 타입으로 뷰의 인터렉션을 받아 처리하거나 뷰에 모델 타입을 전달해 뷰를 표현하기 위한 모든 작업을 해줄 수 있습니다.
그렇기에 Todo 모델 타입 프로퍼티와 라우터의 동작을 위해 router 프로퍼티도 가지고 있습니다.
또한 실제 네트워크 통신 등 코어 서비스 단의 동작을 위해 interactor도 들고있죠.
저는 해당 presenter를 초기 init 시 실제 인터렉터에서 Todo들을 받아오기 위해 fetchTodos 메서드를 구현하고 이를 init 메서드에서 호출해줬습니다.
이렇게 하지 않고 해당 Presenter와 연결된 View에서 viewDidLoad 시에 이 작업을 해줘도 무방할것 같긴 해요.
그리고 화면 전환을 위해 didSelectedTodo 메서드에서 유저의 Todo 항목 탭 인터렉션이 발생하면 들고있는 라우터 프로퍼티의 moveToSelectedTodoDetail 메서드를 호출하여 현재 선택된 Todo 정보를 제공하여 이동시킬 수 있죠.
마찬가지로, didFinishDisplayTodoDetail 메서드를 구성해 isActive를 초기화 하기 위해 현재 선택된 Todo를 다시 초기화 하도록 라우터의 메서드를 호출해줍니다.
이것들은 이제 실제 View 단에서 같이 연동되어 상호작용을 할거에요!
그럼 이제 마지막 대망의 View를 보겠습니다 😁
V (View)
우선 저는 View를 SwiftUI로 구현했으니 참고 부탁드려요 🙏🏻
TodoListView
import SwiftUI struct TodoListView: View { @ObservedObject var presenter: TodoListPresenter var body: some View { NavigationView { List(presenter.todos) { todo in NavigationLink( destination: TodoDetailView(todo: todo), isActive: Binding( get: { presenter.router.selectedTodo?.id == todo.id }, set: { isActive in if isActive { presenter.didSelectedTodo(todo) } else { presenter.didFinishDisplayTodoDetail() } } ), label: { Text(todo.title) } ) } .navigationTitle("Todo List") } } }
가장 메인 진입점으로 TodoListView를 구성합니다.
들고있는 프로퍼티로는 우리가 만든 Presenter 밖에 없어요.
사실상 뷰모델이라고 인지해도 좋을듯해요.
간단히 NavigationView를 사용해 List로 뿌려줍니다.
여기서 화면전환 isActive를 위해 id 값을 판별해주고 선택되면 presenter의 didSelectedTodo 메서드를 호출하고 isActive 상태가 아닌 즉, 한번 클릭해서 다시 나오면 id를 초기화 해주기 위해 presenter의 didFinishDisplayTodoDetail 메서드를 호출합니다.
요러면 일단 TodoListView는 간단히 상호작용 연동 및 구현 완료!
그 다음으로 디테일 뷰를 보시죠~
TodoDetailView
import SwiftUI struct TodoDetailView: View { let todo: Todo var body: some View { VStack { Text(todo.title) .font(.largeTitle) .padding() Spacer() } .navigationBarTitleDisplayMode(.inline) } }
정말 별거없죠~
단순히 해당 Todo의 Title을 담아주는것 그외 없습니다.
여기서 해당 뷰가 disAppear될때 위 TodoListView의 todo id를 초기화하는 코드를 넣어도 되긴합니다.
그렇기 위해서는 TodoDetailView도 프로퍼티로 presenter를 들고 있어야합니다.
요건 선택하기 나름인듯 합니다 🙋🏻
근데 이제 이 만들어진 뷰의 진입점 즉, App 파일에서 어떻게 초기 설정하는지 간단히 보고 끝낼께요.
App
import SwiftUI @main struct TodoListApp: App { var body: some Scene { WindowGroup { TodoListView( presenter: TodoListPresenter( interactor: TodoListInteractor(), router: TodoListRouter() ) ) } } }
해당 TodoListView를 띄우기 위해 presenter 프로퍼티가 필요한데 presenter는 interactor와 router를 의존하고 있기에 각 타입들을 상위부터 의존성 주입을 통해 넣어줄 수 있습니다.
그렇다면 추후 test를 위해 갈아끼기도 편하고 이점이 있겠네요!
이제 동작을 볼까요?
잘 돌아가나 동작해보기
잘만 돌아갑니다~
해당 파일들의 구조 다시 훑어보기
이런 구조로 TodoList 앱 구현을 위해 VIPER 구성요소들을 나눌 수 있었습니다.
마무리
엄... 확실히 파일이 많고 따라야할게 많아 귀찮긴 하지만 테스트 가능한 환경에 유리할것 같네요.
조금 더 공부해보면서 뭐가 실제로 더 좋은지 어떤게 단점같은지는 파악해보면서 한번 정리해보는 포스팅을 따로 가져볼께요 😉
오늘 목표했던 코드로 한번 구현해보기는 가볍게 일단 끝내봤습니다.
해당 프로젝트 코드가 궁금하신분들은 아래 깃헙 레포에서 편하게 보시고 사용해보셔도 좋습니다 🙌
https://github.com/GREENOVER/playground/tree/main/TodoList
다음엔 뭐?
다음번엔 실제 여기서 보완해서 Interactor 부분을 개선해 실제 네트워크 통신을 붙여보면서 한번 더 뿌셔볼께요!
'VIPER' 카테고리의 다른 글
VIPER 뿌셔보기 (3) (16) 2023.08.10 VIPER 뿌셔보기 (1) (10) 2023.08.02