1. 무한스크롤 기본 구현
scrollViewDidScroll 메서드에서 스크롤 위치를 체크하여 화면 끝에 도달할 경우 추가 데이터를 로드하도록 구현.
offsetY + height - contentHeight > threshold 조건을 통해 바닥 근처인지 판단.
- 중복 호출을 막기 위해
isLoading 플래그를 도입.
2. isLoading은 ViewModel 내부에서만 관리
- ViewController는
viewModel.fetchPokemonList()만 호출하도록 역할 분리.
- ViewModel 내에서만
isLoading을 true → false로 전환하며 중복 호출 방지.
3. ViewModel에서 데이터 누적 처리
allPokemonList 배열에 포켓몬 데이터를 누적 저장.
- 새로운 데이터가 도착할 때마다
pokemonListSubject.onNext(allPokemonList) 호출.
- ViewController에서는
pokemonList = pokemons로 교체만 하도록 수정.
- 기존 코드에서
append(contentsOf:)를 했던 부분은 이중 누적으로 인한 중복 데이터 발생의 원인이었음.
4. 스크롤을 오래 끌어당길 경우 중복 호출 이슈
scrollViewDidScroll이 계속 호출되면서 fetchPokemonList()가 반복 요청되는 문제 발생.
- 해결:
viewModel.isLoading을 활용해 조건 충족 + 아직 로딩 중이 아닐 때만 fetch 호출.
if !viewModel.isLoading {
viewModel.fetchPokemonList()
}
5. 비동기 이미지 로딩 이슈
- 셀의 재사용 시, 이전 셀에서 요청한 이미지가 나중에 나타나면서 다른 포켓몬 이미지가 잘못 들어가는 현상 발생.
- 해결 방법:
configure(id:) 내부에서 currentID를 저장하고, 이미지 로딩 완료 시점에 비교
- 또는
Kingfisher, SDWebImage 같은 이미지 캐싱 라이브러리 사용하여 간편하게 해결
킹피셔 쓰기 전
func configure(id: Int) {
let urlString = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(id).png"
guard let url = URL(string: urlString) else { return }
DispatchQueue.global().async { [weak self] in
if let data = try? Data(contentsOf: url) {
if let image = UIImage(data: data) {
DispatchQueue.main.sync {
self?.pokemonImageView.image = image
}
}
}
}
킹피셔 사용
func configure(id: Int) {
let url = URL(string: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/\(id).png")
pokemonImageView.kf.setImage(with: url)
}
완성된 앱 모습!
킹피셔를 적용한 모슴

포켓몬 상세 화면

📚 배운 점 정리
| 항목 | 내용 |
|---|
| ViewModel 책임 분리 | 로딩 상태 및 데이터 누적은 ViewModel에서 |
| 무한스크롤 조건 | offsetY + height - contentHeight > threshold |
| 이미지 셀 이슈 해결 | currentID로 비교 또는 캐싱 라이브러리 사용 |
| 중복 호출 방지 | isLoading으로 fetch 제어 |