Pinch-to-Zoom — 모바일 애플리케이션에서 핀치 제스처 이해하기

저자: IT Sectr 게시일: 2026-02-27 읽는 시간: 8 분
Pinch-to-Zoom(손가락 집기)은 사용자가 화면에서 두 손가락을 모으거나 벌려 콘텐츠를 축소하거나 확대하는 멀티터치 줌 제스처입니다. 이 제스처는 모바일 기기에서 이미지, 지도, 웹 페이지 및 문서를 보기 위한 사실상의 표준이 되었습니다. Apple HIG 및 Google Material Design에 따르면, Pinch-to-Zoom은 사용자가 시각적 콘텐츠를 다루는 모든 애플리케이션에서 기대하는 기본 제스처입니다.

핵심 포인트

  • Pinch-to-Zoom은 모바일 기기에서 콘텐츠를 확대/축소하기 위한 두 손가락 멀티터치 제스처입니다.
  • iOS에서는 .began → .changed → .ended의 연속적인 추적과 함께 UIPinchGestureRecognizer를 통해 처리가 수행됩니다.
  • Android는 Matrix를 통해 줌을 업데이트하기 위해 onScale 콜백과 함께 ScaleGestureDetector(Support Library)를 사용합니다.
  • UIPinchGestureRecognizer에는 scale(이전 상태에 대한 비율) 및 velocity(속도) 속성이 있습니다.
  • 줌을 제한하려면 지정된 최소/최대 값(일반적으로 0.5–3.0) 사이에서 스케일 팩터의 클램프가 적용됩니다.

Pinch-to-Zoom이란?

Pinch-to-Zoom(핀치)은 표시된 콘텐츠의 배율을 변경하기 위해 두 손가락을 사용하는 멀티터치 제스처입니다. 손가락을 모으면(pinch close) 배율이 감소하고, 벌리면(pinch open) 증가합니다. 제스처는 연속적이며, 시스템은 실시간으로 손가락 사이의 거리를 추적하고 중간 이벤트를 생성하여 애플리케이션이 배율을 부드럽게 업데이트할 수 있도록 합니다.

제스처의 메커니즘은 두 터치 포인트 사이의 거리 계산을 기반으로 합니다. iOS와 Android는 유클리드 거리를 사용합니다: sqrt((x2−x1)² + (y2−y1)²). 현재 배율은 현재 거리와 초기 거리의 비율로 계산됩니다. 초기 거리가 100px이고 150px이 되면 배율 = 1.5(50% 증가)입니다. iOS는 이전 상태에 상대적인 배율(델타)을 전달하고, Android는 ScaleGestureDetector를 통해 누적 배율을 전달합니다.

Google Material Design Guidelines에 따르면, Pinch-to-Zoom은 이미지, 지도 또는 문서를 표시하는 애플리케이션에 필수입니다. IT Sectr에서는 시각적 콘텐츠를 다루는 모든 프로젝트에서 UIPinchGestureRecognizer와 ScaleGestureDetector를 사용하여 이 제스처를 구현합니다 — 갤러리에서 이미지 편집기까지.

iOS에서 Pinch-to-Zoom: UIPinchGestureRecognizer

UIPinchGestureRecognizer는 핀치 제스처를 인식하기 위한 UIGestureRecognizer의 연속 하위 클래스입니다. 개별 인식기(UITapGestureRecognizer)와 달리, .began(손가락이 화면에 닿음), .changed(각 손가락 움직임), .ended(손가락을 뗌)의 이벤트 시퀀스를 생성합니다.

주요 속성: scale(CGFloat) — 제스처 시작 이후 손가락 사이 거리의 상대적 변화; velocity(CGFloat) — 초당 배율 변화율. 초기 배율 = 1.0. 손가락을 벌리면 배율이 증가하고(1.0 → 1.5 → 2.0), 모으면 감소합니다(1.0 → 0.8 → 0.5). velocity 속성은 관성에 사용되며, .ended 후에 감속과 함께 연속적인 스케일링을 애니메이션화할 수 있습니다.

