본문 바로가기

FrontEnd

React DnD 튜토리얼

반응형

원문 https://react-dnd.github.io/react-dnd/docs/tutorial

 

React DnD

 

react-dnd.github.io

마이크로소프트 엔지니어의 컴포넌트 아키텍처 설계 프로세스가 드러나는 읽을만한 글이라 정리해둔다

들어가기에 앞서 : React DnD의 Concept

아이템과 타입

  • Item(아이템) : 진실의 소스는 데이터다
    • DOM이나 컴포넌트가 아니라 아이템(데이터-자바스크립트 객체)가 드래그된다.
    • 카드 : { cardId: 42 ,type:'card'}
    • 체스 : { fromCell: 'C5', piece: 'queen' , type:"piece"}
  • type(타입)
    • 객체지향의 클래스와 비슷한 의미를 갖는다.
    • 타입 간의 상호작용을 정의한다.

모니터(collect 함수)

  • 드래그 앤 드롭은 기본적으로 stateful하다
    • 드래그 작업이 진행 중이거나 아니다.
    • 아이템과 타입이 있거나 없다.
    • 드래그 혹은 드랍 할 수 있다 없다.

모니터 객체는 React DnD의 해당 상태를 컴포넌트에 노출하기 위한 수단이다.

모니터 객체를 collect 함수로 감싸, 하이레벨 상태를 컴포넌트에 노출할 수 있다.

 

piece를 드래그 할 때 Chess cell을 강조하고 싶다.

Cell 컴포넌트에서 collect 함수를 다음과 같이 정의하여 사용한다.

function collect(monitor) {
  return {
    highlighted: monitor.canDrop(),
    hovered: monitor.isOver()
  }
}

이제 Cell 컴포넌트에서 highlighted, hovered 속성을 사용할 수 있다.

커넥터

Hooks Overview

훅으로 대체되어 생략

드래그 소스 & 드랍 타겟

드래그앤 드랍 이벤트에 대한 응답으로 사이드 이펙트를 어떻게 수행합니까?
React DnD의 기본 추상화 단위인 드래그 소스와 드롭 타겟을 통해,
타입, 아이템, 이펙트 및 collect 함수를 컴포넌트와 연결합니다.
 
컴포넌트 또는 그 일부를 드래그 가능하게 만들 때마다 해당 컴포넌트를 드래그 소스 선언으로 래핑해야 합니다.
모든 드래그 소스는 특정 타입에 대해 등록되며 컴포넌트의 props에서 항목을 생성하는 메서드를 구현해야 합니다.
드래그 앤 드롭 이벤트를 처리하기 위한 몇 가지 다른 방법을 선택적으로 지정할 수도 있습니다.
드래그 소스 선언을 통해 주어진 컴포넌트에 대해 collect 함수를 특정할 수 있씁니다.
 
드랍 타겟은 드래그 소스와 매우 유사합니다.
유일한 차이점은 단일 드랍 타겟은 여러 아이템 타입을 한 번에 등록할 수 있으며
아이템을 생성하는 대신 해당 개체의 호버 또는 드랍을 처리할 수 있다는 것입니다.

백엔드

React DnD는 HTML5 drag and drop API를 사용합니다.
드래그한 DOM 노드를 스크린샷하고 즉시 "드래그 프리뷰"로 사용하기 때문에 합리적인 기본값입니다.
커서가 움직일 때 그림을 그릴 필요가 없어 편리합니다.
이 API는 파일 드랍 이벤트를 처리하는 유일한 방법이기도 합니다.
 
불행히도 HTML5 드래그 앤 드롭 API에도 몇 가지 단점이 있습니다.
터치 스크린에서는 작동하지 않으며 다른 브라우저보다 IE에서 더 적은 커스터마이징 기회를 제공합니다.
 
