티스토리 뷰

데일리 스크럼

어제 한 일

  • 플로팅 버튼 이미지 변경 및 사이즈 조절
  • 중복 함수 정리
  • Run 뷰 구현 완료

오늘 할 일

  • 올라온 PR확인
  • 이슈 사항 정리
  • 맵킷 데이터 연결
    • 현재 위치
    • 지나온 위치
    • 현재~지나온 위치 경로 그리기
    • 사용자의 지도 상호작용
    • 지도 랜더링 변경
  • 위치 정보 받아오기
  • 날씨 정보 받아오기

Today

오늘은 어제까지 마무리한 뷰에 데이터를 연결하는 작업을 진행했다. 이번 프로젝트에서의 뷰모델 형식은

final class RunningViewModel {
    // MARK: - Input & Output
    enum Input {
    }
    enum Output {
    }
    let input = PassthroughSubject<Input, Never>()
    let output = CurrentValueSubject<Output?, Never>(nil)
    private var cancellables = Set<AnyCancellable>()
    // MARK: - Init
    init() {
        bind()
    }
    // MARK: - Bind (Input -> Output)
    private func bind() {
        input
            .receive(on: DispatchQueue.main)
            .sink { [weak self] event in
                switch event {
                }
            }
            .store(in: &cancellables)
    }
    // MARK: - private Functions
}

이런식으로 Input & Output을 enum의 형태로 감싸는 상태로 전달하는 방식을 채택했다. 새롭게 시도해보는 뷰모델 방식이라 사용방법부터 코드를 어떻게 짜야할지가 너무나 막막한 상태로 코드를 여러번 갈아엎었다.(매번 갈아 엎었대...) 그리고 컴바인을 실제로 적용한것이 처음이고 이전 컴바인 글에서 알 수 있듯이 나는 컴바인의 기초의 기초만 알고 있는 상태였다..

트러블 슈팅

MVVM 데이터 흐름의 최적화

앞서 말했듯이 처음 도전하는 형태와 컴바인의 조합은 나에게 매우 큰 시련으로 다가 왔다. 그래도 오늘 하루를 뷰모델에만 투자를 하니! 드디어 7시30분쯤이 되어 정상적으로 값을 넘기고 뷰를 업데이트 할 수 있었다. 시작은 10시였지만...(내 하루!!!!) 잘만들어졌다면 이 내용일 왜 트러블 슈팅에 있을까 정상적인 기능을 할뿐 코드가 너무 엉망이었다. 조금 정리를 해보자면

  1. LocationManager(이하 Manager)에서 사용자의 이동을 감지 didUpdateLocations 함수를 실행
  2. 감지한 위치를 runHomeViewModel(이하 viewModel)의 input으로 보냄
  3. viewModel의 bind 함수에서 내부 함수 fetchLocationData를 실행
  4. Location정보를 Manager로 보내 도시명이랑 날씨를 받아옴
  5. 받아온 정보를 viewModel의 output으로 RunHomeViewController(이하 ViewController)로 보냄
  6. ViewController에서 뷰를 업데이트

이 순서로 작동했다. 자세히 보면 Manager에서 ViewModel로 값을 보내고 다시 ViewModel에서 Manager로 값을 보내는 매우 비효율 적인 흐름을 가지고있다. 심지어 이 과정의 중간에는 받아온 Location정보를 디자인에 맞게 변경하는 과정이 추가되니 흐름은 더욱 복잡했다.

그래서 정리를 감행했다. (작성된 코드를 전부 주석처리하고 주섬주섬 꺼내서 정리했다) 첫순서로 데이터를 어디에서 어디로 보낼지 순서를 정하고 해당 순서에서 어디까지 처리할지 정했다. Manager -> ViewModel -> ViewController 순으로 데이터를 전달하고 Manager에서는 단순히 데이터를 받아오고 전달하는 과정을 담고 있는 것이 맞다고 판단하였다.

데이터를 가공하는 이유는 뷰를 업데이트 하기 위한 것일 것인데 그렇다면 이건 ViewModel이 처리하는게 맞다고 생각했다. 그래서 자연스럽게 ViewModel로 정보를 넘겨주고 ViewModel에서 ViewController에서 필요한 데이터의 형태로 바꿔서 전달하고 ViewController는 뷰를 업데이트 하는 부분을 진행했다. 그렇게 됨으로써 ViewModel은 뷰의 데이터를 가지고 ViewController는 뷰와 데이터를 연결해주는 연결통로가 되면서 MVVM패턴이 되었다!

 

