๋ฐ˜์‘ํ˜•

์™„์„ฑ๋ณธ (ํด๋ฆญ์‹œ ๋ผ์ด๋ธŒ ๋ฐ๋ชจ๋กœ ์ด๋™)

 

TL;DR


  1. ๋“œ๋ž˜๊ทธํ•  ์ˆ˜ ์žˆ๋Š” ์š”์†Œ๋กœ ๋ณ€๊ฒฝ — draggable={true}
  2. ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ — onDragStart ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ
    • ๋“œ๋ž˜๊ทธํ•˜๊ณ  ์žˆ๋Š” ์š”์†Œ์˜ ์ธ๋ฑ์Šค ์ •๋ณด ์ €์žฅ — state.draggedFrom
    • ๋“œ๋ž˜๊ทธ ์ƒํƒœ true๋กœ ๋ณ€๊ฒฝ — state.isDragging
    • ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ ์‹œ์ ์˜ ์—˜๋ฆฌ๋จผํŠธ ๋ฆฌ์ŠคํŠธ ์ €์žฅ — state.originalOrder
  3. ๋งˆ์šฐ์Šค ์ปค์„œ๊ฐ€ ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์— ์žˆ์„ ๋•Œ — onDragOver ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ
    • drop ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก dragOver ๊ธฐ๋ณธ ์ด๋ฒคํŠธ ๋ฐฉ์ง€ — e.preventDefault()
    • ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ ์œ„์น˜์— ์žˆ๋Š” ์š”์†Œ์˜ ์ธ๋ฑ์Šค ์ €์žฅ — state.draggedTo
    • ์—˜๋ฆฌ๋จผํŠธ ์ˆœ์„œ ๋ณ€๊ฒฝ — state.updatedOrder
      ๋“œ๋ž˜๊ทธ์ค‘์ธ ์•„์ดํ…œ์„ ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ ์œ„์น˜(draggedTo ์ธ๋ฑ์Šค)๋กœ ์ด๋™. ๊ธฐ์กด ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ ์œ„์น˜์— ์žˆ๋˜ ์š”์†Œ๋Š” ๋ฐ”๋กœ ๋’ค๋กœ ๋ฐ€๋ฆผ.
  4. ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์—์„œ ๋“œ๋กญํ–ˆ์„ ๋•Œ — onDrop ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ
    • ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•œ ์—˜๋ฆฌ๋จผํŠธ ๋ฆฌ์ŠคํŠธ ๋ Œ๋”
    • ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ๊ด€๋ จ ์ƒํƒœ ์ดˆ๊ธฐํ™”

 

๋ฐฐ๊ฒฝ ์ง€์‹


HTML ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ Web API๋ฅผ ์ด์šฉํ•ด ๋ฆฌ์ŠคํŠธ ์—˜๋ฆฌ๋จผํŠธ์˜ ์ˆœ์„œ๋ฅผ ๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค. ์—˜๋ฆฌ๋จผํŠธ์˜ draggable ์†์„ฑ์„ true๋กœ ์ฃผ๋ฉด ํ•ด๋‹น ์š”์†Œ๋Š” ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด๊ฐ€ ๋œ๋‹ค. ์ด๋ฏธ์ง€, ๋งํฌ, ์„ ํƒํ•œ ํ…์ŠคํŠธ(ํ…์ŠคํŠธ ๋ธ”๋ก)๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋“œ๋ž˜๊ทธํ•  ์ˆ˜ ์žˆ๋‹ค. ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅ ์ƒํƒœ๊ฐ€ ๋˜๋ฉด onDragStart ๊ฐ™์€ ๋“œ๋ž˜๊ทธ ๊ด€๋ จ ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. onDragStart๋Š” ๋“œ๋ž˜๊ทธ๊ฐ€ ์‹œ์ž‘๋˜๋ฉด ํŠธ๋ฆฌ๊ฑฐ๋˜๋Š” ์ด๋ฒคํŠธ๋‹ค.

