๋ฐ˜์‘ํ˜•

์ž์‹ ์š”์†Œ์˜ ์ฝ˜ํ…์ธ ๊ฐ€ ๋ถ€๋ชจ ์š”์†Œ๋ณด๋‹ค ํฌ๋‹ค๋ฉด, ๋ถ€๋ชจ ์š”์†Œ์— ์Šคํฌ๋กค์ด ์ƒ๊ธฐ๊ณ , ๋งˆ์šฐ์Šค ํœ ๋กœ ์Šคํฌ๋กคํ•  ์ˆ˜ ์žˆ๋‹ค. ๋งˆ์šฐ์Šค ํœ  ์™ธ์—๋„ click / move ์ด๋ฒคํŠธ๋ฅผ ์ด์šฉํ•ด ๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ์Šคํฌ๋กคํ•˜๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

 

 

TL;DR


โถ ๋งˆ์šฐ์Šค๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ

  • `clientX`, `clientY` (๋ทฐํฌํŠธ ๊ธฐ์ค€)์ขŒํ‘œ์™€, ์š”์†Œ์˜ ์Šคํฌ๋กค ์œ„์น˜ `scrollLeft`, `scrollTop` ์ €์žฅ
  • ํด๋ฆญ ์ƒํƒœ `true`๋กœ ๋ณ€๊ฒฝ
  • (CSS) `cursor: grabbing user-select: none`

 

โท ํด๋ฆญํ•œ ์ƒํƒœ์—์„œ ๋งˆ์šฐ์Šค๋ฅผ ์ด๋™(๋“œ๋ž˜๊ทธ)ํ–ˆ์„ ๋•Œ

  • โ‘ ์ด๋™์„ ๋ฉˆ์ถ˜ ์ง€์ (clientX/Y)๊ณผ โ‘ก๋งˆ์šฐ์Šค๋ฅผ ํด๋ฆญํ•œ ์ง€์ **(clientX/Y)**์„ ๋บ€ ๊ฐ’ ๊ณ„์‚ฐ — ์Šคํฌ๋กคํ•œ ๋ฒ”์œ„
  • (์š”์†Œ์˜ ํ˜„์žฌ ์Šคํฌ๋กค ์œ„์น˜)์™€ (์Šคํฌ๋กคํ•œ ๊ฐ’)์„ ๋บ€ ์œ„์น˜๋กœ ์Šคํฌ๋กค ์ด๋™

 

โธ ๋งˆ์šฐ์Šค ํด๋ฆญ์„ ํ•ด์ œํ•˜๊ฑฐ๋‚˜ ์ด๋ฒคํŠธ ์˜์—ญ์„ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ

  • ํด๋ฆญ ์ƒํƒœ `false`๋กœ ๋ณ€๊ฒฝ
  • (CSS) `cursor: grab`

 

โน ์š”์†Œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํด๋ฆญํ•œ ๊ณณ์— ๋งˆ์ปค(์ด๋ฏธ์ง€) ๋“ฑ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ์„ ๋•Œ

  • ๋„ค์ดํ‹ฐ๋ธŒ ์ด๋ฒคํŠธ์— ์ ‘๊ทผํ•ด์„œ `offsetX`, `offsetY` ์ขŒํ‘œ๊ฐ’ ์ €์žฅ ํ›„(์Šคํฌ๋กค ์˜์—ญ๊นŒ์ง€ ํฌํ•จํ•œ ๊ฐ’)
  • ์ž์‹ ์ด๋ฏธ์ง€ ํƒœ๊ทธ์˜ `left`, `top` ํฌ์ง€์…˜ ๊ฐ’์œผ๋กœ `offsetX`, `offsetY` ํ• ๋‹น

 

๊ธฐํ•˜ ํ”„๋กœํผํ‹ฐ


๊ธฐํ•˜ ํ”„๋กœํผํ‹ฐ ์ด๋ฏธ์ง€ - ์ถœ์ฒ˜ JavaScript Info

 

๋ฉ”์„œ๋“œ๋ณ„ ์ธก์ • ๊ธฐ์ค€ โญ๏ธ

  • offset : ์ด๋ฒคํŠธ๊ฐ€ ๊ฑธ๋ ค ์žˆ๋Š” DOM ๊ฐ์ฒด ๊ธฐ์ค€
  • screen : ๋ชจ๋‹ˆํ„ฐ ๊ธฐ์ค€
  • client : ๋ธŒ๋ผ์šฐ์ € ๊ธฐ์ค€
  • page : ๋ฌธ์„œ ๊ธฐ์ค€

 