이것이 HTML5 드래그 앤 드롭 지원이 React DnD에서 플러그 인 방식으로 구현되는 이유입니다.
터치 이벤트, 마우스 이벤트 또는 완전히 다른 것을 기반으로 다른 구현을 작성할 수 있습니다.
이러한 플러그인 스타일 구현을 React DnD에서 백엔드라고 합니다.
라이브러리는 현재 HTML backend와 함께 제공되며 이는 대부분의 웹 애플리케이션에 충분합니다.
모바일 웹 애플리케이션에 사용할 수 있는 Touch backend도 있습니다.
 
백엔드는 React의 합성 이벤트(synthetic event) 시스템과 유사한 역할을 수행합니다.
즉, 브라우저 차이를 추상화하고 기본 DOM 이벤트를 처리합니다.
유사점에도 불구하고 React DnD 백엔드는 React 또는 해당 합성 이벤트 시스템에 종속되지 않습니다.
내부적으로 모든 백엔드는 DOM 이벤트를 React DnD가 처리할 수 있는 내부 Redux 작업으로 변환합니다.

React DnD 튜토리얼

이 예제는 react-dnd의 데이터 드리븐 철학을 보여줍니다.
  • 드래그 소스와 드랍 타겟을 만들고
  • React 컴포넌트와 연결하여
  • 드래그 앤 드롭 이벤트에 대한 응답으로 UI를 변경하는 방법을 배웁니다.

완성할 작품


컴포넌트 식별

  • Knight, our lonely knight piece;
  • Square, a single square on the board;
  • Board, the whole board with 64 squares.

프롭스 설계 : 렌더링에 필요한 데이터 구조?

Knight :
아마도 소품이 필요하지 않을 것입니다. 포지션이 있긴 한데, 나이츠가 알 이유가 없습니다. 스퀘어에 배치되어 위치를 잡을 수 있기 때문입니다.
Square :
렌더링에 실제로 필요한 유일한 정보는 색상이기 때문에 위치는 필요하지 않습니다.
Square를 하얀색으로 만들고 black boolean prop을 추가할 것입니다.
Square는 단일 자식을 허용할 수 있습니다. 현재 그 위에 있는 체스 말입니다.
브라우저 기본값과 일치하도록 기본 배경색으로 흰색을 선택했습니다.
Boards :
보드는 자기 자신의 스퀘어를 가질 수 있기 때문에, Children 프롭으로 전달하는 것은 이치에 맞지 않습니다.
또한 보드는 자기 자신의 Knights를 가질 수 있기 때문에, 이를 전달하지 않습니다.
보드는 Knights의 포지션만 필요로 합니다.
실제 체스 게임에서 Borads는 말의 색상, 타입, 모양, 위치 등의 데이터 구조를 필요로 할 테지만,
우리가 만들 간단한 체스 게임에서는 하나의 말의 위치면 충분합니다.
[0,0]을 A8 (좌측 상단)으로 구현해야 어렵지 않습니다. (브라우저의 좌표 체계와 동기화)
 

현재 좌표 상태는 어디에다 둘까요?

보드에는 두지 않겠습니다. 컴포넌트 내부 상태는 적을수록 좋습니다.

보드는 이미 레이아웃 로직을 갖고 있습니다.

상태관리 로직은 분리하고 싶습니다.

 

일단은 상태가 어딘가에 존재하는 것처럼 컴포넌트를 작성하고
props를 통해 수신할 때 올바르게 렌더링되는지 확인하고 나중에 상태 관리에 대해 생각합니다!

컴포넌트 만들기

저는 상향식으로 시작하는 것을 선호합니다.
이렇게 하면 항상 이미 존재하는 것으로 작업하기 때문입니다.
 
내가 Board를 먼저 개발한다면 Square 개발이 끝날 때까지 결과를 볼 수 없을 것입니다.
즉각적인 피드백 루프가 중요하다고 생각합니다.
 
따라서 knignt 컴포넌트부터 시작합니다.
props가 전혀 없어 개발이 쉽습니다.
import React from 'react'