UIPinchGestureRecognizer는 다른 인식기(예: 지도에서 pan + pinch)와 동시 인식을 위한 delegate 속성을 지원합니다. gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) 메서드는 true를 반환하며, 두 제스처가 병렬로 작동합니다. Apple은 개별 요소 대신 컨테이너 뷰에 UIPinchGestureRecognizer를 추가할 것을 권장합니다 — 이렇게 하면 스케일링 피벗 포인트 계산이 간소화됩니다.

Android에서 Pinch-to-Zoom: ScaleGestureDetector

ScaleGestureDetector는 멀티터치 스케일링을 인식하도록 설계된 Android Support Library(android.view)의 클래스입니다. GestureDetector.onFling과 달리, ScaleGestureDetector는 특히 두 손가락 줌을 위해 구축되었으며 focusX, focusY(손가락 사이의 중심점) 및 scaleFactor(이전 이벤트의 스케일 델타)를 자동으로 계산합니다.

ScaleGestureDetector는 세 가지 메서드가 있는 OnScaleGestureListener 인터페이스를 통해 작동합니다: onScaleBegin(detector)(.began에 해당), onScale(detector)(.changed에 해당, boolean 반환 — 스케일이 수락되면 true), onScaleEnd(detector)(.ended에 해당). 속성: scaleFactor — 이전 이벤트에 상대적인 스케일 승수(천천히 벌리면 약 1.01), focusX/focusY — 손가락 사이 중심점의 좌표.

View에 스케일링을 적용하려면 일반적으로 Matrix가 사용됩니다: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY). Matrix는 변환 누적을 지원하며, 각 postScale 호출은 기존 변환에 새 변환을 추가합니다. 중요한 점: ScaleGestureDetector는 최소/최대 배율을 제한하지 않습니다 — 이는 개발자의 책임입니다. IT Sectr에서는 사용자 지정 배율 경계(0.5–3.0)와 초과 시 반환 애니메이션이 있는 ScaleGestureDetector를 사용합니다.

줌 제한 및 피벗 포인트

Pinch-to-Zoom을 구현할 때 최소 및 최대 배율을 제한하는 것이 매우 중요합니다 — 이렇게 하지 않으면 사용자가 무한대로 확대하거나 보이지 않을 때까지 축소할 수 있습니다. 표준 값: 최소 = 0.5(원본 크기의 50%), 최대 = 3.0(300%). iOS에서는 클램프를 통해 제한합니다: newScale = min(max(currentScale * gesture.scale, minScale), maxScale). Android에서는 postScale 후 검사를 통해 제한합니다.

피벗 포인트는 손가락 사이의 중심점이며, 이를 기준으로 스케일링이 수행됩니다. iOS는 두 터치의 중심을 자동으로 계산하여 location(in:)을 통해 전달합니다 — 개발자가 수동으로 피벗을 계산할 필요가 없습니다. Android는 ScaleGestureDetector에서 focusX/focusY를 제공하지만, Matrix를 통해 적용할 때 피벗은 postScale(scaleFactor, scaleFactor, focusX, focusY)의 세 번째 및 네 번째 인수로 전달됩니다. 피벗 포인트가 없으면 스케일링이 왼쪽 상단 모서리(0,0)를 기준으로 수행되어 줌 중에 콘텐츠가 이동합니다.

IT Sectr의 경험에 따르면, 올바른 피벗 포인트 계산은 Pinch-to-Zoom 구현 시 가장 흔한 버그의 원인입니다. 90%의 경우, GestureRecognizer 및 ScaleGestureDetector의 내장 좌표를 사용하는 것으로 충분하며 피벗을 수동으로 계산할 필요가 없습니다. 스케일링 중 콘텐츠가 이동하면 focusX/focusY를 matrix.postScale()에 전달하고 있는지 확인하세요.