๊ตฌํ˜„์ „ ์•Œ์•„์•ผํ•  ๊ธฐํ•˜ ํ”„๋กœํผํ‹ฐ โญ๏ธ

๊ธฐํ•˜ ํ”„๋กœํผํ‹ฐ์˜ ๊ฐ’์€ ์ˆซ์ž์ด๋ฉฐ ํ”ฝ์…€ ๋‹จ์œ„๋กœ ์ธก์ •. scrollLeft, scrollTop์„ ์ œ์™ธํ•œ ๋ชจ๋“  ๊ธฐํ•˜ ํ”„๋กœํผํ‹ฐ๋Š” ์ฝ๊ธฐ ์ „์šฉ

 

๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ๋ฅผ ์ œ์™ธํ•˜๊ณ  ๋†’์ด(์„ธ๋กœ) ๊ด€๋ จ ํ”„๋กœํผํ‹ฐ๋งŒ ํ‘œ์‹œ ์ฒ˜๋ฆฌ

 

โถ `clientWidth`, `clientHeight` : ํ•ด๋‹น ์š”์†Œ์˜ ๋‚ด๋ถ€ ๋„ˆ๋น„ / ๋†’์ด

  • ํฌํ•จ : padding
  • ์ œ์™ธ : (์กด์žฌํ•˜๋ฉด)์Šคํฌ๋กค๋ฐ” ๋„ˆ๋น„ or ๋†’์ด, border, margin

 

โท `offsetWidth`, `offsetHeight` : ํ•ด๋‹น ์š”์†Œ์˜ ๋„ˆ๋น„ / ๋†’์ด

  • ํฌํ•จ : padding, ์Šคํฌ๋กค๋ฐ” ๋„ˆ๋น„ or ๋†’์ด, border
  • ์ œ์™ธ : margin
  • CSS์—์„œ `box-sizing: border-box` ์ผ ๋•Œ ์ง€์ •ํ•œ ๋„ˆ๋น„ / ๋†’์ด์™€ ๋™์ผํ•จ

 

โธ `scrollWidth`, `scrollHeight` : ์Šคํฌ๋กค๋ฐ”์— ์˜ํ•ด ๊ฐ์ถฐ์ง„ ๋ถ€๋ถ„์„ ํฌํ•จํ•œ ์ฝ˜ํ…์ธ ์˜ ์ „์ฒด ๋„ˆ๋น„ / ๋†’์ด

  • ํฌํ•จ : padding, border
  • ์ œ์™ธ : margin

 

โน `scrollLeft`, `scrollTop` (์ˆ˜์ •๊ฐ€๋Šฅ) : ์Šคํฌ๋กคํ•ด์„œ ๊ฐ€๋ ค์ง„ ์ฝ˜ํ…์ธ  ์˜์—ญ์˜ ๋„ˆ๋น„ / ๋†’์ด

  • scrollTop์„ `0`ํ˜น์€ `1e9`์œผ๋กœ ์„ค์ •ํ•ด์„œ ์ตœ์ƒ๋‹จ / ์ตœํ•˜๋‹จ์œผ๋กœ ์˜ฎ๊ธธ ์ˆ˜ ์žˆ์Œ
  • ์š”์†Œ๊ฐ€ ์•„๋‹Œ, ๋ฌธ์„œ์˜ ์Šคํฌ๋กค ์ƒํƒœ๋ฅผ(๊ฐ€๋ ค์ง„ ์ฝ˜ํ…์ธ  ์˜์—ญ ์ •๋ณด) ํ™•์ธํ•  ๋•Œ (์ฐธ๊ณ )
    • `window.pageXOffset`, `window.pageYOffset` (๋ชจ๋“  ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)
    • `document.documentElement.scrollTop`, `document.documentElement.scrollLeft`

 

โบ `mouseEvent.clientX`, `mouseEvent.clientY` : ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด ์™ผ์ชฝ ์ตœ์ƒ๋‹จ์„ ๊ธฐ์ค€์œผ๋กœ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ์ง€์ ๊นŒ์ง€ ์–ผ๋งˆ๋‚˜ ๋–จ์–ด์ ธ ์žˆ๋Š”์ง€ ๋‚˜ํƒ€๋‚ด๋Š” ์ขŒํ‘œ. ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด์ด ๊ธฐ์ค€์ด๋ฏ€๋กœ ์Šคํฌ๋กคํ•ด๋„ ๊ฐ’์ด ๋ณ€ํ•˜์ง€ ์•Š์Œ.

 

