https://velog.io/@remon/React-react-simple-keyboard-가상-키보드-라이브러리

터치 Ripple Effect 적용

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,
  )
}
/** @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;
}

이미지 드래그 방지 (React 기준)

JSX에서 draggable={false} 속성 사용

<img src="/your-image.png" alt="..." draggable={false} />

텍스트 드래그 방지 (선택 불가)