// ๋ฆฌ์ŠคํŠธ ์š”์†Œ
<div draggable="true" onDragStart={startDragging}>
  Drag Me ๐Ÿฐ
</div>;

 

์—˜๋ฆฌ๋จผํŠธ์— onDrop๊ณผ onDragOver ์ด๋ฒคํŠธ๋ฅผ ๊ฑธ๋ฉด ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ(์œ ํšจํ•œ ๋“œ๋กญ ๋Œ€์ƒ)์ด ๋œ๋‹ค. ์ด ๋‘ ์ด๋ฒคํŠธ๋ฅผ ํ™œ์šฉํ•ด ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. onDragOver๋Š” ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ์— ์žˆ๋Š” ์š”์†Œ๊ฐ€ ์œ ํšจํ•œ ๋“œ๋กญ ๋Œ€์ƒ์ผ ๋•Œ ํŠธ๋ฆฌ๊ฑฐ ๋˜๋ฉฐ, onDrop์€ ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์—์„œ ๋“œ๋กญํ–ˆ์„ ๋•Œ ํŠธ๋ฆฌ๊ฑฐ๋˜๋Š” ์ด๋ฒคํŠธ๋‹ค.

<section onDrop={updateDragAndDropState} onDragOver={receiveDraggedElements}>
  Drop here ๐Ÿคฒ๐Ÿป
</section>;

 

๋“œ๋ž˜๊ทธํ•œ (์š”์†Œ)๋ฐ์ดํ„ฐ์™€ ์ƒํ˜ธ ์ž‘์šฉํ•˜๊ธฐ ์œ„ํ•ด setData()์™€ getData() ๊ฐ™์€ ์ด๋ฒคํŠธ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ํ˜„์žฌ ๋“œ๋ž˜๊ทธ ํ•˜๊ณ  ์žˆ๋Š” ์š”์†Œ ์ •๋ณด(id ๋“ฑ)๋ฅผ setData()๋ฅผ ํ†ตํ•ด ์ €์žฅํ•˜๊ณ , ๋“œ๋กญํ–ˆ์„ ๋•Œ getData()๋ฅผ ํ†ตํ•ด ์š”์†Œ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜จ ํ›„ ์žฌ์ •๋ ฌ ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

event.dataTransfer.setData(key, value); // ์—ฌ๋Ÿฌ key๋ฅผ ์ด์šฉํ•ด ๋‹ค์ˆ˜์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค
event.dataTransfer.getData(key);

 

์ƒํƒœ


๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ ๋ฐ”๊นฅ ์˜์—ญ์— ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ Œ๋”ํ•  ์ƒํƒœ(items)์™€, ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ๊ณผ ๊ด€๋ จํ•œ ์ƒํƒœ์˜ ์ดˆ๊ธฐ๊ฐ’์„ ์ •์˜ํ•œ๋‹ค. ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ์ƒํƒœ์—” ๋“œ๋ž˜๊ทธ ์ค‘์ธ ์š”์†Œ์˜ ์ธ๋ฑ์Šค, ์—…๋ฐ์ดํŠธ ์ „ํ›„์˜ ๋ Œ๋” ๋ฆฌ์ŠคํŠธ(items), ๋“œ๋ž˜๊ทธ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๋Š” ๊ณณ์ด๋‹ค.

const items = [
  { number: "3", title: "๐Ÿ‘จ๐Ÿป‍๐Ÿ’ป Tech Man" },
  // ์ƒ๋žต
];

