0Pricing
React Native Academy · 강의

핀치 확대 및 회전

동시 제스처 인식과 함께 PinchGestureHandler 및 RotationGestureHandler를 결합하여 확대 및 회전이 가능한 이미지 컴포넌트를 구현합니다.

핀치 확대 및 회전은(는) CoddyKit의 무료 React Native Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 React Native Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Pinch and Rotation Matter

Pinch-to-zoom and rotation are hallmark interactions on mobile, found in photo viewers, maps, and document apps. Implementing them well requires multi-touch gesture recognition that runs on the native thread and smooth real-time transform updates via Reanimated shared values.

React Native Gesture Handler provides Gesture.Pinch() and Gesture.Rotation() recognizers, which combined with Reanimated's useAnimatedStyle and useSharedValue, produce butter-smooth interactions that feel indistinguishable from native implementations.

Gesture.Pinch: Detecting Scale

Gesture.Pinch() recognizes a two-finger pinch or spread gesture. The most important value in the event object is event.scale, which starts at 1.0 when the gesture begins and changes proportionally to finger distance. A value of 2.0 means the fingers are twice as far apart as at start.

You must combine the gesture's incremental scale with a stored base scale to accumulate zoom across multiple pinch gestures — otherwise each new pinch resets to 1.0.

import { Gesture } from 'react-native-gesture-handler';
import { useSharedValue, withSpring } from 'react-native-reanimated';

const scale = useSharedValue(1);
const savedScale = useSharedValue(1);

const pinch = Gesture.Pinch()
  .onUpdate((e) => {
    scale.value = savedScale.value * e.scale;
  })
  .onEnd(() => {
    savedScale.value = scale.value;
  });

Clamping Zoom Range

Without clamping, users can zoom in to enormous scales or zoom out until the image disappears. Define a minimum and maximum zoom level and clamp the scale value in the onUpdate callback using a utility worklet.

The clamp function runs on the UI thread as a worklet, keeping the gesture response fast. Choose clamp values appropriate for your content — photo viewers typically allow 1x to 5x zoom, while maps may allow much wider ranges.

const MIN_SCALE = 1;
const MAX_SCALE = 5;

function clamp(value, min, max) {
  'worklet';
  return Math.min(Math.max(value, min), max);
}

const pinch = Gesture.Pinch()
  .onUpdate((e) => {
    scale.value = clamp(savedScale.value * e.scale, MIN_SCALE, MAX_SCALE);
  })
  .onEnd(() => {
    savedScale.value = scale.value;
    // Snap back if over-zoomed out:
    if (scale.value < MIN_SCALE) {
      scale.value = withSpring(MIN_SCALE);
      savedScale.value = MIN_SCALE;
    }
  });

Gesture.Rotation: Tracking Angle

Gesture.Rotation() tracks the angle change of a two-finger twist gesture. The event.rotation value is in radians — the angle the fingers have rotated from the gesture start position. A full clockwise rotation is approximately 6.28 radians (2π).

Like scale, you must accumulate rotation across multiple gestures by saving the current rotation at gesture end and adding new gesture rotation to the saved offset in subsequent gestures.

const rotation = useSharedValue(0);
const savedRotation = useSharedValue(0);

const rotationGesture = Gesture.Rotation()
  .onUpdate((e) => {
    rotation.value = savedRotation.value + e.rotation;
  })
  .onEnd(() => {
    savedRotation.value = rotation.value;
  });

Applying Scale and Rotation in useAnimatedStyle

The transform array in useAnimatedStyle applies both scale and rotation. The order of transforms matters: apply scale first, then rotation (or vice versa depending on desired behavior). React Native's transform array accepts both numeric scale and radian rotation values directly from shared values.

The rotate transform requires a string with the 'rad' suffix, while scale takes a number directly. Use string concatenation (not template literals with backticks) to construct the rotation string in a worklet.

const animatedStyle = useAnimatedStyle(() => ({
  transform: [
    { scale: scale.value },
    { rotate: rotation.value + 'rad' }, // concatenate string, no backticks
  ],
}));

return (
  <GestureDetector gesture={composedGesture}>
    <Animated.Image
      source={{ uri: imageUrl }}
      style={[styles.image, animatedStyle]}
    />
  </GestureDetector>
);

Composing Pinch and Rotation Simultaneously

A photo viewer needs pinch and rotation to work at the same time. Wrap both gestures in Gesture.Simultaneous and pass the composed gesture to a single GestureDetector. This enables the user to zoom and rotate with a two-finger gesture in one fluid motion.

Adding pan simultaneously creates the full photo viewer experience: zoom, rotate, and reposition the image all with two fingers at once.

const composedGesture = Gesture.Simultaneous(
  pinch,
  rotationGesture,
  pan
);

return (
  <GestureDetector gesture={composedGesture}>
    <Animated.Image
      source={{ uri: imageUrl }}
      style={[styles.image, animatedStyle]}
      resizeMode='contain'
    />
  </GestureDetector>
);

Double Tap to Reset Zoom

A standard photo viewer feature is double-tapping to toggle between the natural size and a zoomed-in view, and again to reset. Use Gesture.Tap().numberOfTaps(2) for the double-tap recognizer, and in its handler animate scale and rotation back to their initial values with a spring.