export default function Knight() {
  return <span>♘</span>
}

렌더링이 잘 되나 봅시다.

import React from 'react'
import ReactDOM from 'react-dom'
import Knight from './Knight'

ReactDOM.render(<Knight />, document.getElementById('root'))
다른 컴포넌트를 작업할 때마다 이 작업을 수행하여 항상 렌더링할 항목이 있습니다.
더 큰 앱을 작업할 땐 cosmos나 storybook을 활용합니다.
 
이제 내 Knight 컴포넌트가 보입니다.
다음은 Square를 구현할 차례입니다.
import React from 'react'

export default function Square({ black }) {
  const fill = black ? 'black' : 'white'
  return <div style={{ backgroundColor: fill }} />
}
 
이제 엔트리 코드를 변경하여 Knight가 Square 내부에서 어떻게 보이는지 확인합니다.
import React from 'react'
import ReactDOM from 'react-dom'
import Knight from './Knight'
import Square from './Square'

ReactDOM.render(
  <Square black>
    <Knight />
  </Square>,
  document.getElementById('root')
)
슬프게도 화면이 비어 있습니다. 몇 가지 실수를 했습니다.
  • Square에 차원을 지정하는 것을 잊었으므로 그냥 축소됩니다.
    • 너비: '100%' 및 높이: '100%'를 지정하여 컨테이너를 채웁니다.
  • Square 컴포넌트 내부에  {children}을 넣는 것을 잊었습니다.
이 두 가지 오류를 수정한 후에도 기사가 보이지 않습니다.
기본 페이지 본문 텍스트 색상이 검은색이므로 검은색 정사각형에서는 보이지 않기 때문입니다.
Knight에 color prop을 제공하여 이 문제를 해결할 수 있으나,
훨씬 간단한 수정은 backgroundColor를 설정한 위치에 해당 색상 스타일을 설정하는 것입니다.
 
이 버전의 Square는 실수를 수정하고 두 색상 모두에서 똑같이 훌륭하게 작동합니다.
import React from 'react'

export default function Square({ black, children }) {
  const fill = black ? 'black' : 'white'
  const stroke = black ? 'white' : 'black'

  return (
    <div
      style={{
        backgroundColor: fill,
        color: stroke,
        width: '100%',
        height: '100%'
      }}
    >
      {children}
    </div>
  )
}

드디어 보드 개발을 시작할 시간입니다! 동일한 단일 사각형을 그리는 매우 나이브한 버전으로 시작하겠습니다.
import React from 'react'
import Square from './Square'
import Knight from './Knight'

export default function Board() {
  return (
    <div>
      <Square black>
        <Knight />
      </Square>
    </div>
  )
}
첫번째 의도는 props를 통해 Knight를 그릴 위치를 조정하는 것입니다.
import React from 'react'
import ReactDOM from 'react-dom'
import Board from './Board'

ReactDOM.render(
  <Board knightPosition={[0, 0]} />,
  document.getElementById('root')
)

동일한 단일 사각형을 볼 수 있습니다.

이제 모든 사각형을 렌더링할 것입니다!

하지만 어디서부터 시작해야 할지 모르겠어요.

어떤 것을 렌더링 할까요? forloop? 사각형에 대한 map?

 

지금은 해당 구현을 생각하고 싶지 않습니다. 
나는 Knight가 있건 없건 상관없이 단일 사각형을 렌더링하는 방법을 이미 알고 있습니다. (children props을 통해서)
KnightPosition prop 덕분에 기사의 위치도 알 수 있습니다.
이는 renderSquare method를 전체 보드와 상관없이 구현할 수 있다는 뜻입니다.
renderSquare의 첫번째 구현힙니다.
function renderSquare(x, y, [knightX, knightY]) {
  const black = (x + y) % 2 === 1
  const isKnightHere = knightX === x && knightY === y
  const piece = isKnightHere ? <Knight /> : null

  return <Square black={black}>{piece}</Square>
}

 