const initialDnDState = {
  draggedFrom: null, // ๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•œ ์š”์†Œ์˜(๋งˆ์šฐ์Šค๋ฅผ ํด๋ฆญํ•˜์—ฌ ์›€์ง์ธ ์š”์†Œ) ์ธ๋ฑ์Šค
  draggedTo: null, // ๋“œ๋กญ ๋Œ€์ƒ ์š”์†Œ์˜ ์ธ๋ฑ์Šค(๋“œ๋ž˜๊ทธํ•˜์—ฌ ๋งˆ์šฐ์Šค ์ปค์„œ๊ฐ€ ์œ„์น˜ํ•œ ์š”์†Œ์˜ ์ธ๋ฑ์Šค)
  isDragging: false, // ๋“œ๋ž˜๊ทธ ์—ฌ๋ถ€ Boolean
  originalOrder: [], // ๋“œ๋กญํ•˜๊ธฐ์ „(์ˆœ์„œ๊ฐ€ ๋ฐ”๋€Œ๊ธฐ ์ „) ๊ธฐ์กด list
  updatedOrder: [], // ๋“œ๋กญํ•œ ํ›„ ์ˆœ์„œ๊ฐ€ ๋ฐ”๋€ list
};

 

useState ํ›…์„ ์ด์šฉํ•ด ๋ Œ๋” ๋ฆฌ์ŠคํŠธ์™€ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ์ดˆ๊ธฐ๊ฐ’์„ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€ ์ƒํƒœ๋กœ ๋‘”๋‹ค.

const Lists = () => {
  const [list, setList] = useState(items); // ๋ Œ๋”๋  ์š”์†Œ
  const [dragAndDrop, setDragAndDrop] = useState(initialDnDState); // D&D ๊ด€๋ จ ์ƒํƒœ

  return (
    <section>
      <ul>
        {list.map((item, index) => (
          <li>...</li>
        ))}
      </ul>
    </section>
  );
};

 

๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ์š”์†Œ๋กœ ๋ณ€๊ฒฝ


โžŠ๋งํฌ โž‹์ด๋ฏธ์ง€ โžŒ์„ ํƒํ•œ ํ…์ŠคํŠธ(ํ…์ŠคํŠธ ๋ธ”๋ก)๋Š” ๊ธฐ๋ณธ๊ฐ’์ด ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅ์ด๋‹ค(draggable="ture")

 

<li> ํƒœ๊ทธ์˜ draggable ์†์„ฑ์„ true๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ์š”์†Œ๊ฐ€ ๋˜๋„๋ก ํ•œ๋‹ค. ํ˜„์žฌ ๋“œ๋ž˜๊ทธ ํ•˜๊ณ  ์žˆ๋Š” ์š”์†Œ๋ฅผ ํŒ๋ณ„ํ•˜๊ณ  ์žฌ์ •๋ ฌํ•  ๋•Œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์š”์†Œ์˜ index๋ฅผ data ์†์„ฑ์— ํ• ๋‹นํ•œ๋‹ค.

 

data ์†์„ฑ์€ data-๋ฐ์ดํ„ฐ์ด๋ฆ„ ํ˜•์‹์œผ๋กœ ์ •์˜ํ•˜๊ณ , elem.dataset.๋ฐ์ดํ„ฐ์ด๋ฆ„ ์œผ๋กœ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค.

return (
  <section>
    <ul>
      {list.map((item, index) => (
        <li draggable={true} data-position={index}>
          <span>{item.number}</span>
          <p>{item.title}</p>
        </li>
      ))}
    </ul>
  </section>
);

 

๋“œ๋ž˜๊ทธ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ


onDragOver์™€ onDrop ์ด๋ฒคํŠธ๊ฐ€ ๊ฑธ๋ ค์žˆ๋Š” DOM์ด ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ(์œ ํšจํ•œ ๋“œ๋กญ ๋Œ€์ƒ)์ด ๋œ๋‹ค.

 

