Gesture 기능은 Framer motion에서 제공하는 아주 강력하고 편리한 기능으로, 브라우저에서 사용자의 제스처 모션을 인식할 수 있게 해줍니다. Gesture 기능에 대해서 심층적으로 알아보겠습니다.
motion
컴포넌트에 drag, tap, hover, pan등의 제스처 이벤트 리스너를 달아줄 수 있습니다.
motion
컴포넌트는 whileHover, whileTap, whileFocus등과 같은 다수의 애니메이션 prop을 제공합니다. 해당 제스쳐가 발생할 때 임시로 재생할 애니메이션 객체를 값으로 넣어주면 됩니다. Framer motion에서는 이러한 while+제스쳐명
으로 이루어진 prop들을 helper라고 부릅니다.
<motion.button
whileHover={{
scale: 1.2,
transition: { duration: 1 },
}}
whileTap={{ scale: 0.9 }}
/>
다음과 같이 variant를 사용할수도 있습니다.
<motion.button
whileTap="tap"
whileHover="hover"
variants={buttonVariants}
>
<svg>
<motion.path variants={iconVariants} />
</svg>
</motion.button>
동시에 여러 제스쳐가 하나의 컴포넌트에 걸리는 경우도 있습니다. 예를 들어 호버상태와 탭 상태가 겹치는 경우도 있을 수 있겠죠? 그런데 이런 경우는 어떤 제스쳐가 먼저 시작했으며 언제 어떤 제스쳐가 끝났는지에 따라 Framer motion이 우선순위를 판별하여 자연스럽게 재생할 애니메이션을 선택합니다.
propagation을 방지하려면 다음과 같이 리액트에서 제공하는 Capture
prop으로 e.stopPropagation()
함수를 실행시켜주면 됩니다.
<motion.div whileTap={{ scale: 2 }}>
<button onPointerDownCapture={e => e.stopPropagation()} />
</motion.div>
SVG filter 컴포넌트들은 특이하게도 물리적 실체가 없는것으로 인식되어 다이렉트로 while helper를 달아줄 수 없습니다. 따라서 다음과 같이 리액트 상태를 이용하여 유사한 효과를 내줘야 합니다.
const MyComponent = () => {
const [isHovered, setHovered] = useState(false)
// Simplified example
return (
<svg>
<image
filter="url(#blur)"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
/>
<filter id="blur">
<motion.div
initial={false}
animate={{ stdDeviation: isHovered ? 0 : 2 }}
/>
</filter>
</svg>
)
}
Hover 제스쳐는 포인터가 컴포넌트 내에 위치할 때와 밖으로 나갈 때를 감지합니다.
호버 상태는 onMouseEnter
와 onMouseLeave
와는 다릅니다. 호버 상태는 실제 마우스 포인터를 기준으로 동작합니다. 터치로는 호버상태를 만들 수 없습니다.
<motion.a
whileHover={{ scale: 1.2 }}
onHoverStart={e => {}}
onHoverEnd={e => {}}
/>
다음은motion
컴포넌트에 줄 수 있는 호버 상태 관련 prop들입니다.
<motion.div whileHover={{ scale: 1.2 }} />
(event: MouseEvent, info:EventInfo) => void
형식의 함수<motion.div onHoverStart={() => console.log('Hover starts')} />
(event: MouseEvent, info:EventInfo) => void
형식의 함수<motion.div onHoverEnd={() => console.log("Hover ends")} />
Focus 제스쳐는 CSS규칙에 따라 focus상태가 정의된 엘리먼트에 포커스를 얻거나 잃은 상태를 감지합니다.
전형적으로 input 엘리먼트가 이 focus상태를 얻기도, 잃기도 할 수 있는 엘리먼트입니다. 포커스는 마우스나 키보드 등으로 줄 수 있습니다.
<motion.a
whileFocus={{ scale: 1.2 }}
href="#"
/>
다음은motion
컴포넌트에 줄 수 있는 포커스 상태 관련 prop들입니다.
<motion.div whileFocus={{ scale: 1.2 }} />
Tap 제스쳐는 주 포인터(마우스 좌클릭 또는 첫 터치포인트 등)가 동일한 컴포넌트에 위에서 눌러졌다가 떼어질 때를 감지합니다.
탭 제스쳐는 항상 포인터가 프레스다운된 타겟부터 릴리즈되는 타겟이 동일해야 인식됩니다. 예를 들어, 버튼에 포인터를 올려놓고 좌클릭한 뒤, 클릭한 상태를 유지하다가 버튼 위에서 릴리즈해야 인식됩니다. 버튼 밖에서 릴리즈하면 그것은 탭이 아닌 tapCancel
이벤트라고 합니다.
탭 이벤트의 발생 대상이 되는 엘리먼트들은 모두 키보드로 접근 가능합니다.
tap prop을 가진 모든 엘리먼트는 포커스를 받을 수 있고, 포커스된 상태에서 Enter키를 사용하여 탭 이벤트를 트리거할 수 있습니다.
onTapStart
와 whileTap
을 트리거onTap
트리거onTapCancel
트리거다음은motion
컴포넌트에 줄 수 있는 탭 상태 관련 prop들입니다.
<motion.div whileTap={{ scale: 1.2 }} />
(event: MouseEvent | TouchEvent | PointerEvent, info:TapInfo) => void
형식의 함수function onTap(event, info) {
console.log(info.point.x, info.point.y)
}
<motion.div onTap={onTap} />
(event: MouseEvent | TouchEvent | PointerEvent, info:TapInfo) => void
형식의 함수function onTapStart(event, info) {
console.log(info.point.x, info.point.y)
}
<motion.div onTapStart={onTapStart} />
(event: MouseEvent | TouchEvent | PointerEvent, info:TapInfo) => void
형식의 함수function onTapCancel(event, info) {
console.log(info.point.x, info.point.y)
}
<motion.div onTapCancel={onTapCancel} />
Pan 제스쳐는 어떤 컴포넌트에 포인터가 프레스된 상태로 3픽셀 이상 움직이는 것을 감지합니다.
<motion.div onPan={(e, pointInfo) => {}} />
다음은motion
컴포넌트에 줄 수 있는 팬 상태 관련 prop들입니다.
🛑주의사항: 팬 제스쳐가 터치인풋으로 정상 작동하려면, 해당 엘리먼트가 touch scolling이 disabled되어 있어야 합니다. CSS 규칙인 touch-action 속성을 수정하여 disable시킬 수 있습니다.
(event: MouseEvent | TouchEvent | PointerEvent, info:PanInfo) => void
형식의 함수💡PanInfo 객체란?
다음 값들에 대한 x, y값을 가진 객체를 의미합니다.
point
: 디바이스나 페이지에 대한 좌표값delta
: 마지막 이벤트로부터 멀어진 거리(움직인 거리)offset
: 기존 팬 이벤트로부터의 오프셋velocity
: 포인터의 현재 속도
function onPan(event, info) {
console.log(info.point.x, info.point.y)
}
<motion.div onPan={onPan} />
(event: MouseEvent | TouchEvent | PointerEvent, info:PanInfo) => void
형식의 함수function onPanStart(event, info) {
console.log(info.point.x, info.point.y)
}
<motion.div onPanStart={onPanStart} />
(event: MouseEvent | TouchEvent | PointerEvent, info:PanInfo) => void
형식의 함수function onPanEnd(event, info) {
console.log(info.point.x, info.point.y)
}
<motion.div onPanEnd={onPanEnd} />
Drag 제스쳐는 기본적으로 Pan 제스쳐와 동일한 룰을 따르지만, 제스쳐가 적용된 컴포넌트의 x, y축을 기준으로 포인터의 움직임이 적용됩니다. 이해를 돕기 위한 예시로 피그마를 들어보겠습니다. 피그마에서 H(hand) 툴로 문서 전체에 대한 시야를 바꿀 때는 팬 제스쳐이고, V툴로 어떤 요소를 잡고 이동시킬 때는 드래그 제스쳐 입니다.
<motion.div drag />
다음은motion
컴포넌트에 줄 수 있는 드래그 상태 관련 prop들입니다.
<motion.div whileDrag={{ scale: 1.2 }} />
"x"
또는 "y"
값을 주어 특정 방향만 드래그 가능하게 줄 수도 있다."x"
또는 "y"
<motion.div drag="x" />
false
| Partial<BoundingBox2D>
| RefObject<Element>
// In pixels
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 300 }}
/>
// As a ref to another component
const MyComponent = () => {
const constraintsRef = useRef(null)
return (
<motion.div ref={constraintsRef}>
<motion.div drag dragConstraints={constraintsRef} />
</motion.div>
)
}
div
에 부모 div
의 ref
를 dragConstraints
로 넘겨줌으로써 부모 div
안에서 드래그 가능하게 한 예제 코드0.5
가 주어진다.number
값<motion.div
drag
dragConstraints={{ left: 0, right: 300 }}
dragElastic={0.2}
/>
true
가 주어진다. <motion.div
drag
dragConstraints={{ left: 0, right: 300 }}
dragMomentum={false}
/>
inertia
타입의 애니메이션이 자동으로 재생되는데, dragTransition
prop으로 이 애니메이션을 커스텀할 수 있다. InertiaOptions
<motion.div
drag
dragTransition={{ bounceStiffness: 600, bounceDamping: 10 }}
/>
false
가 주어진다.<motion.div drag="x" dragPropagation />
useDragControls
훅으로 생성할 수 있고, 생성된 객체의 start
메서드로 드래그 제스쳐를 이니시에이트 할 수 있다. DragControls
const dragControls = useDragControls()
function startDrag(event) {
dragControls.start(event, { snapToCursor: true })
}
return (
<>
<div onPointerDown={startDrag} />
<motion.div drag="x" dragControls={dragControls} />
</>
)
참고: dragControls를 쓰면 드래그 제스쳐의 이니시에이팅을 개발자가 컨트롤할 수 있다는 뜻이며, dragListener={false}를 줌으로써 drag prop을 준 엘리먼트가 드래그 제스쳐 이니시에이션을 할 수 없도록 막을 수도 있습니다.
(event: MouseEvent | TouchEvent | PointerEvent, info:PanInfo) => void
형식의 함수<motion.div
drag
onDrag={
(event, info) => console.log(info.point.x, info.point.y)
}
/>
(event: MouseEvent | TouchEvent | PointerEvent, info:PanInfo) => void
형식의 함수<motion.div
drag
onDragStart={
(event, info) => console.log(info.point.x, info.point.y)
}
/>
(event: MouseEvent | TouchEvent | PointerEvent, info:PanInfo) => void
형식의 함수<motion.div
drag
onDragEnd={
(event, info) => console.log(info.point.x, info.point.y)
}
/>
(axis: "x" | "y" => void
형식의 함수<motion.div
drag
dragDirectionLock
onDirectionLock={axis => console.log(axis)}
/>