Board의 렌더링을 다음과 같이 변경할 수 있습니다.
export default function Board({ knightPosition }) {
  return (
    <div
      style={{
        width: '100%',
        height: '100%'
      }}
    >
      {renderSquare(0, 0, knightPosition)}
      {renderSquare(1, 0, knightPosition)}
      {renderSquare(2, 0, knightPosition)}
    </div>
  )
}

이전에 square의 width, height를 100%로 설정하였던 것을 깜빡하였습니다.

이제 Flexbox를 도입해 봅시다.

루트 div에 몇 가지 스타일을 추가하고 사각형을 div에 래핑하여 레이아웃할 수 있도록 했습니다.
일반적으로 컴포넌트를 캡슐화한 상태로 유지하며
컴포넌트가 자신이 배치되는 방식을 알지 못하도록 하는 것이 좋습니다.
 
이것이 래퍼(레이아웃) div를 추가하는 것을 의미하더라도 말입니다.
import React from 'react'
import Square from './Square'
import Knight from './Knight'

function renderSquare(i, [knightX, knightY]) {
  const x = i % 8
  const y = Math.floor(i / 8)
  const isKnightHere = x === knightX && y === knightY
  const black = (x + y) % 2 === 1
  const piece = isKnightHere ? <Knight /> : null

  return (
    <div key={i} style={{ width: '12.5%', height: '12.5%' }}>
      <Square black={black}>{piece}</Square>
    </div>
  )
}

export default function Board({ knightPosition }) {
  const squares = []
  for (let i = 0; i < 64; i++) {
    squares.push(renderSquare(i, knightPosition))
  }

  return (
    <div
      style={{
        width: '100%',
        height: '100%',
        display: 'flex',
        flexWrap: 'wrap'
      }}
    >
      {squares}
    </div>
  )
}

Board의 종횡비를 제한하는 방법을 아직 모르더라도 나중에 추가하기 쉬울 것입니다.

우리는 KnightPosition을 변경하여 KnightBoard 위에서 움직일 수 있습니다.
import React from 'react'
import ReactDOM from 'react-dom'
import Board from './Board'

ReactDOM.render(
  <Board knightPosition={[7, 4]} />,
  document.getElementById('root')
)

이 환상적인 선언형 스타일이 사람들이 리액트를 사랑하는 이유입니다.


게임 상태 추가하기

이제 Knight를 드래그 가능하도록 만들고 싶습니다.

현재 기사 위치를 일종의 상태 저장소에 유지하고 변경할 수 있는 방법이 필요합니다.

 

이 상태를 설정하려면 약간의 생각이 필요합니다.
따라서 드래그를 동시에 구현하지 않겠습니다.
 
대신 더 간단한 구현부터 시작하겠습니다.
특정 사각형을 클릭하면 기사가 이동하지만, 이는 체스 규칙에서 허용하는 경우에만 가능합니다.
이 논리를 구현하면 상태 관리에 대한 충분한 통찰력을 얻을 수 있으므로
후에 클릭을 드래그 앤 드랍으로 대체할 수 있습니다.

React는 상태 관리나 데이터 흐름에 대해 독단적이지 않습니다.

Flux, Redux, Rx 또는 심지어 Backbone을 사용할 수도 있고

뚱뚱한 모델을 피하고 읽기와 쓰기를 분리할 수 있습니다.

(avoid fat models and separate your reads from writes)

 

나는 이 간단한 예제를 위해 Redux를 설치하거나 설정하는 것을 귀찮게 하고 싶지 않으므로 더 간단한 패턴을 따를 것입니다.

Redux만큼의 확장성은 없으나, 필요하지도 않습니다.

아직 상태 관리자용 API를 결정하지 않았지만 Game이라고 부를 예정이며

데이터 변경 사항을 내 React 코드에 신호로 보내는 방법이 확실히 필요합니다.

 