onDragStart, onDragOver, onDrop ๋ฐ onDragLeave(์„ ํƒ) 3๊ฐœ์˜ ์ด๋ฒคํŠธ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

 

  • onDragStart : ๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•  ๋•Œ(๋งˆ์šฐ์Šค๋ฅผ ํด๋ฆญํ•œ ํ›„ ์›€์ง์ผ ๋•Œ) ํŠธ๋ฆฌ๊ฑฐ
  • onDragOver : ๋งˆ์šฐ์Šค ์•„๋ž˜์— ์žˆ๋Š” ์š”์†Œ๊ฐ€ ์œ ํšจํ•œ ๋“œ๋กญ ๋Œ€์ƒ์ผ ๋•Œ(์•„์ดํ…œ์ด ๋“œ๋กญ๋  ์ˆ˜ ์žˆ๋Š” ๊ณณ) ํŠธ๋ฆฌ๊ฑฐ
    → ๋งˆ์šฐ์Šค๊ฐ€ ์œ ํšจํ•œ ๋“œ๋กญ ๋Œ€์ƒ ์œ„์— ์œ„์น˜ํ•ด ์žˆ๋‹ค๋ฉด onDragOver ์ด๋ฒคํŠธ๊ฐ€ ์ˆ˜๋ฐฑ ๋ฐ€๋ฆฌ ์ดˆ๋งˆ๋‹ค ๊ณ„์† ํŠธ๋ฆฌ๊ฑฐ ๋จ
  • onDrop : ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์—์„œ ๋งˆ์šฐ์Šค ํด๋ฆญ์„ ํ•ด์ œํ•˜์—ฌ ๋“œ๋กญํ–ˆ์„ ๋•Œ ํŠธ๋ฆฌ๊ฑฐ
  • onDragLeave : ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์„ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ ํŠธ๋ฆฌ๊ฑฐ
  • onDragEnter : ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์œผ๋กœ(๋Œ€์ƒ ๊ฐ์ฒด) ์ฒ˜์Œ ์ง„์ž…ํ–ˆ์„ ๋•Œ ํŠธ๋ฆฌ๊ฑฐ

 

onDragStart — ๋“œ๋ž˜๊ทธ ์‹œ์ž‘

๋“œ๋ž˜๊ทธํ•˜๋Š” ๋™์•ˆ draggable ์š”์†Œ๋Š” ๋ฐ˜ํˆฌ๋ช… ํ˜•ํƒœ๋กœ ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ๋ฅผ ๋”ฐ๋ผ๋‹ค๋‹Œ๋‹ค

 

๋งˆ์šฐ์Šค๋กœ ์š”์†Œ๋ฅผ ํด๋ฆญํ•˜๊ณ  ๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•˜๋ฉด onDragStart ์ด๋ฒคํŠธ๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ ๋œ๋‹ค. ์ด ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„  โžŠ๋“œ๋ž˜๊ทธํ•˜๊ณ  ์žˆ๋Š” ์š”์†Œ์˜ ์ธ๋ฑ์Šค ์ •๋ณด๋ฅผ ์กฐํšŒํ•œ ํ›„ ์ €์žฅํ•˜๊ณ , โž‹๋“œ๋ž˜๊ทธ ์ƒํƒœ๋ฅผ true๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , โžŒํ•ด๋‹น ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ์‹œ์ ์˜ ์—˜๋ฆฌ๋จผํŠธ ๋ฆฌ์ŠคํŠธ ์ƒํƒœ๋ฅผ ์ €์žฅํ•œ๋‹ค.

const onDragStart = (e) => {
  const initialPosition = Number(e.target.dataset.position);
  setDragAndDrop({
    ...dragAndDrop,
    draggedFrom: initialPosition, // ๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•œ ์š”์†Œ์˜ ์ธ๋ฑ์Šค
    isDragging: true,
    originalOrder: list, // ํ˜„์žฌ list ์ƒํƒœ ์ €์žฅ
  });
};

 

onDragOver — ๋“œ๋กญ ๊ฐ€๋Šฅ ์˜์—ญ โšก๏ธ