줌 후 관성

제스처 종료 후 부드러운 UX를 위해 관성을 추가할 수 있습니다: iOS에서는 UIView.animate 및 스프링 매개변수와 함께 UIPinchGestureRecognizer의 velocity를 사용하고, Android에서는 OverScroller 또는 ValueAnimator를 사용합니다. 관성은 최소/최대 배율 경계를 초과하지 않아야 하며, 제한을 초과하면 스프링 반환 애니메이션으로 최종 값이 제한됩니다.

Swift 및 Kotlin 코드 예제

예제 1: iOS에서 Pinch-to-Zoom (Swift)

0.5에서 3.0까지의 배율 제한으로 UIImageView에 UIPinchGestureRecognizer를 추가합니다. 누적 배율을 유지하는 최소 구현을 보여줍니다.

swift
import UIKit

class ZoomableImageViewController: UIViewController {

    @IBOutlet private var imageView: UIImageView!
    private var currentScale: CGFloat = 1.0
    private let minScale: CGFloat = 0.5
    private let maxScale: CGFloat = 3.0

    override func viewDidLoad() {
        super.viewDidLoad()

        let pinch = UIPinchGestureRecognizer(
            target: self,
            action: #selector(handlePinch(_:))
        )
        imageView.addGestureRecognizer(pinch)
        imageView.isUserInteractionEnabled = true
    }

    @objc private func handlePinch(_: UIPinchGestureRecognizer) {
        if gestureRecognizer.state == .began
            || gestureRecognizer.state == .changed {

            let newScale = currentScale * gestureRecognizer.scale
            imageView.transform = CGAffineTransform(
                scaleX: min(max(newScale, minScale), maxScale),
                y: min(max(newScale, minScale), maxScale)
            )
        }
        if gestureRecognizer.state == .ended {
            currentScale = min(max(
                currentScale * gestureRecognizer.scale,
                minScale
            ), maxScale)
            gestureRecognizer.scale = 1.0
        }
    }
}

로직: .began/.changed에서는 클램프와 함께 CGAffineTransform을 통해 배율이 적용됩니다. .ended에서는 배율이 1.0으로 재설정되고 누적 값이 currentScale에 저장됩니다. 이는 반복적인 제스처 중 배율 드리프트를 방지하며, 각 새 제스처는 현재 상태에서 시작됩니다. 이미지의 경우 CGAffineTransform 대신 minimumZoomScale/maximumZoomScale이 있는 UIScrollView가 권장됩니다.

예제 2: Android에서 Pinch-to-Zoom (Kotlin)

Matrix와 함께 ScaleGestureDetector를 통해 ImageView 스케일링을 구현합니다. 올바른 피벗 포인트 처리와 배율 제한을 보여줍니다.

kotlin
import android.content.Context
import android.graphics.Matrix
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.widget.ImageView

class PinchZoomImageView(context: Context) : ImageView(context) {

    private val matrix = Matrix()
    private var currentScale = 1f
    private val minScale = 0.5f
    private val maxScale = 3.0f

    private val scaleDetector =
        ScaleGestureDetector(context, ScaleListener())

    private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            currentScale = (currentScale * detector.scaleFactor)
                .coerceIn(minScale, maxScale)
            matrix.postScale(
                currentScale,
                currentScale,
                detector.focusX,
                detector.focusY
            )
            imageMatrix = matrix
            return true
        }
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        scaleDetector.onTouchEvent(event)
        return true
    }
}

사용자 지정 ImageView는 onTouchEvent를 재정의하여 ScaleGestureDetector에 이벤트를 전달합니다. OnScaleListener.onScale()은 각 손가락 움직임 시 호출됩니다. detector.scaleFactor는 델타입니다(천천히 벌리면 1.01). currentScale은 클램핑을 위해 coerceIn으로 누적됩니다. focusX/focusY — 손가락 사이의 중심점으로, 올바른 피벗 스케일링을 위해 postScale에 전달됩니다.