โป `mouseEvent.PageX`, `mouseEvent.pageY` : ๋ฌธ์„œ ์™ผ์ชฝ ์ตœ์ƒ๋‹จ์„ ๊ธฐ์ค€์œผ๋กœ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ์ง€์ ๊นŒ์ง€ ์–ผ๋งˆ๋‚˜ ๋–จ์–ด์ ธ ์žˆ๋Š”์ง€ ๋‚˜ํƒ€๋‚ด๋Š” ์ขŒํ‘œ. ๋ฌธ์„œ๊ฐ€ ๊ธฐ์ค€์ด๋ฏ€๋กœ ์Šคํฌ๋กคํ•˜๋ฉด ๊ฐ’๋„ ๋ณ€ํ•จ(์œ„ ์ด๋ฏธ์ง€์—” ํ‘œ์‹œ ์•ˆํ•จ)

 

โผ ์š”์†Œ๋ฅผ ๋๊นŒ์ง€ ์Šคํฌ๋กค ํ–ˆ๋Š”์ง€ ํŒ๋ณ„ํ•˜๊ธฐ

// ๊ฒฐ๊ณผ๊ฐ’์ด true๋ผ๋ฉด ์š”์†Œ์˜ ๋๊นŒ์ง€ ์Šคํฌ๋กคํ•œ ์ƒํƒœ
element.scrollHeight - element.scrollTop === element.clientHeight;

 

โฝ ๋ฌธ์„œ์˜ ์ „์ฒด ๋†’์ด๊ฐ’ ๊ตฌํ•˜๊ธฐ (์ฐธ๊ณ )   

// ๋ฌธ์„œ์˜ ์ •ํ™•ํ•œ ์ „์ฒด ๋†’์ด๋ฅผ ๊ตฌํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ
const scrollHeight = Math.max(
  document.body.scrollHeight,
  document.documentElement.scrollHeight,
  document.body.offsetHeight,
  document.documentElement.offsetHeight,
  document.body.clientHeight,
  document.documentElement.clientHeight,
);
์™œ ์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ ๋ฌธ์„œ ์ „์ฒด ๋†’์ด๋ฅผ ๊ตฌํ•ด์•ผ ํ•˜๋Š” ๊ฑธ๊นŒ์š”? ์ด์œ ๋Š” ์•Œ์•„๋ณด์ง€ ์•Š๋Š” ๊ฒŒ ๋‚ซ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฐ ์ด์ƒํ•œ ๊ณ„์‚ฐ๋ฒ•์€ ์•„์ฃผ ์˜ค๋ž˜ ์ „๋ถ€ํ„ฐ ์žˆ์—ˆ๊ณ  ๊ทธ๋‹ค์ง€ ๋…ผ๋ฆฌ์ ์ด์ง€ ์•Š์€ ์ด์œ ๋กœ ๋งŒ๋“ค์–ด์กŒ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
— JavaScript Info

 

๋ทฐํฌํŠธ ๊ธฐ์ค€ ์š”์†Œ ์œ„์น˜ (์ฐธ๊ณ ์šฉ)

๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด ๊ธฐ์ค€ ์š”์†Œ์˜ ์œ„์น˜ ์ขŒํ‘œ - ์ด๋ฏธ์ง€ ์ถœ์ฒ˜ JavaScript Info

 

`getBoundingClientRect()` ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด์„ ๊ธฐ์ค€์œผ๋กœํ•œ ์š”์†Œ์˜ ์œ„์น˜ ์ขŒํ‘œ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค

const clientRect = mapRef.current.getBoundingClientRect(); // React
const clientRect = elem.getBoundingClientRect(); // Vanilla JS
console.log(clientRect.x);
// ...

 

  • `clientRect.x`, `clientRect.left` : ํ™”๋ฉด ์ขŒ์ธก๋ถ€ํ„ฐ ํ•ด๋‹น ์—˜๋ฆฌ๋จผํŠธ์˜ ์™ผ์ชฝ ๋ณ€๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ
  • `clientRect.y`, `clientRect.top` : ํ™”๋ฉด ์ƒ๋‹จ๋ถ€ํ„ฐ ํ•ด๋‹น ์—˜๋ฆฌ๋จผํŠธ์˜ ์œ„์ชฝ ๋ณ€๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ
  • `clientRect.right` : ํ™”๋ฉด ์ขŒ์ธก์—์„œ ํ•ด๋‹น ์—˜๋ฆฌ๋จผํŠธ์˜ ์˜ค๋ฅธ์ชฝ ๋ณ€๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ
  • `clientRect.bottom` : ํ™”๋ฉด ์ƒ๋‹จ์—์„œ ํ•ด๋‹น ์—˜๋ฆฌ๋จผํŠธ์˜ ์•„๋ž˜์ชฝ ๋ณ€๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ
  • `clientRect.width` : ํ•ด๋‹น ์—˜๋ฆฌ๋จผํŠธ์˜ ๋„ˆ๋น„ (์ฝ˜ํ…์ธ  + padding + border)
  • `clientRect.height` : ํ•ด๋‹น ์—˜๋ฆฌ๋จผํŠธ์˜ ๋†’์ด (์ฝ˜ํ…์ธ  + padding + border)

 

์—˜๋ฆฌ๋จผํŠธ


์ด๋ฏธ์ง€ ์‚ฌ์ด์ฆˆ๊ฐ€ ๋ถ€๋ชจ ์š”์†Œ๋ณด๋‹ค ํฌ๋‹ค๊ณ  ๊ฐ€์ •ํ•œ๋‹ค(๊ทธ๋ž˜์•ผ ์Šคํฌ๋กคํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ).

 

`div`(S.Map)์˜ `scrollLeft`, `scrollTop` (๊ธฐํ•˜ ํ”„๋กœํผํ‹ฐ)์†์„ฑ์— ์ ‘๊ทผํ•ด์•ผํ•˜๋ฏ€๋กœ `useRef()`๋กœ ์ƒ์„ฑํ•œ ๊ฐ์ฒด๋ฅผ `div` ํƒœ๊ทธ์˜ `ref` ์†์„ฑ์— ํ• ๋‹นํ•œ๋‹ค. ๊ทธ๋Ÿผ Ref ๊ฐ์ฒด์˜ `.current` ๊ฐ’์€ `div` ํƒœ๊ทธ๋ฅผ ๊ฐ€๋ฆฌํ‚ค๊ฒŒ ๋œ๋‹ค. ๊ทธ ํ›„ ์•„๋ž˜์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

// scrollLeft๋„ ์žˆ๋‹ค. ์•„๋ž˜๋Š” scrollTop๋งŒ ์˜ˆ์‹œ๋กœ ๋“ฌ
mapRef.current.scrollTop; // ์„ธ๋กœ ์Šคํฌ๋กค ์œ„์น˜ ์กฐํšŒ
mapRef.current.scrollTop = 100; // ์„ธ๋กœ ์Šคํฌ๋กค ์œ„์น˜๋ฅผ 100์œผ๋กœ ์ด๋™(์Šคํฌ๋กค์— ์˜ํ•ด ๊ฐ€๋ ค์ง„ ์ฝ˜ํ…์ธ  ๋†’์ด๊ฐ€ 100px์ด ๋˜๊ฒŒ๋” ์ˆ˜์ •)
mapRef.current.scrollBy(x, y); // ํ˜„์žฌ ์Šคํฌ๋กค ์œ„์น˜์—์„œ x(๊ฐ€๋กœ), y(์„ธ๋กœ)๋งŒํผ ์ด๋™
mapRef.current.scrollTo(pageX, pageY); // ์Šคํฌ๋กค์„ x(๊ฐ€๋กœ), y(์„ธ๋กœ) ์œ„์น˜๋กœ ์ด๋™

 

`img` ํƒœ๊ทธ๋Š” ๋“œ๋ž˜๊ทธํ•  ํ•„์š”๊ฐ€ ์—†์œผ๋ฏ€๋กœ `draggable` ์†์„ฑ์— `false` ๊ฐ’์„ ์ค€๋‹ค.

// Map.js
import React, { useState, useRef, useCallback } from 'react';
import styled, { css } from 'styled-components/macro';

const Map = () => {
  const mapRef = useRef(null);

  return (
    <S.Map ref={mapRef}>
      <img draggable={false} alt="map" src={'...'} />
    </S.Map>
  );
};

 

์š”์†Œ์— ๋งˆ์šฐ์Šค๋ฅผ ์˜ฌ๋ฆฌ๋ฉด ์† ๋ชจ์–‘์œผ๋กœ ๋ฐ”๋€Œ๋„๋ก `cursor` ์†์„ฑ์„ `grab`์œผ๋กœ ์ค€๋‹ค. ์ด๋ฏธ์ง€๊ฐ€ ๋ถ€๋ชจ ์š”์†Œ(S.Map)๋ณด๋‹ค ์ปค๋„ ์Šคํฌ๋กค์ด ์ƒ๊ธฐ์ง€ ์•Š๋„๋ก `overflow`๋Š” `hidden`์œผ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค.

// Map.js - CSS(Styled-Components)

const S = {};

S.Map = styled.div`
  position: relative;
  width: 100%;
  height: 100%;

  cursor: grab;
  overflow: hidden;
`;

 

State


์Šคํฌ๋กค ์ƒํƒœ

๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ๋กœ ๋ฐœ์ƒํ•œ ์ขŒํ‘œ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ์ƒํƒœ ์ •์˜

import React, { useState, useRef, useCallback } from 'react';
import styled, { css } from 'styled-components/macro';

const Map = () => {
  const [pos, setPos] = useState({ top: 0, left: 0, x: 0, y: 0 });
  // ...
};
  • `top` : div(S.Map) ์—˜๋ฆฌ๋จผํŠธ์˜ y(์„ธ๋กœ) ์Šคํฌ๋กค ์œ„์น˜ — ์Šคํฌ๋กค์— ์˜ํ•ด ๊ฐ€๋ ค์ง„ ์˜์—ญ์˜ ์„ธ๋กœ ๊ธธ์ด
  • `left` : div(S.Map) ์—˜๋ฆฌ๋จผํŠธ์˜ x(๊ฐ€๋กœ) ์Šคํฌ๋กค ์œ„์น˜ — ์Šคํฌ๋กค์— ์˜ํ•ด ๊ฐ€๋ ค์ง„ ์˜์—ญ์˜ ๊ฐ€๋กœ ๊ธธ์ด
  • `x` : ๋งˆ์šฐ์Šค ํด๋ฆญ ์ด๋ฒคํŠธ์˜ event.clientX ์ขŒํ‘œ — ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด ๊ธฐ์ค€์˜ X ์ขŒํ‘œ
  • `y` : ๋งˆ์šฐ์Šค ํด๋ฆญ ์ด๋ฒคํŠธ์˜ event.clientY ์ขŒํ‘œ — ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด ๊ธฐ์ค€์˜ Y ์ขŒํ‘œ

 

ํด๋ฆญ ์ƒํƒœ

ํด๋ฆญ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ ์ƒํƒœ ์ •์˜

// Map.js
import React, { useState, useRef, useCallback } from 'react';
import styled, { css } from 'styled-components/macro';

const Map = () => {
  const [isMouseDown, seIsMouseDown] = useState(false);
  // ...
};

 

๋งˆ์šฐ์Šค ํ•ธ๋“ค๋Ÿฌ


React๋Š” ์ƒˆ๋กœ์šด props๋ฅผ ๋ฐ›๊ฑฐ๋‚˜ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์ปดํฌ๋„ŒํŠธ ์•ˆ์˜ ๋ชจ๋“  ํ•จ์ˆ˜๋ฅผ ๋‹ค์‹œ ์ •์˜ํ•˜๋ฏ€๋กœ, `useCallback`์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ’์ด ๋ณ€๊ฒฝ๋์„๋•Œ๋งŒ ์žฌ์ •์˜ํ•˜๋„๋ก ํ•œ๋‹ค.

 

onMouseDown (Tic)

๋งˆ์šฐ์Šค๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ ํ˜ธ์ถœํ•  `startDrag()` ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ •์˜ํ•œ๋‹ค.

 

  1. ํด๋ฆญ ์ƒํƒœ `true` ๋กœ ๋ณ€๊ฒฝ
  2. ํ˜„์žฌ ์š”์†Œ(S.Map)์˜ ์Šคํฌ๋กค ์œ„์น˜ ์ €์žฅ
  3. ๋งˆ์šฐ์Šค๋ฅผ ํด๋ฆญํ•œ ์œ„์น˜ ์ €์žฅ
  4. (CSS) `cursor` ์†์„ฑ์€ `grabbing`์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , `user-select`๋Š” `none`(ํ…์ŠคํŠธ ์„ ํƒ ๋ฐฉ์ง€)์œผ๋กœ ๋ณ€๊ฒฝ

 