onDragOver ์ด๋ฒคํŠธ๋Š” ์ˆ˜๋ฐฑ ๋ฐ€๋ฆฌ์ดˆ๋งˆ๋‹ค ๋ฐœ๋™ํ•˜๊ณ  ์ด์™€ ๋น„์Šทํ•œ onDragEnter๋Š” ๋‹จ ํ•œ๋ฒˆ๋งŒ ๋ฐœ๋™ํ•œ๋‹ค.

 

๋“œ๋ž˜๊ทธ ์ƒํƒœ์—์„œ ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ์— ์žˆ๋Š” ์š”์†Œ๊ฐ€ ์œ ํšจํ•œ ๋“œ๋กญ ๋Œ€์ƒ์ผ ๋•Œ onDragOver ์ด๋ฒคํŠธ๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋œ๋‹ค. ์ด๋•Œ ์ด๋ฒคํŠธ target(event.target)์€ ๋งˆ์šฐ์Šค ์ปค์„œ ์•„๋ž˜์˜ ์š”์†Œ๋ฅผ ๊ฐ€๋ฆฌํ‚จ๋‹ค. ์ด ํ•ธ๋“ค๋Ÿฌ์—์„  ์š”์†Œ๋ฅผ ๋“œ๋กญํ–ˆ์„ ๋•Œ ๋ Œ๋”ํ•  ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฏธ๋ฆฌ ์ค€๋น„ํ•ด๋‘๋Š” ์ž‘์—…(๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ์— ๋”ฐ๋ผ ์š”์†Œ ์ˆœ์„œ ๋ณ€๊ฒฝ)์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

 

โถ onDragOver, onDragEnter ์ด๋ฒคํŠธ๋Š” ๋“œ๋กญ ์ด๋ฒคํŠธ๋ฅผ ์ทจ์†Œ์‹œํ‚ค๋Š” ๊ธฐ๋ณธ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ€์ง„๋‹ค. ๋“œ๋กญ ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด(๋“œ๋กญ ํ—ˆ์šฉ) e.preventDefault() ๋ฉ”์„œ๋“œ๋กœ ๊ธฐ๋ณธ ๋™์ž‘์„ ๋ฐฉ์ง€ํ•œ๋‹ค — ์ฐธ๊ณ  ๋งํฌ

 

โท ํ˜„์žฌ ๋งˆ์šฐ์Šค๊ฐ€ hover๋˜๊ณ  ์žˆ๋Š”(๊ฐ€๋ฆฌํ‚ค๊ณ  ์žˆ๋Š”) ์˜์—ญ์˜ item ์ธ๋ฑ์Šค ์ €์žฅ — draggedTo

 

โธ ํ˜„์žฌ ๋“œ๋ž˜๊ทธํ•˜๊ณ  ์žˆ๋Š” ์š”์†Œ๋ฅผ ์ œ์™ธํ•œ item ๋ฆฌ์ŠคํŠธ ์ €์žฅ — remainingItems

 

โน ๋ฆฌ์ŠคํŠธ ์ˆœ์„œ ๋ณ€๊ฒฝ. ํ˜„์žฌ ๋“œ๋ž˜๊ทธ์ค‘์ธ ์•„์ดํ…œ์„ draggedTo ์ธ๋ฑ์Šค๋กœ(์œ„์น˜)๋กœ ์ถ”๊ฐ€ — updatedOrder

๋”๋ณด๊ธฐ

๋“œ๋ž˜๊ทธํ•˜๊ณ  ์žˆ๋Š” ์š”์†Œ๊ฐ€ `4`(3๋ฒˆ ์ธ๋ฑ์Šค), ํ˜ธ๋ฒ„์ค‘์ธ ์š”์†Œ๊ฐ€ `2`(1๋ฒˆ ์ธ๋ฑ์Šค)๋ผ๊ณ  ๊ฐ€์ •. ์š”์†Œ `4`๊ฐ€ ์š”์†Œ `2`์˜ ์œ„์น˜๋กœ ์ด๋™ํ•˜๊ณ , ์š”์†Œ `2`๋Š” ์š”์†Œ `4` ๋’ค๋กœ ๊ฐ€๊ฒŒ๋” ๋ณ€๊ฒฝํ•œ๋‹ค

 

