#3 우당탕탕 Garden 페이지 개발 기록 - 마우스 추적 오브젝트

💛 nalsae·2024년 1월 21일
2

💭 프로젝트 회고

목록 보기
7/7
post-thumbnail

👉 이전 글 보러 가기 - 오브젝트 설치와 이벤트 처리

🌻 마우스를 따라다니는 오브젝트를 만들어보자!

 지난 글에서는 오브젝트 설치가 불가능한 위치 정보를 토대로 정원 맵의 오브젝트 설치 기능을 구현한 과정을 소개했었다. 그런데 여기서 분량상 다루지 못한 기능이 하나 있어서 부득이하게 이번 글에서 따로 소개하고자 한다. 바로 편집 모드인 경우에 사용자가 설치된 오브젝트를 클릭하면 마우스 커서를 따라다니는 임시 오브젝트가 그것이다. 소개에 앞서 동작 방식을 이미지로 살펴보면 다음과 같다.

 사용자가 클릭한 오브젝트는 정원 맵에서 일시적으로 렌더링되지 않기 때문에, 현재 사용자가 클릭한 오브젝트가 무엇인지 가시적으로 확인할 수 있는 수단이 필요했다. 그 역할은 상단 이미지에서 볼 수 있듯이 클릭 시 마우스 커서 우측 하단에 렌더링되는 <TrackedPlant/> 컴포넌트가 담당하고 있다. 그렇다면 <TrackedPlant/> 컴포넌트는 어떻게 구현되어 있는지 지금부터 소개하고자 한다.

📌 <TrackedPlant/> 컴포넌트

 우선 컴포넌트 구조를 살펴보면 다음과 같다.

// TrackedPlant.tsx

export default function TrackedPlant({
 targetX,
 targetY,
 moveTarget,
}: TrackedPlantProps) {
 const { productName, imageUrlTable, plantSize, imageSize } = moveTarget;

 return (
   <Image
     src={imageUrlTable[plantSize]}
     alt={productName}
     width={TARGETPLANT_SIZE[imageSize]}
     height={TARGETPLANT_SIZE[imageSize]}
     // ⭐️ CSS 최적화 : will-change ⭐️
     className="absolute will-change-transform"
     style={{
       // ⭐️ CSS 최적화 : translate3d ⭐️
       transform: `translate3d(${targetX - 40}px, ${targetY - 140}px, 0)`,
     }}
   />
 );
}

 편집 모드일 경우 사용자가 클릭한 오브젝트의 정보를 담고 있는 moveTarget을 prop으로 받아서 <img>요소만 렌더링해주면 되기 때문에 컴포넌트 구조 자체는 복잡하지 않다. 다만 한 가지 특징적인 부분이 있다면 classNamewill-change-transform과, 인라인 스타일로 적용한 translate3d일 것이다. 먼저 will-change-transform은 Tailwind CSS 문법으로, 순수 CSS의 will-change: transform과 같다. will-change 어트리뷰트의 값으로 지정한 CSS 어트리뷰트는 will-change라는 이름에서도 볼 수 있듯이 브라우저에게 자주 변화할 것이라고 미리 알려주는 역할을 하는데, 이로써 렌더링 최적화를 꾀할 수 있다. 간단하게 설명하면 자주 변화하는 CSS 어트리뷰트를 브라우저가 미리 알게 되면서 최적화를 준비하기 때문이다. 정원 페이지에서는 편집 모드일 때 사용자가 오브젝트를 클릭한 후 마우스 커서를 움직일 때마다 transform 값이 계속 변화할 것이므로, 이를 최적화하기 위해 적용했다. 다음으로 <TrackedPlant/> 컴포넌트의 위치를 top, left와 같은 position 관련 어트리뷰트로 제어하지 않고 transformtranslate3d로 제어하고자 했다. translate3d와 같은 CSS 함수는 CPU가 아닌 GPU를 사용하여 연산하기 때문이다. 렌더링 과정에서 CPU의 경우 'Recalculate > Reflow > Repaint'의 과정을 거치지만, GPU의 경우 'Recalculate > Composite Layer'의 과정으로 보다 빠른 렌더링이 가능하다. will-change와 마찬가지로 <TrackedPlant/>의 위치가 자주 변화할 것을 예상하여 렌더링 최적화를 위해 적용하고자 했다.

 그런데 여기서 한 가지 의문이 들 수도 있다. 그런데 왜 absolutewill-change는 제외하고 transform만 인라인 스타일로 적용하게 되었을까? 이는 Tailwind CSS의 특징과도 연결되어 있는데, Tailwind CSS는 컴파일 단계에서 사용하지 않는 CSS를 제거하여 필요한 스타일만 포함하는 최적의 스타일 시트를 미리 생성해놓기 때문이다. 이러한 특징 때문에 동적으로 클래스를 할당하는 방식의 스타일링이 불가능하다. 이전 글에서 잠깐 다뤘듯이 완성된 문자열의 형태로 className 값에 할당해야 스타일링을 동적으로 적용할 수 있다. 필자는 가독성 관점에서 컴포넌트 함수 내부에 일반 변수를 선언하는 상황을 별로 선호하지 않기도 하고, 마우스를 움직일 때마다 컴포넌트가 자주 리렌더링될 텐데 그때마다 transform의 값을 조작하는 긴 문자열을 생성하는 것보다 인라인 스타일로 동적 스타일링을 구현하는 방법이 메모리 면에서 효율적일 것이라 생각하여 후자의 방법을 택하게 되었다.

