Responder Chain은 터치, 키 누름 및 제스처 이벤트를 UIResponder 객체 계층을 통해 순차적으로 전달하여 그중 하나가 이벤트를 처리할 때까지 전달하는 iOS 메커니즘입니다. 체인은 이벤트를 감지한 객체에서 시작하여 계층을 위로 이동합니다: 뷰에서 슈퍼뷰, 그다음 뷰 컨트롤러, 윈도우, 그리고 최종적으로 UIApplication까지 이동합니다. Apple Developer Documentation (2026)에 따르면, 이 패턴은 인터페이스 구성 요소 간에 이벤트 처리 책임을 분리하여 특정 핸들러에 대한 강한 결합 없이 유연성을 제공합니다.
핵심 사항
Responder Chain은 iOS가 터치, 키 누름, 가속도계 데이터와 같은 입력 이벤트를 전달하는 UIResponder 객체의 시퀀스입니다. 이 체인의 각 객체는 이벤트를 처리하거나 next 속성을 통해 다음 리스폰더 객체로 전달할 수 있습니다.
메커니즘은 뷰 계층을 기반으로 합니다: 터치가 발생하면 iOS는 먼저 어떤 뷰가 터치되었는지(hit-testing을 통해) 확인하고 해당 뷰에서 시작하여 UIApplication까지 체인을 생성합니다. UIApplication은 체인의 마지막 링크입니다 — 이벤트가 도달했지만 처리되지 않으면 단순히 폐기됩니다.
Apple에 따르면, 이 패턴은 이벤트 처리 로직의 캡슐화에 중요합니다. 개발자는 계층의 다른 요소에 영향을 주지 않고 특정 뷰의 동작을 재정의할 수 있습니다. 예를 들어, UITextField는 포커스를 받으면 퍼스트 리스폰더가 되어 부모 UIViewController의 변경 없이 키보드 이벤트를 수신합니다.
이벤트 처리는 Responder Chain을 통해 두 단계로 이루어집니다: 먼저 iOS는 어떤 뷰가 이벤트를 수신했는지 확인하고(hit-testing), 그다음 리스폰더 체인을 실행하여 처리합니다. 객체가 해당 메서드를 구현하지 않으면 이벤트는 더 전달됩니다.
체인은 현재 퍼스트 리스폰더와 뷰 계층을 기반으로 동적으로 형성됩니다. 표준 순서는 다음과 같습니다: 퍼스트 리스폰더 → 해당 뷰 → 슈퍼뷰 → UIViewController → 루트 뷰 → UIWindow → UIApplication. 이러한 객체 중 하나가 예를 들어 touchesBegan을 구현하면 이벤트는 해당 수준에서 처리되고 더 이상 전달되지 않습니다.
import UIKit
class CustomView: UIView {
override func touchesBegan(
_ touches: Set<UITouch>,
with event: UIEvent?
) {
// 이 뷰 수준에서 터치 처리됨
print("CustomView가 터치를 처리함")
// 체인을 따라 이벤트 전달
super.touchesBegan(touches, with: event)
}
}
핵심 기능은 super.touchesBegan 호출이 필수가 아니라는 점입니다. 호출하지 않으면 이벤트는 현재 수준에서만 처리되고 Responder Chain을 따라 더 진행되지 않습니다. 이를 통해 개발자는 처리에 참여할 객체를 완전히 제어할 수 있습니다.
객체가 이벤트를 수신했지만 처리하지 않는 경우(메서드를 재정의하지 않는 경우), iOS는 자동으로 next 속성을 통해 다음 리스폰더 객체로 이벤트를 전달합니다. 이 속성은 Responder Chain이라고 하는 단일 연결 리스트를 형성합니다.
UIViewController는 해당 뷰와 UIWindow 사이에 위치합니다: 뷰가 이벤트를 처리하지 않으면 컨트롤러가 처리할 기회를 얻습니다. 이는 사용자의 손가락 아래에 어떤 뷰가 있든 상관없이 전체 장면에서 작동해야 하는 제스처 처리와 같은 공통 로직에 특히 유용합니다.
Hit-Testing은 iOS가 터치 지점 아래에 어떤 뷰가 있는지 확인하는 프로세스입니다. hitTest:withEvent: 메서드는 UIWindow에서 아래쪽으로 뷰 계층을 탐색하며 어떤 하위 뷰가 터치 지점을 포함하고 숨겨져 있지 않은지 확인합니다.
알고리즘은 재귀적으로 작동합니다: 각 수준에서 iOS는 추가 순서의 역순으로 뷰를 확인합니다(맨 위부터). 뷰가 숨겨져 있지 않고, 투명하지 않으며, 지점이 경계 내에 있으면 모든 하위 뷰에 대해 재귀적으로 hitTest가 실행됩니다. 모든 조건을 충족하는 가장 깊은 뷰가 히트 테스트 뷰가 되어 Responder Chain의 첫 번째 객체가 됩니다.
override func hitTest(
_ point: CGPoint,
with event: UIEvent?
) -> UIView? {
if isUserInteractionEnabled &&
isHidden == false &&
alpha > 0.01 &&
point(inside: point, with: event) {
return super.hitTest(point, with: event)
}
return nil
}
개발자는 hitTest를 재정의하여 기본 동작을 변경할 수 있습니다. 예를 들어, 작은 버튼의 터치 영역을 확장하거나 물리적으로 손가락 아래에 있지 않은 다른 뷰로 이벤트를 리디렉션할 수 있습니다. 이는 사용자 정의 대화형 요소를 만들기 위한 강력한 도구입니다.
UIResponder는 iOS에서 이벤트를 처리할 수 있는 모든 객체의 기본 클래스입니다. UIView, UIViewController, UIApplication 및 UIWindow는 UIResponder를 상속합니다. 이 클래스는 다양한 유형의 이벤트를 처리하기 위해 재정의할 수 있는 메서드 집합을 제공합니다.
주요 메서드 그룹에는 터치용 touchesBegan, touchesMoved, touchesEnded, touchesCancelled; 물리적 버튼용 pressesBegan, pressesEnded; 가속도계 이벤트용 motionBegan, motionEnded가 포함됩니다. 각 메서드는 UITouch 또는 UIPress 객체 집합과 이벤트에 대한 추가 메타데이터를 포함하는 UIEvent에 대한 참조를 받습니다.
| UIResponder 메서드 | 목적 |
|---|---|
| touchesBegan | 터치 시작 시 호출됩니다 |
| touchesMoved | 손가락 이동 시 호출됩니다 |
| touchesEnded | 손가락 뗄 때 호출됩니다 |
| touchesCancelled | 중단 시 호출됩니다(전화, 제어 센터 스와이프) |
| pressesBegan | 물리적 버튼 누를 때 호출됩니다 |
iOS는 이러한 메서드를 퍼스트 리스폰더와 체인의 후속 객체에 대해서만 호출한다는 점을 이해하는 것이 중요합니다. 어떤 객체도 메서드를 재정의하지 않은 경우 이벤트는 오류를 생성하지 않고 무시됩니다. 이벤트 처리 디버깅을 위해 UIResponder touchEvent에 기호 중단점을 사용하십시오.
표준 UIKit 구성 요소는 작동을 위해 Responder Chain을 적극적으로 사용합니다. UITextField는 포커스를 받으면 퍼스트 리스폰더가 되어 자동으로 키보드를 엽니다. UIButton은 UIControl 메커니즘을 통해 터치를 처리하며, 이 역시 리스폰더 체인에 의존합니다.
UITableView 및 UICollectionView는 셀 선택 및 스크롤 제스처 처리를 위해 리스폰더 체인을 사용합니다. 사용자가 셀을 터치하면 이벤트는 먼저 셀 자체에 도달한 다음 UITableView, 그리고 마지막으로 UIViewController에 도달합니다. UIGestureRecognizer는 touchesBegan보다 높은 우선순위를 가지며, 뷰에 제스처 인식기가 추가되면 먼저 이벤트를 수신합니다.
Apple에 따르면, Responder Chain의 올바른 사용은 앱 접근성에 중요합니다. VoiceOver 및 기타 보조 기술은 인터페이스 요소 간 탐색을 위해 리스폰더 체인을 사용합니다. 체인이 끊어지면 장애가 있는 사용자가 애플리케이션과 상호 작용할 수 없게 됩니다.
UIMenuController는 컨텍스트 메뉴 표시에도 Responder Chain을 사용합니다. 사용자가 메뉴를 호출하면 시스템은 canPerformAction 및 해당 액션 메서드를 구현하는 퍼스트 리스폰더를 찾습니다. 메뉴는 현재 리스폰더가 지원하는 액션에 대해서만 표시됩니다.
예를 들어, UITextField가 포커스되어 있을 때만 잘라내기, 복사, 붙여넣기 명령을 표시하고 UILabel 작업 시에는 숨길 수 있습니다. 개발자는 UIResponder 하위 클래스에서 구현하고 canPerformAction에서 true를 반환하여 컨텍스트 메뉴에 사용자 정의 액션을 추가할 수 있습니다.
사용자 정의 리스폰더 객체를 생성하면 개발자에게 이벤트 처리에 대한 완전한 제어권이 주어집니다. 이를 위해서는 UIResponder(또는 UIView/UIViewController)의 하위 클래스를 만들고 필요한 이벤트 처리 메서드를 재정의해야 합니다.
사용자 정의 리스폰더 객체는 표준 UIGestureRecognizer로 처리되지 않는 특정 제스처를 처리하는 데 자주 사용됩니다. 예를 들어, 도형 그리기 인식, 복잡한 멀티터치 조합 또는 독점 입력 패턴 등이 있습니다. 사용자 정의 리스폰더는 여러 손가락의 이벤트를 집계하고 그 조합에 따라 결정을 내릴 수 있습니다.
class DrawingResponder: UIResponder {
private var activeTouches: [UITouch: CGPoint] = [:]
override func touchesBegan(
_ touches: Set<UITouch>,
with event: UIEvent?
) {
for touch in touches {
activeTouches[touch] = touch.location(in: self)
}
}
override func touchesMoved(
_ touches: Set<UITouch>,
with event: UIEvent?
) {
for touch in touches {
let currentPoint = touch.location(in: self)
activeTouches[touch] = currentPoint
drawLine(from: activeTouches[touch]!, to: currentPoint)
}
}
}
사용자 정의 리스폰더를 만들 때는 next 체인을 올바르게 구성하는 것이 중요합니다. 객체가 표준 UIKit 계층의 일부가 아닌 경우, 어떤 객체가 다음 리스폰더가 될지 명시적으로 지정해야 합니다. 이를 통해 처리되지 않은 이벤트가 Responder Chain을 따라 계속 이동할 수 있습니다.
자주 묻는 질문
Responder Chain은 iOS가 터치, 누름 및 제스처 이벤트를 순차적으로 전달하는 UIResponder 객체의 계층적 체인입니다. 객체가 이벤트를 처리하지 않으면 체인의 다음 리스폰더 객체로 UIApplication까지 전달됩니다.
UIResponder 객체의 next 속성을 재정의하여 순서를 변경할 수 있습니다. 기본 객체 대신 다른 객체를 반환하면 처리되지 않은 이벤트가 해당 객체로 리디렉션됩니다. 이는 사용자 정의 컨테이너가 여러 하위 컨트롤러를 관리하는 경우와 같은 비표준 계층에 유용합니다.
Hit-Testing은 터치 지점 아래의 뷰(초기 수신자)를 결정하고, Responder Chain은 hit-test 후 객체 간에 이벤트가 전달되는 방식을 결정합니다. Hit-test는 첫 번째 객체를 찾고, 해당 객체가 이벤트를 처리하지 않으면 리스폰더 체인이 추가 라우팅을 제공합니다.
체인을 중단하려면 UIResponder에서 이벤트를 처리하고 super를 호출하지 않으면 됩니다. 예를 들어, touchesBegan을 재정의하고 super.touchesBegan을 호출하지 않으면 이벤트가 더 전달되는 것을 방지합니다. 이벤트는 현재 수준에서 처리되고 체인의 다음 링크에 도달하지 않습니다.
가장 일반적인 이유: isUserInteractionEnabled가 false로 설정됨, 뷰가 숨겨짐(isHidden = true), alpha가 0.01 미만, 또는 뷰가 부모 컨테이너의 경계를 벗어남. 또한 뷰나 해당 슈퍼뷰에 touchesBegan 전에 이벤트를 가로채는 UIGestureRecognizer가 없는지 확인하십시오.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.