// Map.js
import React, { useState, useRef, useCallback } from 'react';
import styled, { css } from 'styled-components/macro';

const Map = () => {
  const [isMouseDown, seIsMouseDown] = useState(false);
  const startDrag = useCallback(({ clientX, clientY }) => {
    seIsMouseDown(true);

    // ํด๋ฆญ ์‹œ์ ์˜ clientX, clientY ์ขŒํ‘œ์™€ ์š”์†Œ์˜ ์Šคํฌ๋กค ์œ„์น˜ ์ €์žฅ
    setPos({
      left: mapRef.current.scrollLeft, // ์š”์†Œ์˜ left ์Šคํฌ๋กค ์œ„์น˜(์Šคํฌ๋กค์— ์˜ํ•ด ๊ฐ€๋ ค์ง„ ์˜์—ญ์˜ ๊ฐ€๋กœ ๊ธธ์ด)
      top: mapRef.current.scrollTop, // ์š”์†Œ์˜ top ์Šคํฌ๋กค ์œ„์น˜(์Šคํฌ๋กค์— ์˜ํ•ด ๊ฐ€๋ ค์ง„ ์˜์—ญ์˜ ์„ธ๋กœ ๊ธธ์ด)
      x: clientX, // ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด(๋ทฐํฌํŠธ) ๊ธฐ์ค€์˜ X ์ขŒํ‘œ(left)
      y: clientY, // ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด(๋ทฐํฌํŠธ) ๊ธฐ์ค€์˜ Y ์ขŒํ‘œ(top)
    });
  }, []);
  // ...

  return (
    <S.Map ref={mapRef} onMouseDown={startDrag} isMouseDown={isMouseDown}>
      <img draggable={false} alt="map" src={'...'} />
    </S.Map>
  );
};
// CSS (styled-components)
S.Map = styled.div`
  // ...
  cursor: grab;
  overflow: hidden;

  ${({ isMouseDown }) =>
    isMouseDown &&
    css`
      cursor: grabbing;
      user-select: none;
    `}
`;

 

onMouseMove (๋“œ๋ž˜๊ทธ) โญ๏ธ

๋งˆ์šฐ์Šค๋ฅผ ๋“œ๋ž˜๊ทธํ–ˆ์„ ๋•Œ ํ˜ธ์ถœํ•  `dragging` ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ •์˜ํ•œ๋‹ค. ํด๋ฆญํ•˜์ง€ ์•Š์€ ์ƒํƒœ๋ผ๋ฉด ์•„๋ฌด ์ž‘์—…๋„ ํ•˜์ง€ ์•Š๊ณ , ๋งŒ์•ฝ ํด๋ฆญํ•œ ์ƒํƒœ๋ผ๋ฉด...

 

โถ ๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋ฅผ ๋ฉˆ์ถ˜ ์ง€์ ๊ณผ, ๋งˆ์šฐ์Šค๋ฅผ ํด๋ฆญํ•œ ์ง€์ ์„ ๋บ€ ๊ฐ’ `dx`, `dy`๋ฅผ ๊ตฌํ•˜๊ณ  — ์Šคํฌ๋กคํ•œ ๋ฒ”์œ„์˜ ๊ฐ’

 

โท ํ˜„์žฌ ์Šคํฌ๋กค ์œ„์น˜์™€, ์Šคํฌ๋กคํ•œ ๊ฐ’์„ ๋บ€ ์œ„์น˜๋กœ ์Šคํฌ๋กค ์ด๋™

 

"์•„๋ž˜(ํด๋ฆญ) → ์œ„" ๋ฐฉํ–ฅ์œผ๋กœ ๋“œ๋ž˜๊ทธ๋Š” ์Šคํฌ๋กค `+`

ํ˜„์žฌ `top`์€ `0`, `clientY`(๋“œ๋ž˜๊ทธ ๋ฉˆ์ถ˜ ์ง€์ ) `30`, `y`(ํด๋ฆญํ•œ ์ง€์ ) `60`์ด๋ผ๊ณ  ๊ฐ€์ •

  • `30 - 60 = -30` → dy(clientY - y)
  • `0 - (-30) = 30` → top - dy
  • ์Šคํฌ๋กค top `30`์œผ๋กœ ์ด๋™ → ์Šคํฌ๋กค top์ด 0์—์„œ 30์œผ๋กœ ๋Š˜์–ด๋‚จ

 

