Skip to content

Conversation

@Yeonnies
Copy link
Collaborator

🍎 iOS Pull request

  • 컴바인 과제!

✅ 작업(과제) 내용

  • 영화진흥원으로 combine+mvvm 적용하기~~

💡 새로 알게 된 내용

  • 저번처럼 모든 ViewModel이 같은 패턴을 따르도록 ViewModelProtocol을 채택하였습니다.
protocol ViewModelProtocol {
    associatedtype Input
    associatedtype Output

    func transform(input: Input) -> Output
}
  • 저는 각각의 상태를 가지고 있게 하기 위해 Subject들을 CurrentValueSubject로 movieSubject(영화 목록 저장하는 녀석), isLoadingSubject(로딩 상태 저장하는 녀석), errorSubject(에러 메세지 저장하는 녀석)을 만들어줬습니다. 처음 만들때 초기값도 넣었어요.

  • transform에서 Input을 받아서 Output으로 반환해줍니다. viewWillAppear 이벤트를 구독해두고 뷰컨에서 viewWillAppear 이벤트가 일어날 때 API 호출하는 클로저를 실행해 줬습니다.

  • SubjectPublisher로 변환해서 반환시킵니다. 값이나 상태가 변할 때마다 ViewController가 자동으로 변한 값을 받습니다.

func transform(input: Input) -> Output {
        input.viewWillAppear
            .sink { [weak self] _ in
                self?.fetchMovies()
            }
            .store(in: &cancellables)
        
        return Output(
            movies: movieSubject.eraseToAnyPublisher(), isLoading: isLoadingSubject.eraseToAnyPublisher(), error: errorSubject.eraseToAnyPublisher()
            )
    }
  • fetchMovies()에서는 처음에는 isLoadingSubject.send(true), 즉 isLoading 구독자가 true를 받도록 하다가 API 호출 후 성공이나 실패를 반환할 때 값을 false로 바꿔줍니다.

  • 뷰컨에서는 InputSubjectviewWillAppearSubject를 만들어줬습니다. viewWillAppear할 경우 ViewModelInput으로 전달되어 fetchMovies가 실행됩니다.

override func viewWillAppear(_ animated: Bool) {
        viewWillAppearSubject.send(())
    }
  • 뷰모델의 transform을 실행시켜 Output을 받아요.
let output = viewModel.transform(input: input)
  • 하나만 설명해보면 output.movies는 메인 스레드에서 받고, movies 값이 올 때마다 이 클로저 실행하여 movies 값을 갱신하고 이에 따라 tableView의 데이터값도 갱신해요. 그리고 이 구독을 저장합니다.
output.movies
            .receive(on: DispatchQueue.main)
            .sink { [weak self] movies in
                self?.movies = movies
                self?.movieView.tableView.reloadData()
            }
            .store(in: &cancellables)

간단하게 흐름으로 나타내면 이렇습니다

사용자가 화면 진입 → viewWillAppear 호출 → viewWillAppearSubject 이벤트 발행 → ViewModel의 Input이 이벤트 받음 → transform 내부의 sink가 실행, fetchMovies() 호출 → isLoadingSubject가 Output.isLoading으로 전달, 뷰에서 로딩 인디케이터 실행 → API 호출 → 성공시 movieSubject가 Output.movies로 전달, 테이블뷰 갱신 → 인디케이터뷰 정지

왜 Subject에서 Publisher로, Publisher에서 Subject로? (헷갈리는 개념 정리)

  • 캡슐화를 위해!
    Output을 Subject 그대로 반환할 경우 ViewController가 직접 데이터 변경이 가능해지므로 위험합니다. 외부에서 값을 변경하지 못하도록 캡슐화해주기 위해서 Publisher로 반환해줍니다.

  • 단방향 데이터 흐름을 보장하기 위해!
    ViewModel → ViewController : 데이터 전달
    ViewController → ViewModel : 이벤트 전달

Publisher로 주면 ViewController에서는 구독 후 읽기만 가능하니까 사용합니다.

직접 사용해보니 흐름이 더 명확해져서 따로 정리해보았습니다!

📸 스크린샷

기능 로딩->화면
GIF

@Yeonnies Yeonnies requested a review from y-eonee December 16, 2025 12:36
@Yeonnies Yeonnies self-assigned this Dec 16, 2025
Copy link
Collaborator

@y-eonee y-eonee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

완벽하네요..

Comment on lines +68 to +77
output.isLoading
.receive(on: DispatchQueue.main)
.sink { isLoading in
if isLoading {
self.movieView.loadingIndicator.startAnimating()
self.movieView.tableView.alpha = 0.5
} else {
self.movieView.loadingIndicator.stopAnimating()
self.movieView.tableView.alpha = 1
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

대박

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants