핵심 포인트
Pull-to-Refresh는 사용자가 목록이나 스크롤 가능한 영역을 아래로 당겨(pull down) 콘텐츠를 새로고침하는 사용자 인터페이스 패턴입니다. 시각적으로 제스처에는 화면 상단에 나타나는 로딩 표시기(스피너)가 수반되며 데이터를 수신하면 사라집니다. 이 패턴은 iPhone용 Tweetie 앱(2008)에 의해 대중화되었으며 이후 Apple(iOS 6 — UIRefreshControl)과 Google(Android Support Library — SwipeRefreshLayout)에 의해 표준화되었습니다.
기술적 관점에서 Pull-to-Refresh는 패닝(손가락 변위 추적)과 임계값 도달 시 트리거의 조합입니다. 사용자는 저항(저항성 오버스크롤)을 극복하며 목록을 아래로 당기고, 임계값(iOS에서 ~80px, Android에서 ~64dp)을 초과하면 표시기 애니메이션과 비동기 로딩이 시작됩니다. 사용자가 임계값 전에 손가락을 떼면 목록은 새로고침 없이 원래 위치로 돌아갑니다.
Material Design 가이드라인에 따르면, Pull-to-Refresh는 탐색이나 탭 전환에 사용해서는 안 됩니다 — 유일한 목적은 데이터 새로고침입니다. IT Sectr에서는 데이터의 신선도가 사용자 경험에 중요한 뉴스 피드, 주문 피드 및 채팅에서 Pull-to-Refresh를 사용합니다.
UIRefreshControl은 Pull-to-Refresh용 표준 iOS 컨트롤로, iOS 6부터 사용 가능합니다. UIRefreshControl은 refreshControl 속성(iOS 10+)을 통해 UITableViewController에 추가되거나 이전 버전에서는 테이블의 하위 뷰로 추가됩니다. 사용자 지정 가능한 색상(tintColor), title 속성 및 레이블이 있는 속성 문자열(예: "업데이트 중...")이 포함된 내장 스피너가 있습니다.
UIRefreshControl은 target-action 메커니즘을 통해 작동합니다: 제스처가 활성화되면 지정된 메서드(예: refresh(_:))가 호출됩니다. 메서드 내에서 비동기 데이터 로딩이 수행됩니다. 완료 후 endRefreshing()이 호출되어 애니메이션과 함께 표시기를 숨깁니다. UIRefreshControl은 자동으로 제스처 감도를 관리합니다 — 테이블이 최상단 위치에 있을 때만 트리거됩니다(contentOffset.y <= 0).
tintColor 속성은 스피너 색상을 설정합니다. attributedTitle을 사용하면 완료 후 "2분 전 업데이트됨"과 같은 텍스트를 표시할 수 있습니다. iOS 10부터 UIRefreshControl은 UIActivityIndicatorView 또는 영구 사용자 지정 뷰를 통한 사용자 지정 애니메이션을 지원합니다. IT Sectr에서는 브랜드에 맞게 tintColor를 구성하고 attributedTitle을 통해 마지막 업데이트 시간을 표시합니다 — 이를 통해 사용자의 데이터 신뢰도가 높아집니다.
SwipeRefreshLayout은 Android Support Library(androidx.swiperefreshlayout)의 ViewGroup으로, 스크롤 가능한 콘텐츠(RecyclerView, NestedScrollView, ListView)를 래핑하고 Pull-to-Refresh 기능을 추가합니다. UIRefreshControl(컨트롤이지 컨테이너가 아님)과 달리 SwipeRefreshLayout은 자식의 터치 이벤트를 가로채고 임계값을 초과하면 새로고침 표시기를 트리거하는 컨테이너입니다.
SwipeRefreshLayout은 setColorSchemeColors()를 통한 색상 사용자 지정이 가능한 원형 Material Design 진행 표시기를 사용합니다. setOnRefreshListener 메서드는 onRefresh() 콜백을 설정하며, 그 안에서 비동기 로딩이 수행됩니다. 완료 후 setRefreshing(false)가 호출되어 표시기를 숨깁니다. 중요: setRefreshing(true)는 onRefresh()를 다시 호출합니다 — 따라서 프로그래밍 방식으로 새로고침을 시작하려면 플래그 또는 post 메서드를 사용하세요.
setProgressBackgroundColorSchemeResource 속성은 표시기의 배경을 변경합니다. setSize(SwipeRefreshLayout.LARGE)는 스피너 크기를 설정합니다. XML 레이아웃에서 SwipeRefreshLayout은 RecyclerView를 래핑합니다: swipe_refresh_layout → recycler_view. Google I/O 2024에 따르면, SwipeRefreshLayout은 콘텐츠 피드가 있는 Android 앱의 85%에서 사용됩니다. IT Sectr에서는 비동기적으로 로드된 목록이 있는 모든 화면을 SwipeRefreshLayout으로 래핑합니다 — 이를 통해 모든 Android 버전에서 일관된 UX를 제공합니다.
Android 12(Material You)부터 Google은 material-1.6.0+ 라이브러리(Compose의 경우 androidx.compose.material3.pulltorefresh)의 새로운 Material Pull-to-Refresh 사용을 권장합니다. 새 API는 스프링 애니메이션 지원과 배경화면 기반 적응형 색상을 갖춘 애니메이션 표시기를 사용합니다. SwipeRefreshLayout은 Android 12 미만 버전에서도 호환됩니다.
Pull-to-Refresh는 구현하기 쉬운 패턴이지만 UX를 저하시키는 몇 가지 일반적인 실수가 있습니다. 이를 살펴보고 방지 방법을 알아보겠습니다.
IT Sectr에서는 테스트 서버 로그에서 중복 요청을 발견한 후 모든 프로젝트에 isRefreshing 확인을 추가했습니다 — 손가락이 빠른 사용자가 연속으로 최대 3번까지 새로고침을 트리거하고 있었습니다.
사용자 지정 스피너 색상 및 속성 제목으로 UITableViewController에 Pull-to-Refresh를 추가합니다. 데이터 로딩 후 표시기가 숨겨집니다.
import UIKit
class FeedTableViewController: UITableViewController {
private var items: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.refreshControl = UIRefreshControl()
refreshControl?.tintColor = .systemBlue
refreshControl?.attributedTitle = NSAttributedString(
string: "당겨서 새로고침"
)
refreshControl?.addTarget(
self,
action: #selector(refreshData),
for: .valueChanged
)
}
@objc private func refreshData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
self.items = FeedService().fetchLatest()
self.tableView.reloadData()
self.refreshControl?.endRefreshing()
}
}
}
tableView.refreshControl 속성(iOS 10+)이 UIRefreshControl을 설정합니다. .valueChanged 이벤트가 있는 addTarget은 제스처가 활성화될 때 발생합니다. endRefreshing()은 필수입니다 — 없으면 표시기가 무한히 회전합니다. 비동기 로딩은 DispatchQueue.main.asyncAfter로 시뮬레이션됩니다 — 실제 프로젝트에서는 URLSession 또는 async/await를 사용하세요.
사용자 지정 표시기 색상으로 SwipeRefreshLayout에 RecyclerView를 래핑합니다. onRefresh가 로딩을 시작하고 완료 후 표시기를 숨깁니다.
class FeedFragment : Fragment() {
private var _binding: FragmentFeedBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentFeedBinding.inflate(inflater, container, false)
binding.swipeRefreshLayout.setColorSchemeColors(
resources.getColor(R.color.brand_blue, null),
resources.getColor(R.color.brand_green, null)
)
binding.swipeRefreshLayout.setOnRefreshListener {
loadData()
}
return binding.root
}
private fun loadData() {
viewModelScope.launch {
try {
val result = repository.getLatestFeed()
adapter.submitList(result)
} finally {
binding.swipeRefreshLayout.isRefreshing = false
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
setColorSchemeColors는 회전하는 Material Design 표시기의 색상을 설정합니다. isRefreshing = false는 finally 블록에서 호출되어 로딩 오류 시에도 표시기를 숨깁니다. ViewModelScope.launch는 프래그먼트 수명 주기 내에서 코루틴을 실행합니다 — 프래그먼트가 소멸되면 코루틴이 자동으로 취소되어 메모리 누수를 방지합니다.
최신 SwiftUI는 .refreshable 수정자를 제공하여 List 또는 ScrollView에 자동으로 Pull-to-Refresh를 추가합니다.
import SwiftUI
struct FeedView: View {
@State private var items: [String] = []
var body: some View {
List(items, id: \.self) { item in
Text(item)
}
.refreshable {
items = await FeedService().fetchLatestAsync()
}
}
}
.refreshable 수정자는 Pull-to-Refresh 시 실행되는 async 클로저를 받습니다. SwiftUI는 자동으로 새로고침 표시기를 표시하고 숨기며, 상태 경쟁을 관리하고(현재 새로고침이 완료될 때까지 새로고침을 시작하지 않음), 플랫폼에 맞게 애니메이션을 조정합니다. iOS 15+의 경우 SwiftUI에서 Pull-to-Refresh를 구현하는 권장 방법입니다.
자주 묻는 질문
네, SwiftUI는 List 또는 ScrollView용 .refreshable 수정자를 제공하며 iOS 15부터 사용 가능합니다. 클로저 내에서 비동기 데이터 로딩 코드가 실행됩니다. SwiftUI는 자동으로 새로고침 표시기를 관리하고 현재 로딩이 완료될 때까지 반복 트리거를 차단합니다 — 이는 새 프로젝트의 표준 권장 방식입니다.
isRefreshing 플래그를 사용하세요: 로딩 시작 시 true로 설정하고 완료 후 false로 설정합니다. iOS에서는 endRefreshing()이 호출될 때까지 UIRefreshControl이 자동으로 반복 호출을 차단합니다. Android에서는 onRefresh() 시작 시 SwipeRefreshLayout.isRefreshing을 확인합니다: true이면 return합니다. 이렇게 하면 제스처당 하나의 요청이 보장됩니다.
UIRefreshControl과 SwipeRefreshLayout은 목록이 최상단 위치에 있을 때만 트리거됩니다(contentOffset == 0). 아키텍처가 충돌을 제거합니다: 목록이 1px이라도 스크롤된 동안에는 Pull-to-Refresh 제스처가 활성화되지 않습니다. 충돌이 발생하면 Android의 nestedScrollingEnabled 또는 터치를 가로채는 사용자 지정 GestureRecognizer의 존재를 확인하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.