"์œ„(ํด๋ฆญ) → ์•„๋ž˜" ๋ฐฉํ–ฅ์œผ๋กœ ๋“œ๋ž˜๊ทธ๋Š” ์Šคํฌ๋กค `-`

ํ˜„์žฌ `top`์€ `30`, `clientY`(๋“œ๋ž˜๊ทธ ๋ฉˆ์ถ˜ ์ง€์ ) `60`, `y`(ํด๋ฆญํ•œ ์ง€์ ) `30`์ด๋ผ๊ณ  ๊ฐ€์ •

  • `60 - 30 = 30` → dy(clientY - y)
  • `30 - 30 = 0` → top - dy
  • ์Šคํฌ๋กค top `0`์œผ๋กœ ์ด๋™ → ์Šคํฌ๋กค top์ด 30์—์„œ 0์œผ๋กœ ์ค„์–ด๋“ฌ

 

// Map.js
import React, { useState, useRef, useCallback } from 'react';
import styled, { css } from 'styled-components/macro';

const Map = () => {
  const dragging = useCallback(
    ({ clientX, clientY }) => {
      if (isMouseDown) {
        const { x, y, left, top } = pos;
        // (๋งˆ์šฐ์Šค ์ด๋™์„ ๋ฉˆ์ถ˜ ์ง€์ )๊ณผ (๋งˆ์šฐ์Šค๋ฅผ ํด๋ฆญํ•œ ์ง€์ )์„ ๋บ€ ๊ฐ’ → ์Šคํฌ๋กคํ•œ ๋ฒ”์œ„์˜ ๊ฐ’
        const dx = clientX - x;
        const dy = clientY - y;

        // (๋งˆ์šฐ์Šค๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ์˜ ์š”์†Œ scroll ์œ„์น˜)์™€ (์Šคํฌ๋กคํ•œ ๊ฐ’)์„ ๋บ€ ๋งŒํผ ์Šคํฌ๋กค ์ด๋™
        // dy๊ฐ€ -๋ฉด (์•„๋ž˜)์—์„œ (์œ„) ๋ฐฉํ–ฅ ๋“œ๋ž˜๊ทธ์ด๋ฉฐ, -(-dy) → +dy → ์Šคํฌ๋กค +
        // dy๊ฐ€ +๋ฉด (์œ„)์—์„œ (์•„๋ž˜) ๋ฐฉํ–ฅ ๋“œ๋ž˜๊ทธ์ด๋ฉฐ, -(dy) → -dy → ์Šคํฌ๋กค -
        mapRef.current.scrollTo(left - dx, top - dy);
      }
    },
    [isMouseDown, pos],
  );
  // ...

  return (
    <S.Map ref={mapRef} onMouseMove={dragging}>
      <img draggable={false} alt="map" src={'...'} />
    </S.Map>
  );
};

 

onMouseUp (Toc)

๋งˆ์šฐ์Šค ํด๋ฆญ์„ ํ•ด์ œํ•˜๊ฑฐ๋‚˜ ์ด๋ฒคํŠธ ์˜์—ญ์„ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ, ๋งˆ์šฐ์Šค ํด๋ฆญ ์ƒํƒœ `isMouseDown`์„ `false`๋กœ ๋ฐ”๊ฟ€ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ •์˜ํ•œ๋‹ค. ๋งˆ์šฐ์Šค ํด๋ฆญ ์ƒํƒœ๊ฐ€ `false`๊ฐ€ ๋์œผ๋‹ˆ CSS์˜ `cursor` ์†์„ฑ์€ `grab`์œผ๋กœ ๋Œ์•„๊ฐ„๋‹ค.

// Map.js
import React, { useState, useRef, useCallback } from 'react';
import styled, { css } from 'styled-components/macro';

const Map = () => {
  const [isMouseDown, seIsMouseDown] = useState(false);

  const finishDrag = useCallback(({ type }) => {
    if (type === 'mouseup' || type === 'mouseleave') {
      seIsMouseDown(false);
    }
  }, []);
  // ...

  return (
    <S.Map ref={mapRef} onMouseUp={finishDrag} onMouseLeave={finishDrag}>
      <img draggable={false} alt="map" src={''} />
    </S.Map>
  );
};

 