const list = [1, 2, 3, 4, 5]; // ์›ํ•˜๋Š” ๊ฒฐ๊ณผ๊ฐ’ [1, 4, 2, 3, 5]
const filtered = list.filter((el) => el !== 4); // [1, 2, 3, 5] ๋“œ๋ž˜๊ทธํ•˜๊ณ  ์žˆ๋Š” ์š”์†Œ ์ œ์™ธ
const from = filtered.slice(0, 1); // [1]
const to = filtered.slice(1); // [2, 3, 5]
const result = [...from, 4, ...to]; // [1, 4, 2, 3, 5]

 

โบ dragAndDrop ์ƒํƒœ์˜ updatedOrder ๋ฐ draggedTo ์—…๋ฐ์ดํŠธ. ๋งŒ์•ฝ dragAndDrop ์ƒํƒœ์— ์ €์žฅํ•œ draggedTo์™€ ํ˜„์žฌ ์ด๋ฒคํŠธ๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋ผ์„œ ์ €์žฅํ•œ draggedTo ๊ฐ’์ด ๊ฐ™๋‹ค๋ฉด ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋Š” ์Šคํ‚ตํ•œ๋‹ค.

const onDragOver = (e) => {
  e.preventDefault(); // onDragOver ๊ธฐ๋ณธ ์ด๋ฒคํŠธ ๋ฐฉ์ง€
  const draggedTo = Number(e.target.dataset.position); // ํ˜„์žฌ hover ๋˜๊ณ  ์žˆ๋Š”(๋งˆ์šฐ์Šค๊ฐ€ ์œ„์น˜ํ•œ) item์˜ ์ธ๋ฑ์Šค
  const { originalOrder, draggedFrom } = dragAndDrop; // ๊ธฐ์กด ๋ฆฌ์ŠคํŠธ ๋ฐ ๋“œ๋ž˜๊ทธ์ค‘์ธ ์š”์†Œ์˜ ์ธ๋ฑ์Šค ์กฐํšŒ
  const remainingItems = originalOrder.filter(
    (_, index) => index !== draggedFrom, // ํ˜„์žฌ ๋“œ๋ž˜๊ทธ ํ•˜๊ณ  ์žˆ๋Š” ์š”์†Œ๋ฅผ ์ œ์™ธํ•œ items ๋ชฉ๋ก
  );

  // ๋ฆฌ์ŠคํŠธ ์ˆœ์„œ ๋ณ€๊ฒฝ.
  // ํ˜„์žฌ ๋“œ๋ž˜๊ทธ์ค‘์ธ ์•„์ดํ…œ์„ draggedTo(ํ˜„์žฌ ๋งˆ์šฐ์Šค๊ฐ€ ์œ„์น˜ํ•œ) ์ธ๋ฑ์Šค ์œ„์น˜๋กœ ์ถ”๊ฐ€
  const updatedOrder = [
    ...remainingItems.slice(0, draggedTo),
    originalOrder[draggedFrom], // ํ˜„์žฌ ๋“œ๋ž˜๊ทธ์ค‘์ธ ์•„์ดํ…œ
    ...remainingItems.slice(draggedTo),
  ];

  // ์ˆœ์„œ ๋ณ€๊ฒฝํ•œ ๋ฆฌ์ŠคํŠธ ๋ฐ hover ์š”์†Œ์˜ ์ธ๋ฑ์Šค(draggedTo) ์—…๋ฐ์ดํŠธ.
  // ์ƒํƒœ์— ์ €์žฅํ•œ hover ์š”์†Œ ์ธ๋ฑ์Šค์™€ ๊ฐ™์ง€ ์•Š์„๋•Œ๋งŒ ์—…๋ฐ์ดํŠธํ•œ๋‹ค
  if (draggedTo !== dragAndDrop.draggedTo) {
    setDragAndDrop({
      ...dragAndDrop,
      updatedOrder, // ๋“œ๋กญ ์ด๋ฒคํŠธ๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋˜๋ฉด ํ•ด๋‹น ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋ Œ๋”๋จ
      draggedTo,
    });
  }
};

 