아직 존재하지 않는 가상의 Game으로 index.js를 다시 작성할 수 있습니다.

이번에는 상상으로 코드를 작성하고 있으며 아직 실행할 수 없습니다. 아직 API를 파악하고 있기 때문입니다.

import React from 'react'
import ReactDOM from 'react-dom'
import Board from './Board'
import { observe } from './Game'

const root = document.getElementById('root')

observe((knightPosition) =>
  ReactDOM.render(<Board knightPosition={knightPosition} />, root)
)
ovserve 함수는 뭔가요?
변화하는 상태를 구독하는 것은 내가 생각할 수 있는 가장 최소한의 방법일 뿐입니다.
EventEmitter로 만들 수도 있었지만 단일 변경 이벤트면 충분합니다.
 
Game 객체 모델을 만들 수도 있었지만 값의 흐름만 있으면 되는데 왜 그렇게 할까요?
 
이 구독 API가 의미가 있는지 확인하기 위해 임의의 위치를 ​​내보내는 가짜 게임을 작성하겠습니다.
export function observe(receive) {
  const randPos = () => Math.floor(Math.random() * 8)
  setInterval(() => receive([randPos(), randPos()]), 500)
}​
 

이것은 분명히 별로 유용하지 않습니다.
상호 작용을 원하면 컴포넌트에서 Gamestate를 수정하는 방법이 필요합니다.
지금은 내부 상태를 직접 수정하는 moveKnight 함수를 노출할 것입니다.
 
이것은 다른 상태 저장소가 단일 사용자 작업에 대한 응답으로 상태를 업데이트하는 복잡한 앱에서는 잘 작동하지 않지만
우리의 경우에는 충분합니다.
let knightPosition = [0, 0]
let observer = null

// 변경 발생 시마다 observer를 호출함
function emitChange() {
  observer(knightPosition)
}

export function observe(o) {
  if (observer) {
    throw new Error('Multiple observers not implemented.')
  }

  observer = o
  emitChange()
}

export function moveKnight(toX, toY) {
  knightPosition = [toX, toY]
  emitChange()
}
컴포넌트는 오직 렌더링을 위한 데이터만 필요로 합니다.
Square는 렌더링할 위치를 알 필요가 없습니다.
따라서 이 시점에서 moveKnight 메서드와 커플링하지 않는 것이 좋습니다.
대신에, 우리는 onClick handler를 Board 내부의 Square를 래핑하는 div에 추가할 것입니다. (renderSquare 메소드 내부)
import React from 'react'
import Square from './Square'
import Knight from './Knight'
import { moveKnight } from './Game'

/* ... */

function renderSquare(i, knightPosition) {
  /* ... */
  return <div onClick={() => handleSquareClick(x, y)}>{/* ... */}</div>
}

function handleSquareClick(toX, toY) {
  moveKnight(toX, toY)
}

Square에 onClickprop을 추가하고 대신 사용할 수도 있었지만

나중에 드래그 앤 드롭 인터페이스를 위해 클릭 핸들러를 제거할 것이기 때문에 귀찮게 할 필요가 없습니다.

 

현재 마지막으로 누락된 부분은 체스 규칙 확인입니다. 기사는 임의의 사각형으로 이동할 수 없으며 L자형 이동만 허용됩니다. canMoveKnight(toX, toY) 함수를 Game에 추가하고 초기 위치를 B1으로 변경하여 Chess 규칙과 일치하도록 합니다.
let knightPosition = [1, 7] // B1

/* ... */

export function canMoveKnight(toX, toY) {
  const [x, y] = knightPosition
  const dx = toX - x
  const dy = toY - y

  return (
    (Math.abs(dx) === 2 && Math.abs(dy) === 1) ||
    (Math.abs(dx) === 1 && Math.abs(dy) === 2)
  )
}​

마지막으로 handleSquareClick 메서드에 canMoveKnightcheck를 추가합니다.

