[React] ๋ฆฌ์กํธ ๋ง์ฐ์ค ๋๋๊ทธ ๊ฐ๋ฅํ ์์ ๋ง๋ค๊ธฐ / ๊ธฐํ ํ๋กํผํฐ
์์ ์์์ ์ฝํ ์ธ ๊ฐ ๋ถ๋ชจ ์์๋ณด๋ค ํฌ๋ค๋ฉด, ๋ถ๋ชจ ์์์ ์คํฌ๋กค์ด ์๊ธฐ๊ณ , ๋ง์ฐ์ค ํ ๋ก ์คํฌ๋กคํ ์ ์๋ค. ๋ง์ฐ์ค ํ ์ธ์๋ 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ํ ๋น
๊ธฐํ ํ๋กํผํฐ

๋ฉ์๋๋ณ ์ธก์ ๊ธฐ์ค โญ๏ธ
- 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
๋ทฐํฌํธ ๊ธฐ์ค ์์ ์์น (์ฐธ๊ณ ์ฉ)

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() ํธ๋ค๋ฌ๋ฅผ ์ ์ํ๋ค.
- ํด๋ฆญ ์ํ
true๋ก ๋ณ๊ฒฝ - ํ์ฌ ์์(S.Map)์ ์คํฌ๋กค ์์น ์ ์ฅ
- ๋ง์ฐ์ค๋ฅผ ํด๋ฆญํ ์์น ์ ์ฅ
- (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;
๋ ํผ๋ฐ์ค
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [Algorithm] ํน์ ์ ๊น์ง์ ํฉ ๊ตฌํ๊ธฐ / ๋ฑ์ฐจ์์ด (๊ฐ์ฐ์ค ๊ณต์) (0) | 2024.04.28 |
|---|---|
| [CS] ์ง๋ฒ ๊ณ์ฐ ๋ฐฉ๋ฒ โ 10์ง์ โ 2์ง์ ๋ณํ (0) | 2024.04.28 |
| [JS] ์๋ฐ์คํฌ๋ฆฝํธ URL ๊ฐ์ฒด / searchParams (0) | 2024.04.27 |
| [JS] ์๋ฐ์คํฌ๋ฆฝํธ Map / Set ์๋ฃ๊ตฌ์กฐ (0) | 2024.04.27 |
| [Web] ์ธ์ vs ์ฟ ํค vs ํ ํฐ (0) | 2024.04.27 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[Algorithm] ํน์ ์ ๊น์ง์ ํฉ ๊ตฌํ๊ธฐ / ๋ฑ์ฐจ์์ด (๊ฐ์ฐ์ค ๊ณต์)
[Algorithm] ํน์ ์ ๊น์ง์ ํฉ ๊ตฌํ๊ธฐ / ๋ฑ์ฐจ์์ด (๊ฐ์ฐ์ค ๊ณต์)
2024.04.28 -
[CS] ์ง๋ฒ ๊ณ์ฐ ๋ฐฉ๋ฒ — 10์ง์ โ 2์ง์ ๋ณํ
[CS] ์ง๋ฒ ๊ณ์ฐ ๋ฐฉ๋ฒ — 10์ง์ โ 2์ง์ ๋ณํ
2024.04.28 -
[JS] ์๋ฐ์คํฌ๋ฆฝํธ URL ๊ฐ์ฒด / searchParams
[JS] ์๋ฐ์คํฌ๋ฆฝํธ URL ๊ฐ์ฒด / searchParams
2024.04.27 -
[JS] ์๋ฐ์คํฌ๋ฆฝํธ Map / Set ์๋ฃ๊ตฌ์กฐ
[JS] ์๋ฐ์คํฌ๋ฆฝํธ Map / Set ์๋ฃ๊ตฌ์กฐ
2024.04.27