onDrop — ๋“œ๋กญ

๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์—์„œ ๋“œ๋กญํ–ˆ์„ ๋•Œ onDrop ์ด๋ฒคํŠธ๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋œ๋‹ค. onDropOver์—์„œ ์ž‘์—…ํ–ˆ๋˜, ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ Œ๋”๋  ๋ฆฌ์ŠคํŠธ(list)๋กœ ์—…๋ฐ์ดํŠธํ•˜๊ณ  dragAndDrop ์ƒํƒœ๋ฅผ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.

const onDrop = () => {
  // onDropOver์—์„œ ์ž‘์—…ํ•ด๋‘”(๋งˆ์šฐ์Šค ์ปค์„œ์— ๋”ฐ๋ผ ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•œ) ์š”์†Œ ๋ฆฌ์ŠคํŠธ ์—…๋ฐ์ดํŠธ
  setList(dragAndDrop.updatedOrder);

  // dragAndDrop ์ƒํƒœ ์ดˆ๊ธฐํ™”
  setDragAndDrop({
    ...dragAndDrop,
    draggedFrom: null,
    draggedTo: null,
    isDragging: false,
  });
};

 

onDragLeave

๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์„ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ onDragLeave ์ด๋ฒคํŠธ๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋œ๋‹ค. ๋“œ๋กญ ๋Œ€์ƒ ์š”์†Œ์— ๋Œ€ํ•œ ์ธ๋ฑ์Šค๋ฅผ ๋‹ด๋Š” ์ƒํƒœ์ธ DraggedTo ์ƒํƒœ ๊ฐ’์€ null๋กœ ์„ค์ •ํ•œ๋‹ค — ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์„ ๋ฒ—์–ด๋‚˜์„œ ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์š”์†Œ ์ž์ฒด๊ฐ€ ์—†์œผ๋ฏ€๋กœ

const onDragLeave = () => {
  setDragAndDrop({
    ...dragAndDrop,
    draggedTo: null,
  });
};

 

ํ•ธ๋“ค๋Ÿฌ ํ• ๋‹น


์œ„์—์„œ ์ž‘์„ฑํ•œ ํ•ธ๋“ค๋Ÿฌ๋ฅผ Drop ์˜์—ญ์ด ๋  ๊ณณ์ธ <li> ํƒœ๊ทธ์— ํ• ๋‹นํ•œ๋‹ค. onDragOver์™€ onDrop ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ• ๋‹นํ•˜๋ฉด, ํ•ด๋‹น ์š”์†Œ๋Š” ๋“œ๋กญ ๊ฐ€๋Šฅํ•œ ์˜์—ญ์ด ๋œ๋‹ค.

return (
  <section>
    <ul>
      {list.map((item, index) => (
        <li
          // ...
          onDragStart={onDragStart}
          onDragLeave={onDragLeave}
          onDragOver={onDragOver}
          onDrop={onDrop}
        ></li>
        // ...
      ))}
    </ul>
  </section>
);

 

CSS


Placeholder ์Šคํƒ€์ผ

DROP HERE ๋ถ€๋ถ„์ด Placeholder

 