import { canMoveKnight, moveKnight } from './Game'

/* ... */

function handleSquareClick(toX, toY) {
  if (canMoveKnight(toX, toY)) {
    moveKnight(toX, toY)
  }
}​

지금까지 아주 잘 동작하고 있습니다.

Adding Drag and Drop Interaction

이제 React DnD와 함께 컴포넌트에 드래그 앤 드롭 상호 작용을 추가하는 것이 얼마나 쉬운지 확인해 봅시다.
React DnD와 이에 대한 HTML5 백엔드를 설치하는 것으로 시작하겠습니다.
npm install react-dnd react-dnd-html5-backend
touch backend와 같은 대체 서드파티 백엔드를 탐색할 수 있지만 이는 이 자습서의 범위를 벗어납니다.

Setting up the Drag and Drop Context

앱에서 가장 먼저 설정해야 할 것은 DndProvider입니다.

이것은 우리 애플리케이션의 최상단 근처에 장착되어야 합니다.

HTML5Backend를 사용할 것임을 설정하려면 이것이 필요합니다.

import React from 'react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'

function Board() {
  /* ... */
  return <DndProvider backend={HTML5Backend}>...</DndProvider>
}

Define Drag Types

드래그 가능한 아이템 타입에 대한 상수를 생성하겠습니다.
우리 게임에는 KNIGHT라는 단일 아이템 타입만 있을 것입니다.
이를 내보내는 Constants module을 만듭니다.
export const ItemTypes = {
  KNIGHT: 'knight'
}
이제 준비 작업이 끝났습니다. Knight를 드래그 가능하도록 변경합시다!

Knight를 드래그 가능하도록 만들기

useDrag hook은 사양 객체를 반환하는 메모이제이션 함수를 파라미터로 받습니다.
이 객체에서 item.type은 방금 정의한 상수로 설정되어 있으므로 이제 collect 함수를 작성해야 합니다.
const [{ isDragging }, drag] = useDrag(() => ({
  type: ItemTypes.KNIGHT,
  collect: (monitor) => ({
    isDragging: !!monitor.isDragging()
  })
}))
하나씩 살펴봅시다.
  • useDrag는 사양 개체를 허용합니다. item.type property는 필수이며 드래그되는 아이템의 타입을 지정합니다.
    • 드래그되는 조각의 종류를 식별하기 위해 여기에 추가 정보를 첨부할 수도 있지만 이것은 토이 응용 프로그램이므로 타입만 정의하면 됩니다.
  • collect는 collect 함수를 정의합니다.
    • 드래그 앤 드롭 상태를 컴포넌트에 사용할 수 있는 prop (props 객체)로 변환하는 방법입니다.
  • result 배열에는 다음이 포함됩니다.
    • 첫 번째 항목인 props object : 드래그 앤 드롭 시스템에서 수집한 props 객체가 포함됩니다.
    • 두 번째 항목 ref 함수.
      • 이것은 DOM 요소를 react-dnd에 부착하는데 사용됩니다.

이제 드래그 기능이 업데이트 된 Knight 컴포넌트를 살펴봅시다.

import React from 'react'
import { ItemTypes } from './Constants'
import { useDrag } from 'react-dnd'

function Knight() {
  const [{isDragging}, drag] = useDrag(() => ({
    type: ItemTypes.KNIGHT,
    collect: monitor => ({
      isDragging: !!monitor.isDragging(),
    }),
  }))

  return (
    <div
      ref={drag}
      style={{
        opacity: isDragging ? 0.5 : 1,
        fontSize: 25,
        fontWeight: 'bold',
        cursor: 'move',
      }}
    >
      ♘
    </div>,
  )
}

export default Knight

Board 사각형(Squares)을 Droppable(드래그 아이템을 놓을 수 있는 위치)로 만들기

Knight는 이제 드래그 소스이지만 아직 드롭을 처리할 드롭 타겟이 없습니다.

이제 Square를 드롭 타겟으로 만들 것입니다.

이번에는 Square로 포지션을 넘길 수 밖에 없습니다.

Square가 자신의 위치를 ​​모르면 드래그한 Knight를 자기 위에 두어야 하는지 알 수 있을까요?

하지만 Square의 엔터티 자체가 변한게 아니라, 보드에서 말과의 인터랙션 관점의 역할이 추가된 것으로 볼 수도 있습니다.

즉 다른 곳에서의 Square의 사용성 측면을 고려하면 position이 필요하지 않습니다.

이 딜레마에 직면했을 때 똑똑한 컴포넌트와 멍청한 컴포넌트(smart and dumb components)를 분리할 때입니다.

 

BoardSquare라는 새로운 컴포넌트를 소개하겠습니다.
우리의 좋은 오래된 Square를 렌더링하지만 위치도 알고 있습니다.
사실, 그것은 보드 내부의 renderSquaremethod가 하던 로직의 일부를 캡슐화하고 있습니다.
React 컴포넌트는 적절한 시기에 이러한 렌더링을 위한 서브 메서드에서 추출되는 경우가 많습니다.
 
import React from 'react'
import Square from './Square'

export default function BoardSquare({ x, y, children }) {
  const black = (x + y) % 2 === 1
  return <Square black={black}>{children}</Square>
}
해당 컴포넌트를 사용하기 위해 Board를 변경합니다.
/* ... */
import BoardSquare from './BoardSquare'

function renderSquare(i, knightPosition) {
  const x = i % 8
  const y = Math.floor(i / 8)
  return (
    <div key={i} style={{ width: '12.5%', height: '12.5%' }}>
      <BoardSquare x={x} y={y}>
        {renderPiece(x, y, knightPosition)}
      </BoardSquare>
    </div>
  )
}

function renderPiece(x, y, [knightX, knightY]) {
  if (x === knightX && y === knightY) {
    return <Knight />
  }
}
이제 useDrophook으로 BoardSquare를 래핑해 보겠습니다.
drop event만 처리하는 드랍 타겟 사양을 작성하겠습니다.
const [, drop] = useDrop(
  () => ({
    accept: ItemTypes.KNIGHT,
    drop: () => moveKnight(x, y)
  }),
  [x, y]
)
drop method는 BoardSquare의 props을 스코프에서 접근할 수 있으므로, knight가 어느 위치에 있어야 하는지 알 수 있습니다.
monitor.getItem()을 사용하여 드래그 소스가 beginDrag를 통해 반환한 드래그된 항목을 검색할 수 있지만
우리는 드래그 가능한 항목이 하나만 있으므로 필요하지 않습니다.
내 collect 함수에서 포인터가 현재 BoardSquare 위에 있는지 여부를 모니터에 물어볼 것이므로
이를 이용해 강조 표시할 수 있습니다.
const [{ isOver }, drop] = useDrop(
  () => ({
    accept: ItemTypes.KNIGHT,
    drop: () => moveKnight(x, y),
    collect: (monitor) => ({
      isOver: !!monitor.isOver()
    })
  }),
  [x, y]
)
렌더 함수를 변경하여 드롭 대상을 연결하고 하이라이트 오버레이를 표시한 후 BoardSquare는 다음과 같이 되었습니다.
import React from 'react'
import Square from './Square'
import { canMoveKnight, moveKnight } from './Game'
import { ItemTypes } from './Constants'
import { useDrop } from 'react-dnd'

function BoardSquare({ x, y, children }) {
  const black = (x + y) % 2 === 1
  const [{ isOver }, drop] = useDrop(() => ({
    accept: ItemTypes.KNIGHT,
    drop: () => moveKnight(x, y),
    collect: monitor => ({
      isOver: !!monitor.isOver(),
    }),
  }), [x, y])

  return (
    <div
      ref={drop}
      style={{
        position: 'relative',
        width: '100%',
        height: '100%',
      }}
    >
      <Square black={black}>{children}</Square>
      {isOver && (
        <div
          style={{
            position: 'absolute',
            top: 0,
            left: 0,
            height: '100%',
            width: '100%',
            zIndex: 1,
            opacity: 0.5,
            backgroundColor: 'yellow',
          }}
        />
      )}
    </div>,
  )
}

