문제 상황
iOS 앱에서 UI 인터랙션을 구현하다 보면, 값이 경계 근처에서 계속 튀는 현상을 자주 마주하게 된다.
예를 들어 스크롤 방향에 따라 View를 제어하는 다음과 같은 로직을 생각해 보자.
위로 스크롤 → View 숨김
아래로 스크롤 → View 표시
이 로직 자체는 단순하지만, 실제 사용 환경에서는 문제가 발생한다. 손가락의 위치가 경계값 근처에 머무르면 View가 보였다가 숨겨졌다가를 반복하며 UI가 불안정해 보이게 된다.
이럴 때 유용하게 사용할 수 있는 개념이 바로 Hysteresis Buffer다.
상태를 바꾸기 위한 조건을, 되돌리기 위한 조건보다 더 엄격하게 만드는 것"
문제 상황: 버퍼 없이 스크롤 방향을 판단하면
enum ScrollDirection {
case up
case down
}
final class ScrollDirectionDetector {
private var lastOffsetY: CGFloat = 0
func detectDirection(currentOffsetY: CGFloat) -> ScrollDirection {
defer { lastOffsetY = currentOffsetY }
if currentOffsetY > lastOffsetY {
return .down
} else {
return .up
}
}
}이 코드는 논리적으로는 전혀 문제가 없다. 이전 offset과 현재 offset을 비교해 스크롤 방향을 판단한다.
하지만 실제 스크롤 환경에서는 다음과 같은 요소들 때문에 문제가 발생한다.
손가락의 미세한 움직임
관성 스크롤로 인한 잔 떨림
스크롤의 bounce 구간
이로 인해 up ↔ down 상태가 프레임 단위로 계속 전환되며, View 역시 짧은 시간 안에 반복적으로 숨겨졌다 나타났다를 반복하게 된다.
해결 방안: Hysteresis Buffer 도입
해결 아이디어는 의외로 단순하다.
방향을 바꾸려면 일정 거리 이상 움직여야 한다
그 전까지는 기존 상태를 유지한다
이를 위해 방향 전환에 필요한 임계값(threshold) 을 두고, 이 값을 넘었을 때만 상태를 변경한다.
enum ScrollDirection {
case up
case down
}
final class ScrollDirectionHysteresisDetector {
private var lastOffsetY: CGFloat = 0
private var currentDirection: ScrollDirection = .down
/// 방향 전환에 필요한 최소 이동 거리
private let hysteresisBuffer: CGFloat = 12
func detectDirection(currentOffsetY: CGFloat) -> ScrollDirection {
let delta = currentOffsetY - lastOffsetY
switch currentDirection {
case .down:
// 위로 충분히 움직였을 때만 방향 전환
if delta < -hysteresisBuffer {
currentDirection = .up
}
case .up:
// 아래로 충분히 움직였을 때만 방향 전환
if delta > hysteresisBuffer {
currentDirection = .down
}
}
lastOffsetY = currentOffsetY
return currentDirection
}
}기존 방향을 기준으로 판단한다
반대 방향으로 의미 있는 이동이 발생해야 상태가 전환된다
작은 떨림이나 미세한 움직임에는 반응하지 않는다
그 결과, UI는 훨씬 안정적으로 동작하게 된다.
사실 Hysteresis Buffer는 거창한 패턴이라기 보다는, UI를 덜 예민하게 만들어 주는 작은 장치에 가깝다.
하지만 이 사소한 차이 하나로
UI가 훨씬 더 안정적으로 느껴지고,
사용자는 앱이 더 '잘 만들어졌다'고 인식하게 된다
스크롤, 드래그, 제스처처럼 사용자의 의도가 점진적으로 드러나는 인터랙션이라면, Hysteresis Buffer는 충분히 고려해볼 만한 선택지다.