Skip to content

Conversation

@Rudy-009
Copy link
Collaborator

@Rudy-009 Rudy-009 commented Dec 17, 2025

1. 화면 영상

화면 홈화면 검색화면
기능 무한 스크롤링 검색 + 무한 스크롤링
영상 Home Search

2. URLSession.shared.dataTaskPublisher

주어진 URL에 대한 URLSession 응답을 개시하는 Publisher를 반환한다. (URLRequest도 사용가능하다. 좀 더, 복잡한 RestAPI 에 사용)

(data: Data, response: URLResponse) Element의 형태

URLSession.shared.dataTaskPublisher(for: URL)
	.map(\.data)
	.decode(type: ResponseType.self, decoder: JSONDecoder())
	.sink(receiveCompletion: { completion in
			switch completion {
			case .failure(let error):
			case .finished:
	}, receiveValue: { [weak self] response in
			// response 데이터 처리
	})

map

업스트림 퍼블리셔를 제공된 클로저를 이용해서 변환(가공)한다.

keyPath를 이용하여, data 부분만을 반환한다.

3. Throttling / Debouncing

Throttling

일정 시간 동안 이벤트를 한 번만 실행되도록 제어하는 것

주로, 무한 스크롤에서 사용된다. 스크롤 위치가 여러번 감지 되었을 때, GET 호출을 제어해야 한다. 그렇지 않으면 매우 많은 호출이 발생할 수 있다.

Debouncing

연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것

검색 시, 키보드 입력 이후, 일정 시간이 지날 때, 검색이 되도록 만들 수 있다.

4. Combine에서 Throttling / Debouncing

Debouncing을 이용한 검색

searchView.searchBar.textDidChangePublisher()
		.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
		.sink { [weak self] keyword in
		    // TextField 동작
		}

검색 TextField의 입력 수정 Publisher에서 입력 변화에 대해 바로 API를 호출하는 것이 아닌, debouncing을 통해 API 호출을 제안할 수 있다.

Throttling

private let scrollEventSubject = PassthroughSubject<Void, Never>()

scrollEventSubject
    .throttle(for: .seconds(0.3), scheduler: RunLoop.main, latest: false)
    .sink { [weak self] _ in
        // 무한 스크롤 동작
    }
    .store(in: &cancellables)

extension ViewController: UICollectionViewDelegate {
		func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = scrollView.contentOffset.y
        let contentHeight = scrollView.contentSize.height
        let height = scrollView.frame.size.height
        
        if offsetY > contentHeight - height - 100 {
            scrollEventSubject.send()
        }
    }
}

5. Input/Output 패턴과 맞는가?

호출 횟수 제한은 ViewController와 ViewModel 둘 중 어디에 책임이 있는지 생각해보면 ViewModel가 더 맞다고 생각한다. 하지만, 아직 Combine에 대한 이해가 부족하여, 부득이하게 ViewController에서 구현했다.

원인 input 분리에 대한 낮은 이해도가 원인이었다.

flatMap

flatMap이란? 업스트림의 요소들에 개별적으로 접근하여 새로운 퍼블리셔로 전환하다. 이 부분에서 막혔다.

.flatMap { input -> AnyPublisher<Input, Never> in
    switch input {
    case .hitBottom:
        return Just(input)
            .debounce(for: .seconds(0.3), scheduler: RunLoop.main)
            .eraseToAnyPublisher()
    case .search:
		    return Just(input)
						.throttle(for: .seconds(0.3), scheduler: RunLoop.main, latest: false)
            .eraseToAnyPublisher()
    case .viewDidLoad:
        return Just(input).eraseToAnyPublisher()
    }
}

debounce가 적용이 안된다…. 왜 그럴까…

제미나이 왈) "매번 새로운 스트림이 생성됨"

flatMap 내부에서 Just(input)를 생성하는 순간, 오직 값 하나만 들어있는 새로운 파이프라인이 만들어집니다.

  • 원래 의도: "0.3초 동안 여러 번 들어오는 신호를 묶어서 하나만 보낼래 (Throttle)"
  • 현재 코드의 동작:
    1. 신호가 들어옴 -> flatMap이 실행됨.
    2. 방금 들어온 그 신호 딱 하나만 가진 새로운 Just 통로를 만듦.
    3. 그 통로에 throttle을 걸었지만, 어차피 데이터가 하나뿐이라 기다릴 것 없이 즉시 통과됨.
    4. 다음 신호가 들어오면 또 별개의 새로운 통로를 만듦.

“결과적으로 모든 신호가 각각의 "1인용 통로"를 타고 그대로 통과되어 버립니다.”

@Rudy-009 Rudy-009 changed the title Leeseungjun [컴바인 과제 - 이승준] Dec 17, 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.

굿입니다

Copy link
Collaborator

Choose a reason for hiding this comment

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

이거 이그노어처리해주세요!

}

func bindViewModel() {
let output = viewModel.transform(input: inputSubject.eraseToAnyPublisher())
Copy link
Collaborator

Choose a reason for hiding this comment

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

뷰모델에서 eraseToAnyPublisher를 처리해서 뷰컨으로 보내주는 방법도 있을 것 같아요!

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