export default BoardSquare

이 튜토리얼을 완료하려면 한 가지 변경 사항만 남았습니다.
유효한 이동을 나타내는 BoardSquares를 강조 표시하고
유효한 BoardSquare에만 드롭을 처리하고 싶습니다.

 

고맙게도 React DnD를 사용하면 정말 쉽습니다.

드랍 타겟 사양 객체에서 canDrop method를 정의하기만 하면 됩니다.

const [{ isOver, canDrop }, drop] = useDrop(
  () => ({
    accept: ItemTypes.KNIGHT,
    canDrop: () => canMoveKnight(x, y),
    drop: () => moveKnight(x, y),
    collect: (monitor) => ({
      isOver: !!monitor.isOver(),
      canDrop: !!monitor.canDrop()
    })
  }),
  [x, y]
)

또한 collect 함수의 canDrop 속성을 이용해 오버레이를 추가합니다.

import React from 'react'
import Square from './Square'
import { canMoveKnight, moveKnight } from './Game'
import { ItemTypes } from './Constants'
import { useDrop } from 'react-dnd'

function BoardSquare({ x, y, children }) {
  const black = (x + y) % 2 === 1
  const [{ isOver, canDrop }, drop] = useDrop(
    () => ({
      accept: ItemTypes.KNIGHT,
      drop: () => moveKnight(x, y),
      canDrop: () => canMoveKnight(x, y),
      collect: (monitor) => ({
        isOver: !!monitor.isOver(),
        canDrop: !!monitor.canDrop()
      })
    }),
    [x, y]
  )

  return (
    <div
      ref={drop}
      style={{
        position: 'relative',
        width: '100%',
        height: '100%'
      }}
    >
      <Square black={black}>{children}</Square>
      {isOver && !canDrop && <Overlay color="red" />}
      {!isOver && canDrop && <Overlay color="yellow" />}
      {isOver && canDrop && <Overlay color="green" />}
    </div>
  )
}

export default BoardSquare

드래그 프리뷰 이미지 추가하기

마지막으로 보여드리고 싶은 것은 드래그 미리보기 이미지를 커스터마이징 하는 것입니다.

물론 브라우저는 DOM 노드의 스크린샷을 찍을 것이지만 사용자 정의 이미지를 표시하려면 어떻게 해야 할까요?

 

useDrag hook에서 제공하는 preview ref를 활용합니다.
const [{ isDragging }, drag, preview] = useDrag(() => ({
  type: ItemTypes.KNIGHT,
  collect: (monitor) => ({
    isDragging: !!monitor.isDragging()
  })
}))
이렇게 하면 드래그 항목에 사용한 것처럼 렌더 메서드에서 dragPreview를 연결할 수 있습니다.
react-dnd는 또한 이 참조를 사용하여 이미지를 드래그 미리보기로 표시하는 유틸리티 컴포넌트인 DragPreviewImage를 제공합니다.
  const knightImage = 'data:image/png;base64,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';
  render() {
    return (
      <>
        <DragPreviewImage connect={preview} src={knightImage} />
        <div
          ref={drag}
          style={{
            ...knightStyle,
            opacity: isDragging ? 0.5 : 1,
          }}
        >
          ♘
        </div>
      </>
    )
  }
}

결론

이 튜토리얼의 목표는 React DnD가 React의 철학에 아주 잘 맞는다는 것과

복잡한 상호 작용을 구현하기 전에 먼저 앱 아키텍처에 대해 생각해야 한다는 것을 보여주는 것입니다.

반응형