ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • VIPER 뿌셔보기 (3)
    VIPER 2023. 8. 10. 10:08

    안녕하세요. 그린입니다 🍏
    이번 포스팅에서는 VIPER의 네트워킹과 Interactor 부분을 치중하여 한번 학습을 해보려합니다 🙌

     

    일단 이전 포스팅을 기반으로 이야기를 풀어나갈 예정이기에 사전에 꼭 보고오시는게 좋습니다!

     

    VIPER 뿌셔보기 (2)

    안녕하세요. 그린입니다 🍏 이번 포스팅에서는 VIPER 2탄, 코드로 알아보는 VIPER를 소개하면서 학습해보겠습니다 🙌 우선, VIPER의 기본적인 개념 및 특징 그리고 구조 등을 1탄에서 다뤘기에 먼저

    green1229.tistory.com

     

    자 그럼 Interactor를 알고 계신다는 가정하에 진행하겠습니다.



     

    실제 네트워크 통신을 가진 Interactor 구현하기

     

    이전 2탄에서는 Interactor를 네트워크 통신 없이 데이터 모델을 임의로 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
      }
    }

     

    이렇게 예시코드처럼 말이죠.

    그런데 실제로 네트워크 통신을 붙여서 사용하는 경우가 대다수일거에요.

    그렇기에 이번에는 해당 fetchTodos 메서드 호출 시 네트워크 통신을 통해 데이터를 불러오고 모델을 업데이트 해주도록 하겠습니다.

     


    Service 구현하기

    가장 우선은 Service 구조체를 만들어야 합니다.

     

    import Foundation
    
    protocol TodoNetworkServiceProtocol {
      func getTodos(completion: @escaping (Result<[Todo], Error>) -> Void)
    }
    
    struct TodoNetworkService: TodoNetworkServiceProtocol {
      private let baseURL = URL(string: "여기에 API URL을 넣어줍니다!")!
      
      func getTodos(completion: @escaping (Result<[Todo], Error>) -> Void) {
        URLSession.shared.dataTask(with: baseURL) { (data, response, error) in
          if let error = error {
            completion(.failure(error))
            return
          }
          
          guard let data = data else {
            let error = NSError(
              domain: "",
              code: 0,
              userInfo: [NSLocalizedDescriptionKey: "서버에서 데이터를 받지 못했습니다."]
            )
            completion(.failure(error))
            return
          }
          
          do {
            let decoder = JSONDecoder()
            let todos = try decoder.decode([Todo].self, from: data)
            completion(.success(todos))
          } catch {
            completion(.failure(error))
          }
        }.resume()
      }
    }

     

    저는 Service 그룹을 별도로 만들고 거기에 서비스 파일을 생성했습니다.

    프로토콜을 만들어서 해당 서비스에서 구현해야할 메서드들을 정의합니다.

    Todo를 업데이트하기 위해 Result 타입으로 컴플리션으로 파라미터를 지정했습니다.

     

    그리고 실제 TodoNetworkService 구조체를 만들면서 해당 프로토콜을 채택하고 baseURL을 사용하실 URL로 지정해줘요!

     

    그리고 getTodos 메서드 구현을 합니다.

     

    URLSession 통신 및 디코딩은 해당 파트에서 다룰 필요가 없기에 패스하겠습니다 😉

     

    아..! Todo 모델 타입은 기존 뿌셔보기 2탄에서는 Decodable하지 않았는데 이제는 디코딩을 위해 채택하도록 추가해야 합니다~

     

    그럼 이어서 해당 서비스를 Interactor에 녹여봐야겠죠?


    Interactor 리팩토링 하기

    import Foundation
    
    protocol TodoListInteractorInput: AnyObject {
      func fetchTodos(completion: @escaping (Result<[Todo], Error>) -> Void)
    }
    
    class TodoListInteractor: TodoListInteractorInput {
      private let todoNetworkService: TodoNetworkServiceProtocol
      
      init(todoNetworkService: TodoNetworkServiceProtocol) {
        self.todoNetworkService = todoNetworkService
      }
    
      func fetchTodos(completion: @escaping (Result<[Todo], Error>) -> Void) {
        todoNetworkService.getTodos(completion: completion)
      }
    }

     

    기존 Interactor 구현에서 리팩토링을 했습니다.

    보시면 프로토콜을 통해 fetchTodos가 기존에는 [Todo] 타입을 반환했는데 이제는 컴플리션 핸들러로 다룰것이기에 위와 같이 이스케이핑 클로저로 변경하여 파라미터를 담아줍니다.

     

    그리고 TodoListInteractor 클래스를 리팩토링 해줍니다.

    먼저 네트워킹을 위해서 todoNetworkService 프로퍼티를 선언하고 초기화 시 주입받도록 합니다.

    그리고 fetchTodos 메서드 구현에서 위 서비스의 getTodos 메서드를 호출하여 completion을 담아주면 됩니다.

     

    여기까지 된다면 Interactor에서 해야할 간단한일이 끝났어요.

     

    정리하면 Interactor는 모델 업데이트하여 Presenter에 알려주기 위해 실제 네트워크 통신인 비지니스 로직을 호출하고 처리해줍니다.

     

    그리고 이걸 이제 Presenter에서 사용하기만 하면 됩니다.

     

    그럼 이어서 해당 Interactor의 fetchTodos를 호출하는 부분인 Presenter도 리팩토링을 해줘야 합니다 🙋🏻

     


    Presenter 리팩토링 하기

    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() {
        interactor.fetchTodos { [weak self] result in
          DispatchQueue.main.async {
            switch result {
            case .success(let fetchedTodos):
              self?.todos = fetchedTodos
            case .failure(let error):
              // Handle Error
              print("Error fetching todos: \(error.localizedDescription)")
            }
          }
        }
      }
      
      func didSelectedTodo(_ todo: Todo) {
        router.moveToSelectedTodoDetail(todo: todo)
      }
      
      func didFinishDisplayTodoDetail() {
        router.closeSelectedTodoDetail()
      }
    }

     

    위 코드에서 변경되는 메서드 부분만 보시면 됩니다.

    기존에는 todos에 fetchTodos 메서드 호출로 리턴되온 [Todo] 값을 담아주면 되었었는데 이제는 Result 타입의 컴플리션 핸들러로 동작합니다.

     

    즉, 에러 처리를 더 잘 다룰 수 있죠.

    그래서 Presenter의 fetchTodos 메서드가 호출되면 interactor의 fetchTodos 메서드를 호출합니다.

    여기서 클로저로 네트워킹 후 결과 값을 가지고 todos를 패치해줄지 에러를 다뤄줄지 적절히 구현해줍니다.

     

    이러면 끝!

     

    그럼 마지막으로, Interactor의 networkService를 최상단에서 주입하로 가볼까요?

     


    최상단 App 구조체에서 networkService 주입하기

    import SwiftUI
    
    @main
    struct TodoListApp: App {
      var body: some Scene {
        WindowGroup {
          TodoListView(
            presenter: TodoListPresenter(
              interactor: TodoListInteractor(todoNetworkService: TodoNetworkService()),
              router: TodoListRouter()
            )
          )
        }
      }
    }

     

    우리는 Interactor를 초기화 시 todoNetworkService 인스턴스를 받아와야됨을 위에서 확인했어요.

    그렇기에 어디선가부터 주입하여 내려와야 되겠죠?

     

    즉, 현재 구조에서 최상단인 TodoListApp에서 TodoListView 초기화 시 interactor 인스턴스가 필요합니다.

    그렇기에 해당 interactor 인스턴스를 만들때 엮여서 todoNetworkService 인스턴스가 필요하죠 🙋🏻

    저는 요기서 생성하여 주입해줬습니다.

     

    꼭 여기서 해야하는것만은 아닙니다.
    적절하게 그 구조에 맞게 주입시켜주면 좋습니다 😄


    마무리

    해당 뿌셔보기 2탄에서 뼈대로 만들어진 프로젝트에 리팩토링을 가미한것입니다.

    그렇기에 아래 동일한 레포에서 확인하실 수 있습니다!

    만약 2탄으로 보고 싶으시면 최신 커밋 바로 이전 커밋으로 돌려서 확인하셔도 됩니다 🙋🏻

    https://github.com/GREENOVER/playground/tree/main/TodoList

     


    다음엔 뭐?

    음... 아직 생각안해봤는데 VIPER를 이정도만 해봐도 충분할지 이제 다른 아키텍쳐 학습으로 넘어갈지 고민해보고 돌아오겠습니다 🙋🏻

    아마도 VIPER를 아주 간단히 사용해보면서 느낀 장단점들을 정리해볼 수도 있지 않을까해요!

    'VIPER' 카테고리의 다른 글

    VIPER 뿌셔보기 (2)  (16) 2023.08.07
    VIPER 뿌셔보기 (1)  (10) 2023.08.02
Designed by Tistory.