React로 마우스, 키보드 조작이 가능한 Joystick 만들기
라이브러리를 사용하지 않고 만들어보는 조이스틱
라이브러리 있는데 굳이 직접 만든 이유
라이브러리 적용해서 스타일이나 내가 원하는 기능 찾는 것과 직접 만드는 것 중에 어떤게 더 복잡하고 오래걸릴지를 비교해봤는데, 직접 만드는게 나중에 관리하기도 편하고 내부 api 를 연결하기도 편할 것 같았다. 하지만 가장 큰 이유는 단순히 재밌어 보였기 때문이다. 히히
(개고생 할줄은 몰랐음)
구현
이걸 혼자할수는 없었다. 내 짝꿍 GPT의 도움을 많이 받았다.
하.지.만. GPT도 많이 틀렸다. 그래서 내가 따로 검색하고 계산하고 이런 고생도하면서 만들었다.(짝꿍아…)
-
Joystick 그리기
시작은 또 그냥 대뜸 GPT한테 요청한다.
그럼 대충 코드를 짜주는데 마음에 들지 않아서 내가 대충 짜봤다.
UI 그리는거야 어렵지 않으니까..(?)const [position, setPosition] = useState({ x: 18, y: 18 }); // 대충 중앙에 맞춰 준다. return ( <JoystickContainer > <JoystickStick x={position.x} y={position.y} /> // position을 업데이트 해주고 해당 업데이트된 포지션을 따라서 조시스틱의 움직임을 구현해줄 예정. </JoystickContainer> ); // css const JoystickContainer = styled.div` position: relative; width: 85px; height: 85px; background-color: #2C2C2C; border-radius: 50%; overflow: hidden; `; const JoystickStick = styled.div<{ x: number; y: number }>` position: absolute; width: 50px; height: 50px; border-radius: 50%; background: radial-gradient(circle at 25px 25px, #444444, #141414); transform: translate(${(props) => props.x}px, ${(props) => props.y}px); transition: transform 0.2s; z-index: 3; `;
이렇게 하면 아래와 같은 이미지 처럼 ui가 그려진다.
- 이벤트 추가하기
마우스 + 키보드 조작이 가능하도록 해야한다.- 마우스 이벤트 추가
const [isDragging, setIsDragging] = useState(false); const handleMouseDown = () => { setIsDragging(true); }; const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => { event.preventDefault(); if (isDragging) { const containerRect = joystickRef.current?.getBoundingClientRect(); if (containerRect) { const x = event.clientX - containerRect.left - 25; // 조이스틱 중앙 정렬 const y = event.clientY - containerRect.top - 25; // 조이스틱 중앙 정렬 moveJoystick(x, y) } } }; const handleMouseUp = () => { setIsDragging(false); resetJoystick() }; const handleMouseLeave = () => { if (isDragging) { setIsDragging(false); resetJoystick(); } }; const resetJoystick = () => { setPosition({ x: 18, y: 18 }); }; <JoystickContainer onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} > <JoystickStick x={position.x} y={position.y} /> </ JoystickContainer>
- 키보드 이벤트 추가
const [isPressKey, setIsPressKey] = useState(false); const handleKeyDown = (event: KeyboardEvent) => { if (isDragging) return; if (!isPressKey) setIsPressKey(true); switch (event.key) { case 'ArrowUp': moveJoystickByKeyboard(position.x, 0); break; case 'ArrowDown': moveJoystickByKeyboard(position.x, 35); break; case 'ArrowLeft': moveJoystickByKeyboard(0, position.y); break; case 'ArrowRight': moveJoystickByKeyboard(35, position.y); break; default: break; } }; const handleKeyUp = () => { if (!isDragging) { resetJoystick(); } setIsPressKey(false); }; const moveJoystickByKeyboard = (targetX: number, targetY: number) => { const updatePosition = () => { const dx = targetX - position.x; const dy = targetY - position.y; setPosition({x: position.x += dx, y: position.y += dy}) updateDirection(position.x, position.y); // 조이스틱이 영역 끝쪽에 닿으면 업데이트 중지 if (Math.abs(dx) < 0.1 && Math.abs(dy) < 0.1) { setPosition({ x: targetX, y: targetY }); updateDirection(targetX, targetY); } else { requestAnimationFrame(updatePosition); } }; updatePosition(); };
- 마우스 이벤트 추가
- 이벤트에 따른 방향 업데이트 함수
const updateDirection = (x: number, y: number) => { if (!isControlOn) return; let direction = ''; // 센터는 약 18이고 원의 가장자리는 0 또는 35 이다. 해당 값을 기준으로 아래 방향이 설정된다. if (x > 9 && x <= 26.5 && y > -17.5 && y <= 9) { // x: 18, y: 0 direction = 'front'; } else if (x > -17.5 && x <= 9 && y > -17.5 && y <= 9) { // x: 0, y: 0 direction = 'frontLeft'; } else if (x > 26.5 && y > -17.5 && y <= 9) { // x: 35, y: 0 direction = 'frontRight'; } else if (x > 9 && x <= 26.5 && y > 26.5) { // x: 18, y: 35 direction = 'back'; } else if (x > -17.5 && x <= 9 && y > 26.5) { // x: 0, y: 35 direction = 'backLeft'; } else if (x > 26.5 && y > 26.5) { // x: 35, y: 35 direction = 'backRight'; } else if (x > -17.5 && x <= 9 && y > 9 && y <= 26.5) { // x: 0 y: 18 direction = 'left'; } else if ((x > 26.5) && (y > 9 && y <= 26.5)) { // x: 35, y: 18 direction = 'right'; } onChange(direction); };
이렇게까지 하면 아래와 같이 마우스와 키보드 조작이 가능해진다.
뭔가 공튀기는 모습 같기도..
-
방향 화살표 그리기
쓸데 없지만 화살표도 그려주자.const [arrowDirection, setArrowDirection] = useState<string>(''); // 조이스틱 상태에 따른 화살표 활상화 상태 ... const resetJoystick = () => { setPosition({ x: 18, y: 18 }); setArrowDirection('') // 입력 값이 없을때 화살표 활성화 상태 초기화 }; const updateDirection = (x: number, y: number) => { ... setArrowDirection(direction) } ... // 조이스틱과 같은 라인에 화살표를 넣어준다. <JoystickContainer onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} > // frontLeft, frontRight 등도 처리해줬어야 했는데 깜박했다. {['front', 'right', 'back', 'left'].map((arrow, index) => { return ( <ArrowWrapper key={index} rotate={index % 2 === 1 && index * 90}> <Arrow fill={arrow === arrowDirection ? '#00E100' : '#6A6762'}/> </ArrowWrapper> ) })} <JoystickStick x={position.x} y={position.y} /> </ JoystickContainer> ... const ArrowWrapper = styled.div<{rotate: number}>` position: absolute; width: fit-content; height: fit-content; top: ${({rotate}) => (rotate === 0 && '5%') || (rotate === 180 && '95%') || '50%'}; left: ${({rotate}) => (rotate === 90 && '95%') || (rotate === 270 && '5%') || '50%'}; transform: translate(-50%, -50%) rotate(${({rotate}) => rotate + 'deg'}); z-index: 1; `;
그런데 조이스틱이 화살표를 가려서 의미가 있는걸까 싶다.
후기
컴포넌트가 처음에는 가벼운 느낌이었으나 마우스 이벤트와 키보드 이벤트를 넣고 해당 이벤트 값에 대한 조이스틱 이동 함수를 넣으니 좀 복잡해졌다. 가벼운 마음으로 시작했는데 하다보니 생각보다 가볍지 않았다..
러닝커브를 없애고 유지보수성을 좀 높여보고자 직접 작성한 것인데 코드가 길어져서 과연 유지보수성이 좋아질지 모르겟다.
라이브러리가 존재하는 이유가 여기 있는게 아닐까 싶다.
이렇게 한번 구현해 봤으니 라이브러리는 어떤식으로 구현되어있는지 뜯어 보는 것도 좋은 공부가 될 것 같다.