Cursor Effect를 개발하면서

Aug 16, 2025

개발을 하게 된 이유

웹서핑하다가 발견한 웹 디자인 스튜디오가 있었다.
https://tv-sy.com/

3개의 점을 활용한 브랜딩이 심플하지만 인상적이었다.

이전에 내게 충격을 줬었던 Lusion 사이트도 생각이 나면서, 내 블로그에도 뭔가 재밌는 마우스 인터랙션을 심어두고 싶었다.

tv-sy 스타일 구현하기

마우스가 움직일 때 점이 따라와주도록 하면된다.

구현은 어렵지 않았다. 현재 마우스 위치를 트래킹해서 3개의 점을 그려주면 된다.
소스코드

마우스를 박스 안에서 움직여 보세요.


몇 가지 핵심 트릭을 기록해본다.

먼저, 마우스의 위치가 변경될 때마다 리렌더링시키는 것이 아닌 requestAnimationFrame을 통해서 브라우저 주사율에 맞춰 계속 그려주는 것이다. 자바스크립트 런타임 오버헤드를 줄일 수 있으며 보다 부드러운 화면 변화를 만들어낼 수 있다.

const loop = () => {
  updateDots();
  animationRef.current = requestAnimationFrame(loop);
};ts

애니메이션의 핵심 트릭은 CSS의 transition duration를 통해서 각 점이 조금의 시차를 두고 부드럽게 따라오도록 하는 것이다. 자연스러운 움직임을 위해서 그저 이 숫자를 조율하는 것 뿐이다.

.d1 {
  transition: all 0.12s ease-out;
  transform: translateX(21.6px) translateY(18.8px);
}

.d2 {
  transition: all 0.8s ease-out;
  transform: translateX(47.2px) translateY(26px);
}

.d3 {
  transition: all 0.52s ease-out;
  transform: translateX(28.8px) translateY(41.2px);
}css

Lusion 스타일 구현하기

마우스 주변에 빛 굴절 같은 유체 흐름을 만들어야 했다.

WebGL 기반 그래픽을 풀스크린 캔버스에 올리고 마우스의 클릭과 움직임에 따라 효과를 그려내면 된다. 아직 관련 이해도가 높지 않기 때문에 React Bitssplash-cursor를 거의 그대로 가져왔다.

기본 뼈대는 아래와 같다.

export default function SplashCursor(config) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    // 1. WebGL 세팅
    // WebGL 컨텍스트 → 셰이더 컴파일 → 프레임버퍼 생성

    // 2. 물리 시뮬레이션 세팅
    // 와류 → 압력 해결 → 이류 계산 순서로 실행

    // 3. 메인 루프 수행
    function updateFrame() {
      // 입력 처리 → 물리 시뮬레이션 연산 → 렌더링 → 반복
      requestAnimationFrame(updateFrame);
    }

    // 4. 이벤트 등록
    window.addEventListener('mousemove', (e) => {
      // 4-1. 마우스 움직임을 유체에 전달
      // updatePointerDownData
    });
  }, []);

  return (
    <div className="pointer-events-none fixed top-0 left-0 z-50 h-full w-full">
      <canvas ref={canvasRef} id="fluid" className="block h-screen w-screen" />
    </div>
  );
}tsx

핵심 로직은 WebGL 셰이더를 통해 유체 시뮬레이션을 처리하는 것이다.
과정을 간소화 해보면 Shader 생성 > Program 생성 > FBO 바인딩 > 렌더링 단계로 구성된다.

구성요소역할연결점
ShaderGPU 실행 코드Program의 구성 요소
ProgramGPU 파이프라인Shader들을 연결
FBO렌더링 대상Program 실행 결과 저장

추가로 알면 좋을 것이 FBO(Frame Buffer Object)를 활용한 더블 버퍼링이다. 이전 프레임의 결과를 텍스처로 사용해서 다음 프레임을 계산하는 방식으로 연속적인 유체 흐름을 만들어낸다. 이 패턴을 통해 CPU 대신 GPU에서 병렬 계산을 수행하여 실시간 유체 시뮬레이션이 가능해진다.

코드로 간단히 살펴보면 아래와 같다.

// 1. 여러 셰이더 생성
const curlShader = createShader(gl.FRAGMENT_SHADER, curlShaderSource);
const pressureShader = createShader(gl.FRAGMENT_SHADER, pressureShaderSource);
const advectionShader = createShader(gl.FRAGMENT_SHADER, advectionShaderSource);

// 2. 각각의 프로그램 생성
const curlProgram = createProgram(baseVertexShader, curlShader);
const pressureProgram = createProgram(baseVertexShader, pressureShader);
const advectionProgram = createProgram(baseVertexShader, advectionShader);

// 3. 더블 버퍼링을 위한 FBO 객체
const pressureFBO = createDoubleFBO(simWidth, simHeight); // 압력
const velocityFBO = createDoubleFBO(simWidth, simHeight); // 속도장

// 4. 시뮬레이션 단계별 실행
function step() {
  // 4-1. Curl 계산 (와류)
  gl.useProgram(curlProgram);
  gl.bindFramebuffer(gl.FRAMEBUFFER, curlFBO.framebuffer);
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

  // 4-2. Pressure 계산 (압력)
  gl.useProgram(pressureProgram);
  gl.bindFramebuffer(gl.FRAMEBUFFER, pressureFBO.write.framebuffer);
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
  pressureFBO.swap(); // 버퍼 교체

  // 4-3. Advection 계산 (이류)
  gl.useProgram(advectionProgram);
  gl.bindFramebuffer(gl.FRAMEBUFFER, velocityFBO.write.framebuffer);
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
  velocityFBO.swap(); // 버퍼 교체
}tsx

실제 로직은 훨씬 복잡하다. 이해하기 어려운 최적화 코드도 많이 탑재되어 있어 더 자세히 알고 싶다면 소스코드를 참고하길 바란다.

최종적으로 렌딩페이지에 적용된 커서 이펙트:

랜딩 페이지 예시

맺으면서

디자인 엔지니어링의 영역은 정말 어려운 것 같다. 간단한 CSS 트릭으로 감각적인 인터랙션을 만들기도 하고, 복잡한 WebGL 기능으로 화려한 그래픽을 그려내기도 한다.

처음부터 엄청난 것을 만들 순 없다.
그저 하나하나 따라해보고 원리를 이해해보면 나만의 인터랙션을 만들어 낼 수 있을 것이다.

참고한 자료

https://tholman.com/cursor-effects/
https://reactbits.dev/animations/splash-cursor

더 참고해보면 좋을 자료:

https://motion-primitives.com/docs/cursor
https://magicui.design/docs/components/smooth-cursor
https://reactbits.dev/animations/blob-cursor