📌 useMouseTrack 커스텀 훅

 지금까지는 렌더링 관점에서 살펴보았으니 이번에는 데이터 관점에서 살펴보자. 사용자가 마우스 커서를 움직일 때마다 마우스 위치를 저장하는 로직useMouseTrack이라는 커스텀 훅이 담당한다.

// useMouseTrack.ts

const useMouseTrack = () => {
 const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); // 사용자의 마우스 커서 위치 좌표 값

 return {
   targetX: mousePosition.x,
   targetY: mousePosition.y,
   setMousePosition,
 };
};

// useGardenMap.ts

const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
 // 🚨 클릭한 오브젝트가 없는 경우 얼리 리턴 🚨
 if (!moveTarget) return;

 setMousePosition({ x: e.clientX, y: e.clientY });
};

 useMouseTrack 훅의 내부 로직은 간단하다. 좌표 값과 그 좌표 값을 변경하는 setter 함수를 반환한다. 그리고 이 setter 함수를 mousemove 이벤트 핸들러의 콜백 함수인 handleMouseMove 함수에서 호출하여 마우스 커서를 움직일 때마다 그 위치 정보를 저장한다. 이때 moveTarget이 존재하지 않는 경우, 즉 오브젝트를 클릭하지 않은 상태에서는 아무 동작도 수행하지 않아야 하기 때문에 얼리 리턴 처리를 해주었다.

 그런데 글을 작성하면서 생각해보니 굳이 targetX, targetY<TrackedPlant/> 컴포넌트의 props로 넘겨줄 필요가 없어보인다. 어차피 커스텀 훅을 만들었으니 <TrackedPlant/> 컴포넌트 내부에서 커스텀 훅을 호출하는 방식으로 좌표 값을 참조하는 방식이 더 나을 것 같다. 전달하는 props 개수를 불필요하게 늘리지 않아도 되니 말이다. 나중에 리팩토링을 하게 되면 이 부분을 수정해야겠다. 말이 나온 김에 하는 이야기이지만 리팩토링에는 끝이 없는 것 같다. 분명 몇 번의 수정을 거쳐 최선의 결과물이라고 생각했던 코드인데도 시간이 지나고 나서 보면 또 수정할 부분이 눈에 보인다. 지금처럼 말이다.


🥳 정원 맵 톺아보기 완료!

 여기까지 3편의 글을 거쳐 전반적으로 정원 맵의 컴포넌트 구조와 렌더링 방식에 초점을 맞춰 설명해보았다. 처음에는 복잡해보였지만 구현하려는 기능을 하나씩 단계별로 생각하면 크게 어렵지 않았던 것 같다. 자 그렇다면 이렇게 사용자가 변경한 오브젝트의 설치 정보는 서버에 어떻게 저장하고 있을까? 다음 글에서는 정원 맵 데이터의 흐름과 저장 방식에 대해 다뤄볼 예정이다.

profile
𝙸'𝚖 𝚊 𝚍𝚎𝚟𝚎𝚕𝚘𝚙𝚎𝚛 𝚝𝚛𝚢𝚒𝚗𝚐 𝚝𝚘 𝚜𝚝𝚞𝚍𝚢 𝚊𝚕𝚠𝚊𝚢𝚜. 🤔

0개의 댓글