예제 3: 줌 지원이 있는 UIScrollView (iOS)

UIScrollView는 델리게이트를 통해 내장된 Pinch-to-Zoom 지원을 제공합니다. 이것이 이미지와 문서에 선호되는 접근 방식입니다 — ScrollView가 배율, 관성 및 스크롤을 자동으로 관리합니다.

swift
class ScrollableImageViewController: UIViewController,
                                    UIScrollViewDelegate {

    @IBOutlet private var scrollView: UIScrollView!
    @IBOutlet private var imageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.minimumZoomScale = 0.5
        scrollView.maximumZoomScale = 3.0
        scrollView.delegate = self
    }

    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageView
    }
}

UIScrollView는 자동으로 UIPinchGestureRecognizer를 생성하고, CALayer 변환을 통해 스케일링을 관리하며, 경계를 초과할 때 바운스 효과를 추가합니다. 코드는 min/maxZoomScale 설정 및 viewForZooming 반환으로 축소됩니다. IT Sectr에서는 모든 전체 화면 이미지에 UIScrollView를 사용합니다 — 이는 UIPinchGestureRecognizer를 통한 수동 구현에 비해 코드 양을 5배 줄여줍니다.

자주 묻는 질문

핀치가 트랙패드의 집기와 어떻게 다른가요?

터치 스크린에서 핀치는 두 손가락 사이의 거리 추적과 함께 두 손가락을 사용합니다. MacBook 트랙패드에서는 동일한 상태(.began → .changed → .ended)의 UIPinchGestureRecognizer를 통해 줌이 구현되지만, 터치 소스는 정전식 화면이 아닌 Force Touch 트랙패드입니다. API는 동일하고 메커니즘은 동일합니다.

최소 및 최대 줌을 어떻게 제한하나요?

iOS에서: UIPinchGestureRecognizer에서 배율을 얻은 후 currentScale을 곱하고 clamp(min: 0.5, max: 3.0)을 적용합니다. Android에서: matrix.postScale() 전에 coerceIn(minScale, maxScale)을 사용합니다. UIScrollView에서는 minimumZoomScale 및 maximumZoomScale 속성을 통해 제한합니다. 제한이 없으면 배율이 무한대나 0이 될 수 있습니다.

Pinch-to-Zoom이 한 손가락으로 작동하나요?

아니요, 핀치는 최소 두 손가락이 필요합니다. 한 손가락 줌(더블 탭 + 위/아래로 드래그)의 경우 UIPanGestureRecognizer 또는 GestureDetector.onScroll을 통한 사용자 지정 구현이 필요합니다. 이것은 다른 로직을 가진 다른 제스처이며, 일반적으로 카메라나 지도에서 줌에 사용됩니다.

요약

  • Pinch-to-Zoom은 콘텐츠를 확대/축소하기 위한 두 손가락 멀티터치 제스처로, 이미지, 지도 및 문서의 표준입니다.
  • iOS의 UIPinchGestureRecognizer는 scale(델타) 및 velocity(속도)가 있는 연속 인식기입니다.
  • Android의 ScaleGestureDetector는 피벗 포인트를 위한 onScale(), focusX/focusY가 있는 특수 검출기입니다.
  • UIScrollView의 경우 minimumZoomScale/maximumZoomScale을 통해 줌이 내장되어 있어 수동 구현보다 간단합니다.
  • 항상 클램프를 통해 배율(0.5–3.0)을 제한하세요 — 그렇지 않으면 콘텐츠가 보이지 않게 될 수 있습니다.
  • 스케일링 피벗 포인트는 손가락 사이의 중심입니다. 이것이 없으면 줌 중에 콘텐츠가 이동합니다.
  • IT Sectr에서는 이미지에 UIScrollView를, 편집기에 사용자 지정 ScaleGestureDetector를 사용합니다 — 선택은 변환의 복잡성에 따라 달라집니다.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기