앞에서와 같이 순서를 매겨보면

  1. Manager에서 사용자의 이동을 감지 didUpdateLocations 함수를 실행
  2. 감지한 위치에 대한 날씨와 도시명을 ViewModel의 input으로 보냄
  3. ViewModel에서 가공한 데이터를 ViewController의 output으로 보냄
  4. ViewController가 뷰를 업데이트

데이터 가공을 포함하고도 훨씬 짧은 흐름을 가지고 있는 것을 알 수 있다. 물론 코드도 그만큼 간단해 가독성도 좋아졌다.

알게된 내용

Publisher vs ViewModel Input

이 내용은 코드를 정리하는 과정에서의 코드에 대한 내용으로 두 방식에 대한 정리하는 글이다 (대충 작성해서 타입은 안맞을 수 있습니다)

// LocationManager.swift
private let locationSubject = PassthroughSubject<CLPlacemark, Never>()
private let weatherSubject = PassthroughSubject<WeatherData, Never>()
var locationPublisher: AnyPublisher<CLPlacemark, Never> {
	locationSubject.eraseToAnyPublisher()
}
var weatherPublisher: AnyPublisher<WeatherData, Never> {
	weatherSubject.eraseToAnyPublisher()
}
private let locationManager = LocationManager.shared
private func fetchCityName(location: CLLocation) {
	...
	self.locationSubject.send(placemark)
}
private func fetchWeatherData(location: CLLocation) {
	...
	self.weatherSubject.send(weatherData)
}

// RumHomeViewModel.swift
// 구독의 형태
locationManager.locationPublisher
    .receive(on: DispatchQueue.main)
    .sink { [weak self] placemark in
        print(placemark)
	    self?.output.send(.locationUpdate(placemark))
    }
    .store(in: &cancellables)
locationManager.weatherPublisher
    .receive(on: DispatchQueue.main)
    .sink { [weak self] weather in
        print(weather)
        self?.output.send(.weatherUpdate(placemark))
    }
    .store(in: &cancellables)

Publisher 장점

  • ViewModel이 Publisher를 구독하기 때문에 여러 곳에서 데이터를 사용할 수 있음 (다중 구독 가능)
  • LocationManager가 ViewModel을 직접 참조하지 않아 결합도 낮음
  • LocationManager는 데이터 제공 역할만 하므로 역할 분리 명확

Publisher 단점

  • ViewModel이 Publisher를 구독해야 하기 때문에 Combine 스트림을 이해하고 관리해야 함
  • ViewModel이 데이터 흐름을 완전히 제어하지 못함 (Publisher는 계속 값을 방출하기 때문)

 

// LocationManager.swift
var runHomeViewModel: RunHomeViewModel?
private func fetchCityName(location: CLLocation) {
	...
	runHomeViewModel?.input.send(.locationUpdate(placemark))
}

// RunHomeViewModel.swift
enum Input {
	case locationUpdate(CLPlacemark)
	case weatherUpdate(WeatherData)
}
input
	.receive(on: DispatchQueue.main)
	.sink { [weak self] input in
		switch input {
			case .locationUpdate(let placemark):
				print(placemark)
				self?.output.send(.locationUpdate(city))
			case .weatherUpdate(let weather):
				print(weather)
				self?.output.send(.weatherUpdate(weather))
		}
	}
	.store(in: &cancellables)

ViewModel Input 장점

  • ViewModel이 데이터 흐름을 100% 제어 가능 (데이터를 어디에서 받을지 직접 관리) 
  • LocationManager에서 ViewModel.input.send(...)를 호출하기 때문에 비즈니스 로직이 ViewModel에 집중됨 
  • 테스트하기 쉬움 (Mock ViewModel을 만들면 input.send(...) 테스트 가능)

ViewModel Input 단점

  • LocationManager가 ViewModel을 직접 참조하므로 결합도가 높아짐 
  • ViewModel 외부에서는 Publisher를 사용할 수 없으므로 다중 구독이 불가능함 
  • LocationManager가 ViewModel을 참조해야 하므로 유닛 테스트가 어려울 수 있음

 

GPT의 최종 선택 가이드 (알려줘 GPT!!!)

MVVM + Combine 사용

  • Publisher - (locationPublisher)

ViewModel이 모든 데이터 흐름을 제어해야 함

  • input.send(...) 방식

여러 곳에서 같은 데이터를 받아야 함

  • Publisher 방식

ViewModel과 Manager의 결합도를 낮추고 싶음

  • Publisher 방식

테스트를 쉽게 하고 싶음

  • input.send(...) 방식
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함