๋“œ๋กญํ•  ์œ„์น˜์— ์ ์„  ๋ชจ์–‘์˜ Placeholder ์Šคํƒ€์ผ์„ ์ ์šฉํ•ด ์‹œ๊ฐ์  ํžŒํŠธ๋ฅผ ์ค„ ์ˆ˜ ์žˆ๋‹ค. draggedTo ์ƒํƒœ(๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•œ ํ›„ ๋งˆ์šฐ์Šค ์ปค์„œ๊ฐ€ ์œ„์น˜ํ•œ ์š”์†Œ์˜ ์ธ๋ฑ์Šค)์™€ ๋ Œ๋”ํ•œ ์š”์†Œ์˜ ์ธ๋ฑ์Šค๊ฐ€ ๊ฐ™๋‹ค๋ฉด .dropArea ํด๋ž˜์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ํ•ด๋‹น ํด๋ž˜์Šค์— ๋Œ€ํ•œ placeholder ์Šคํƒ€์ผ์„ ์ง€์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

return (
  <section>
    <ul>
      {list.map((item, index) => (
        <li
          className={dragAndDrop?.draggedTo === Number(index) ? "dropArea" : ""}
          // ...
        ></li>
        // ...
      ))}
    </ul>
  </section>
);

 

์Šคํƒ€์ผ์„ ์œ„ํ•œ ๋ชฉ์ ์ด๋ฏ€๋กœ ๋”ฐ๋กœ ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๊ณ  ::before ์ˆ˜๋„ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ด์šฉํ•œ๋‹ค. ๋“œ๋ž˜๊ทธ๋ฅผ ์‹œ์ž‘ํ•˜์—ฌ ๋งˆ์šฐ์Šค ์ปค์„œ๊ฐ€ ์œ„์น˜ํ•œ ์š”์†Œ์˜ <li> ์Šคํƒ€์ผ์€ .dropArea ํด๋ž˜์Šค ์Šคํƒ€์ผ์ด ์ ์šฉ๋œ๋‹ค.

li {
  // ...
  &.dropArea {
    color: white !important; // ์ด๋ชจํ‹ฐ์ฝ˜์„ ์ œ์™ธํ•œ ํ…์ŠคํŠธ ๋ชจ๋‘ ํฐ์ƒ‰์œผ๋กœ
    background: white !important;
    position: relative;

    &::before {
      content: "Drop Here";
      color: "#687bf7";
      font-size: 0.5em;
      text-transform: uppercase; // ๋Œ€๋ฌธ์ž
      width: 100%;
      height: 100%;
      border: 2px dashed "#687bf7"; // ์ ์„ 
      border-radius: 3px;
      position: absolute;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    span {
      display: none; // ์•„์ดํ…œ ๋„˜๋ฒ„ ๊ฐ์ถ”๊ธฐ
    }

    p {
      margin-left: 1em;
    }
  }
  // ...
}

 

Margin ์—ฌ๋ฐฑ ์• ๋‹ˆ๋ฉ”์ด์…˜

transition ์†์„ฑ์„ ํ™œ์šฉํ•ด ๋งˆ์šฐ์Šค๋ฅผ ๋“œ๋ž˜๊ทธํ•˜์—ฌ Placeholder ์Šคํƒ€์ผ์ด ์ ์šฉ๋˜๊ณ , ๋‹ค์‹œ ๊ธฐ์กด <li> ์Šคํƒ€์ผ๋กœ ๋Œ์•„์˜ฌ ๋•Œ๋งˆ๋‹ค ์š”์†Œ์˜ ํ…์ŠคํŠธ๊ฐ€ ์›€์ง์ด๋Š” ๋“ฏํ•œ ํšจ๊ณผ๋ฅผ ์ค„ ์ˆ˜๋„ ์žˆ๋‹ค.

li {
  p {
    // margin-left๊ฐ€ ์ ์šฉ๋˜๊ธฐ๊นŒ์ง€์˜ ์‹œ๊ฐ„์„ 50ms๋กœ ์„ค์ •
    transition: margin-left 50ms ease-in-out;
  }
}

 

๋ ˆํผ๋Ÿฐ์Šค


 


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