https://velog.io/@remon/React-react-simple-keyboard-가상-키보드-라이브러리
pointerdown
(터치 or 클릭) 이벤트 발생import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
type Ripple = {
x: number
y: number
id: number
}
export default function GlobalRippleEffect() {
// 상태 관리
// ripples는 현재 화면에 표시할 리플 원들을 저장하는 배열
// Ripple은 객체로 x, y 좌표와 id 포함
const [ripples, setRipples] = useState<Ripple[]>([])
// 이벤트 핸들링
// pointerdown이 발생하면 클릭 위치의 좌표를 기반으로 리플 객체를 ripples 배열에 추가
// setTimeout()을 써서 0.5초 뒤에 해당 리플을 배열에서 제거해서 화면에서 없앰
// Date.now()는 유일한 ID 역할
useEffect(() => {
const handlePointerDown = (e: PointerEvent) => {
const id = Date.now()
const x = e.clientX
const y = e.clientY
setRipples((prev) => [...prev, { x, y, id }])
// 500ms 뒤 ripple 제거
setTimeout(() => {
setRipples((prev) => prev.filter((r) => r.id !== id))
}, 500)
}
window.addEventListener('pointerdown', handlePointerDown)
return () => {
window.removeEventListener('pointerdown', handlePointerDown)
}
}, [])
return createPortal(
<div className="fixed inset-0 z-[9999] pointer-events-none">
{/* 리플 랜더링
span 태그 하나가 리플 하나
클릭한 위치를 기준으로 원을 중앙에 표시하도록 x - 32, y - 32로 위치 조정
w-16 h-16은 지름이 64px → 반지름 32px
animate-[ripple_0.6s_ease-out]: 앞서 만든 커스텀 애니메이션 적용
*/}
{ripples.map((ripple) => (
<span
key={ripple.id}
className="absolute w-16 h-16 rounded-full bg-yellow-300 opacity-70 animate-[ripple_0.6s_ease-out]"
style={{
left: ripple.x - 32,
top: ripple.y - 32,
}}
/>
))}
</div>,
// 리플 요소를 document.body 하위에 렌더링
// 플이 어떤 컴포넌트 위에도 겹쳐서 뜰 수 있음
document.body,
)
}
animation: { ripple: 'ripple 0.5s ease-out' }
'ripple'
: 아래 keyframes.ripple
을 참조해서 실행할 애니메이션 이름
0.5s
: 애니메이션 지속 시간 (0.5초)
ease-out
: 애니메이션 속도 곡선 → 시작은 빠르게, 끝은 느리게
animation: ripple 0.5s ease-out;이라는 CSS를 만들어주는 Tailwind 설정
keyframes: ripple
@keyframes
를 정의한 것과 같아/** @type {import('tailwindcss').Config} */
export default {
darkMode: ['class'],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
animation: {
ripple: 'ripple 0.5s ease-out',
},
keyframes: {
ripple: {
'0%': {
transform: 'scale(0)', /* 시작 시 크기 0 */
opacity: '0.7', /* 투명도 70% */
},
'100%': {
transform: 'scale(2)', /* 2배 크기로 확장 */
opacity: '0', /* 완전히 사라짐 */
},
},
},
...
},
plugins: [require('tailwindcss-animate')],
}
import GlobalRippleEffect from './styles/RippleEffect'
function App() {
return (
<>
<Fonts />
<GlobalRippleEffect />
<Routes>
{/* 관리자 로그인 */}
<Route path="/" element={<AdminLoginPage />} />
{/* 시작 화면 */}
<Route path="/user" element={<UserLoginPage />} />
</Routes>
</>
)
}
CSS에서 전체 앱 또는 특정 영역에 다음 속성 설정
* {
cursor: none;
}
JSX에서 draggable={false}
속성 사용
user-drag
은 존재하지만 비표준이라서 신뢰하기 어려움<img src="/your-image.png" alt="..." draggable={false} />
Text
컴포넌트 내부에도 영향을 줄 수 있도록 전역 적용이 가장 확실