Use Gesture.Exclusive with the double-tap gesture having priority over single-tap to prevent single-tap from firing during a double-tap sequence.

const doubleTap = Gesture.Tap()
  .numberOfTaps(2)
  .onEnd(() => {
    if (scale.value > 1) {
      // Reset to natural size:
      scale.value = withSpring(1);
      savedScale.value = 1;
      rotation.value = withSpring(0);
      savedRotation.value = 0;
      offsetX.value = withSpring(0);
      offsetY.value = withSpring(0);
    } else {
      // Zoom to 2x:
      scale.value = withSpring(2);
      savedScale.value = 2;
    }
  });

Pivot Point for Pinch Zoom

By default the scale transform scales the image from its center point. For a more natural feel, the image should zoom around the focal point — the midpoint between the two fingers. The gesture event provides event.focalX and event.focalY for this purpose.

Implementing focal-point zoom requires adjusting the translation offset alongside scale so the point under the fingers stays fixed during zoom. This math involves calculating the offset change caused by the scale change at a given focal point.

const pinch = Gesture.Pinch()
  .onUpdate((e) => {
    const newScale = clamp(savedScale.value * e.scale, MIN_SCALE, MAX_SCALE);
    const scaleDelta = newScale / scale.value;
    // Adjust offset to keep focal point fixed:
    offsetX.value = e.focalX - scaleDelta * (e.focalX - offsetX.value);
    offsetY.value = e.focalY - scaleDelta * (e.focalY - offsetY.value);
    scale.value = newScale;
  });

Boundary Constraints After Pan and Zoom

After zooming in, the user can pan to see different parts of the image. But you should constrain panning so the image cannot be dragged to reveal empty canvas outside its edges. Calculate the maximum pan offset based on the current scale and image dimensions, and clamp the offset in the pan's onUpdate.

This is the polish that distinguishes a professional photo viewer from a rough prototype. The constraint logic is a few lines of math that run as worklets on the UI thread.

const IMAGE_WIDTH = 300;
const CONTAINER_WIDTH = 375;

const pan = Gesture.Pan()
  .onUpdate((e) => {
    const maxOffsetX = Math.max(0, (scale.value * IMAGE_WIDTH - CONTAINER_WIDTH) / 2);
    offsetX.value = clamp(
      savedOffsetX.value + e.translationX,
      -maxOffsetX,
      maxOffsetX
    );
  });

Animating Back on Over-Zoom

When the user lifts their fingers after over-zooming (below minimum or above maximum), spring the scale back to the limit rather than leaving the image at an unusual size. Do this in the onEnd callback of the pinch gesture.

Similarly, after the user zooms back out to 1x, reset the pan offset to zero so the image returns to center. This combination of boundary checks in onEnd makes the component feel polished and predictable.

const pinch = Gesture.Pinch()
  .onEnd(() => {
    if (scale.value < MIN_SCALE) {
      scale.value = withSpring(MIN_SCALE);
      savedScale.value = MIN_SCALE;
      // Reset pan to center:
      offsetX.value = withSpring(0);
      offsetY.value = withSpring(0);
    } else if (scale.value > MAX_SCALE) {
      scale.value = withSpring(MAX_SCALE);
      savedScale.value = MAX_SCALE;
    } else {
      savedScale.value = scale.value;
    }
  });

Putting It All Together

A complete zoomable, rotatable, pannable photo viewer combines: shared values for scale, rotation, and offset; useAnimatedStyle applying all transforms; Gesture.Simultaneous composing pinch, rotation, pan, and double-tap; and boundary clamping in onUpdate and spring correction in onEnd.

This is one of the most impressive interactions you can build in React Native, and it runs entirely on the UI thread with no JavaScript thread involvement during the gesture — making it indistinguishable from a native implementation.

const composed = Gesture.Simultaneous(
  Gesture.Exclusive(
    doubleTap,
    singleTap
  ),
  pan,
  pinch,
  rotationGesture
);

const style = useAnimatedStyle(() => ({
  transform: [
    { translateX: offsetX.value },
    { translateY: offsetY.value },
    { scale: scale.value },
    { rotate: rotation.value + 'rad' },
  ],
}));

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: Gesture.Pinch() provides event.scale to track two-finger zoom relative to the gesture start, Gesture.Rotation() provides event.rotation in radians to track two-finger twist, and Gesture.Simultaneous composes pinch, rotation, and pan so all three work at once for a full photo viewer experience. Next up we configure custom URL schemes for deep linking into specific app screens.

자주 묻는 질문

“핀치 확대 및 회전” 강의는 무료인가요?

네 — “핀치 확대 및 회전” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 React Native Academy 강의 전체를 잠금 해제할 수 있습니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“핀치 확대 및 회전”에서 뭘 배우나요?

동시 제스처 인식과 함께 PinchGestureHandler 및 RotationGestureHandler를 결합하여 확대 및 회전이 가능한 이미지 컴포넌트를 구현합니다. 브라우저에서 직접 실행하는 실습 코드로 React Native Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

React Native Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 React Native Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“핀치 확대 및 회전” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 React Native Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 React Native Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. Reanimated 및 공유 값 설치
  2. Worklet과 UI 스레드에서 코드 실행하기
  3. Gesture Handler로 팬 및 스와이프 제스처 구현하기
  4. 핀치 확대 및 회전
← React Native Academy(으)로 돌아가기