onContextMenu (๋งˆ์šฐ์Šค ์šฐํด๋ฆญ)

๋งˆ์šฐ์Šค๋ฅผ ์šฐํด๋ฆญํ–ˆ์„ ๋•Œ์˜ ์ขŒํ‘œ๋ฅผ ์ €์žฅํ•˜๊ณ , ์ €์žฅํ•œ ์ขŒํ‘œ ์ง€์ ์— ๋งˆ์ปค(์ด๋ฏธ์ง€) ๋“ฑ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ๋„ค์ดํ‹ฐ๋ธŒ ์ด๋ฒคํŠธ์— ์ ‘๊ทผํ•ด์„œ `offsetX`, `offsetY` ๊ฐ’์„ ์ €์žฅํ•˜๋ฉด ๋œ๋‹ค.

 

โšก๏ธ `offsetX`, `offsetY`๋Š” DOM ์ด๋ฒคํŠธ๊ฐ€ ๊ฑธ๋ ค ์žˆ๋Š” ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ณณ์˜ ์ขŒํ‘œ๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค. ํ™”๋ฉด์— ๋ณด์ด์ง€ ์•Š๋Š” ์Šคํฌ๋กค ์˜์—ญ๊นŒ์ง€ ํฌํ•จํ•œ๋‹ค.

 

๋งˆ์šฐ์Šค ์šฐํด๋ฆญ์— ๋Œ€ํ•œ ์ด๋ฒคํŠธ๋Š” `onContextMenu` ์†์„ฑ์„ ์ด์šฉํ•œ๋‹ค. ์šฐํด๋ฆญ ํ›„ ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด๊ฐ€ ์ž๋™์œผ๋กœ ๋‚˜ํƒ€๋‚˜๋Š”๋ฐ ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด ๊ณ ์œ  ๋™์ž‘์„ ๋ง‰์•„์ฃผ๋Š” `e.preventDefault()` ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

// Map.js
import React, { useState, useRef, useCallback } from 'react';
import styled, { css } from 'styled-components/macro';
import Marker from './Maker'; // ๋งˆ์ปค ์ด๋ฏธ์ง€ ์ปดํฌ๋„ŒํŠธ import

const Map = () => {
  const [markers, setMarkers] = useState([]);
  const addMarker = useCallback(
    (e) => {
      setMarkers([
        ...markers,
        [e.nativeEvent.offsetX, e.nativeEvent.offsetY],
        // offsetX/Y: DOM ์ด๋ฒคํŠธ๊ฐ€ ๊ฑธ๋ ค์žˆ๋Š” ์š”์†Œ๋ฅผ ๊ธฐ์ค€(ํ™”๋ฉด์— ๋ณด์ด์ง€ ์•Š๋Š” ์Šคํฌ๋กค ์˜์—ญ ํฌํ•จ)์œผ๋กœ ์ขŒํ‘œ ์ถœ๋ ฅ
      ]);
      e.preventDefault(); // ์šฐํด๋ฆญ ๋ฉ”๋‰ด ์ถœ๋ ฅ ๋ฐฉ์ง€
    },
    [markers, setMarkers],
  );
  // ...

  return (
    <S.Map ref={mapRef} onContextMenu={addMarker}>
      <img draggable={false} alt="map" src={''} />
      {markers.length >= 1
        ? markers.map((markerPos) => (
            <Marker key={''} left={markerPos[0]} top={markerPos[1]} />
          ))
        : null}
    </S.Map>
  );
};

 

์ €์žฅํ•œ `offetX`, `offetY` ์ขŒํ‘œ๊ฐ’์€ ์ž์‹ ์ด๋ฏธ์ง€ ํƒœ๊ทธ์˜ `left`, `top` ํฌ์ง€์…˜ ๊ฐ’์œผ๋กœ ์„ค์ •ํ•œ๋‹ค.

// Marker.js
import React from 'react';
import styled from 'styled-components/macro';
import markerImg from '../assets/marker.png';

const Marker = ({ left, top }) => {
  return (
    <Image
      draggable={false}
      alt="marker"
      src={markerImg}
      left={left}
      top={top}
    />
  );
};

const Image = styled.img`
  position: absolute;
  width: 60px;
  left: ${({ left }) => left}px;
  top: ${({ top }) => top}px;
`;

export default Marker;

 

๋ ˆํผ๋Ÿฐ์Šค


 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•