react dnd 이용하여 drag resizing 구현하기(display:grid)

성도원·2021년 7월 15일
8

react-project

목록 보기
2/3
post-thumbnail

목표

  • 네모(재생목록을 담고 있는)가 가로 세로로 리사이징 되었을때 주변에 있는 네모들이 자연스럽게 어우러지며 배치 되도록 하고 싶었다.(display:grid 를 선택한 이유)
  • 리사이징이 될때 내용물(재생목록)의 갯수가 자연스럽게 결정되고, 내용물이 네모를 자연스럽게 가득 채울것.
  • 리사이징된 크기가 다음에도 유지가 될것.

구조

nemo = {
  column :4,
  row : 4,
  channelId:'UCKmEDAD5k5KFMcY5wvGIeGQ',
  nemoTitle: 'asmr soupe',
  isLargerSize: false,
  videos:[{...},{...},{...},...]
}

네모의 json구조
그 중 리사이징과 관련된 데이터는 다음과 같다.
가로,세로를 결정 짓는 column,row를 추가 시켜주었다.
그리고 isLargerSize썸네일의 크기두 가지로 사용 할 수 있도록 하는 boolean 정보를 담고 있다.
videos는 재생목록의 25개의 비디오정보를 포함하고 있다. 25개로 정한것은 네모최대크기column:10,row:10으로 정했고 그렇게 했을때 25개의 비디오를 표시할 수 있도록 했기 때문이다.

grid의 column과row 값 활용

⬆ Page의 그리드 정보 : `가로`는 `column 10개`로 이루어져 있고 세로 `row`는 `auto-fill`을 이용해서 추가된 컨텐츠에 맞게 늘어난다.
      page.module.css
      네모가 모여있는 상위 컴포넌트 Page의 그리드 css값
      .grid {
        display: grid;
        grid-template-columns: repeat(10, 1fr);
        grid-template-rows: repeat(auto-fill, 1fr);
        gap: 0.5em 1em;
      }
      //nemo.jsx
      //네모의 스타일을 jsx내에서 인라인으로 지정해 nemo의 column,row값에 접근했다.
      <Nemo
        id={nemo && nemo.nemoId}
        className={styles.nemo}
        style={{
          opacity: isDragging ? '0.3' : '1',
        //gridColumn,gridRow에서 값을 각각 `auto/span ${nemo.column || nemo.row}`로 적용
          gridColumn: nemo && `auto/span ${nemo.column}`,
          gridRow: nemo && `auto/span ${nemo.row}`,
        }}
      >
      //...네모콘텐츠생략
      </Nemo>
⬆ 네모의 isLargerSize : 왼쪽이 `false` 오른쪽이 `true`이다.
각각 네모 내부에서 그리드 값을 2*2,3*3으로 갖게 했다.(1.5배가 가장 적합한 크기 차이였다.)
//nemo.jsx
//isLargerSize가 'true'일때 비디오 가 'auto/span 3'을 갖고, 아닐때 'auto/span 2'를 갖도록 하기위한 gridRatio 변수 선언
const gridRatio = isLargerSize ? 3 : 2;

return(
  <Nemo        
    id={nemo && nemo.nemoId}
    className={styles.nemo}
    style={{
      opacity: isDragging ? '0.3' : '1',
      gridColumn: nemo && `auto/span ${nemo.column}`,
      gridRow: nemo && `auto/span ${nemo.row}`,
    }}>
      {videos &&
          videos.map(
            (video, index) =>
    //**이 부분은 네모안에 비디오 갯수를 정하는 부분. 네모의 grid column,row를 각각 비디오의 grid column,row값으로 나누어 소수점을 버린후에 곱한 값이 네모안에 표시될 비디오의 갯수이다.
              index <
                parseInt(nemo.column / gridRatio) * parseInt(nemo.row / gridRatio) && (
                <Video
                  key={index}
                  video={video}
                  isLargerSize={isLargerSize} //네모 내부의 비디오 컴포넌트에 isLargerSize 전달 //fontSize: isLargerSize ? `1.5em` : '1.1em'
                  gridRatio={gridRatio} // 비디오 컴포넌트에서 인라인 스타일 적용을 위함 gridColumn: `auto/span ${gridRatio}`
                  nemoPlayer={nemoPlayer}
                  darkTheme={darkTheme}
                />
              )
      )}
</Nemo>
)

⬆ 네모 안의 비디오 갯수 구하는 방법

index <parseInt(nemo.column / gridRatio) * parseInt(nemo.row / gridRatio)
네모의 grid column,row를 각각 비디오의 grid column,row값으로 나누어 소수점을 버린 후 곱한 값이 네모안에 표시될 비디오의 갯수이다.

ex)네모column:5,row:6이고 isLargerSize?false이면 gridRatio2이므로 parseInt(5/2) x parseInt(6/2) = 2 x 3으로 비디오가 6개 표시됨.
네모의 column5이면 비디오의 column2일때 가로로 2개가 표시되어야 하고, row6이면 세로방향으로 3개가 표시되어야하기 때문이다.
하지만 column과 row를 각각 나누어서 소수점을 버리지 않고 parseInt(5/2 x 6/2)를 하게 되면 비디오가 7개가 표시되면서 그리드가 파괴된다.

drag 리사이징 구현하기

drag & drop 에서는 dragRefdropRef가 모두 nemo에서 이루어졌지만,
drag resizing에서는 dragRef는 리사이징 되어야 할 nemo에서 선언되고, dropRefpage에서 선언되어 page 영역 내에서 드랍되었을 때의 정보를 토대로 nemo가 리사이징 된다.

계획

네모widthheight을 각각 columnrow로 나누어서 column당 너비,row당 높이를 구해 드래그된 x,y좌표 값과 비교해 columnrow값을 더하고 줄여 주려고 한다.

Nemo

//nemo.jsx
    //1.변경된 그리드값을 네모에 적용해주는 함수.
    //throttleGrid는 네모의 grid,column값을 변경해 저장하는 함수이다. 
    //_.throttle은 lodash 라이브러리의 스로틀함수로 함수가 너무 잦게 실행되는것을 방지해준다.
    const throttleGrid = _.throttle((newGrid) => {
      const { column, row } = newGrid;
      const newNemo = { ...nemo, column: column, row: row };
      //saveNemo는 변경된 네모의 값을 state와 firebase RealtimeDB에 저장해주는 함수이다.
      saveNemo(newNemo);
    }, 100);//100 ms 값을 줘서 아무리 빨리 반복되어 실행되더라도 100ms에 한번만 실행된다. 100ms간격으로 실행된다는 말이다.

    //2.리사이징 되는 기준을 잡아줄 width와 height정보를 rect라는 state에 저장.
    //useEffect로 rectRef와 nemo가 변경될때마다 width,height값을 rect state값에 저장해주었다.  
    useEffect(() => {
      const width = rectRef.current.clientWidth;
      const height = rectRef.current.clientHeight;
      setRect({ width, height });
      console.log(rect);
    }, [rectRef, nemo]);

    //3.react dnd의 useDrag함수.
    //이전 포스트에서 사용했던 드래그 드랍과 같은 함수
    //이번에는 드래그 될때 전달할 정보에 column,row,width,height,
    //throttleGrid,isLargerSize, 6가지 데이터를 전달해주었다.
    //6가지 데이터를 통해서 page의 useDrop 함수에서 리사이징될 조건을 만들어 줄 것이다.
    const [{ isResizing }, resizeRef] = useDrag(
      () => ({
        //resizeRef에서 발생하는 드래그 아이템을 Resize로 정했다.
        type: ItemTypes.Resize,
        item: {
          column: nemo && nemo.column,
          row: nemo && nemo.row,
          width: rect && rect.width,
          height: rect && rect.height,
          throttleGrid,
          isLargerSize: isLargerSize,
        },
        //isResizing은 리사이징중이라는 상태를 boolean으로 리턴해준다.
        collect: (monitor) => ({
          isResizing: monitor.isDragging(),
        }),
      }),
      [nemo, rect, isLargerSize, throttleGrid]
    );

    return (
      <Nemo
        id={nemo && nemo.nemoId}
        className={styles.nemo}
        style={{
          opacity: isDragging ? '0.3' : '1',
          gridColumn: nemo && `auto/span ${nemo.column}`,
          gridRow: nemo && `auto/span ${nemo.row}`,
        }}
      >
        //...네모 내부 생략
        //비디오들이 담겨있는 div의 rect정보를 위해서 rectRef를 추가.
        //최상위 div에 rectRef를 선언하고 싶었지만, 거기에는 드래그엔 드랍에 사용되는 previewRef가 사용되어서 불가능했다.
        //previewRef는 reactDnD의 ref여서 rect값에 접근이 불가능 했다. 그리고 video를 담고있는 컨테이너가 전체 크기와 많이 다르지않아 무리없었다.
        <div ref={rectRef} className={styles.videos}>
          ...
          <Video/>
          ...  
        </div>
        //리사이즈 버튼 position:absolute로 네모의 오른쪽 하단에 배치함.
        //useDrag함수에서 resizeRef로 정한 변수명을 ref에 적용시켜준다.
        <button className={`${styles.drag} ${themeClass}`} ref={resizeRef}>
          //리사이징 중일 때 버튼에 네모의 컬럼과 로우를 표시해줬다.
          {isResizing && `${nemo.column}x${nemo.row}`}
        </button>
      </Nemo>
);

Page

//page.jsx
  //Nemo의 useDrag함수에서 전달해준 정보를 받는 useDrop함수.
  const [, resizeDrop] = useDrop(
    () => ({
      //아이템 타입이 Resize인 드래그만 받겠다는 뜻.
      //드래그엔드랍은 ItemTypes.Nemo로 해주었었다.
      accept: ItemTypes.Resize,
      canDrop: () => false,
      //아이템 타입이 Resize인 아이템이 hover 중 일때 실행되는 함수. 
      hover(item, monitor) {
        //Nemo에서 전달해준 6가지 정보를 es6 destructuring을 이용해 할당함.
        const {
          column,
          row,
          width: w, //width와 height은 짧게 w와 h 로 바꿔줬다.
          height: h,
          throttleGrid,
          isLargerSize, //썸네일이 크냐 작냐에 따라 네모의 최소 column,row사이즈를 다르게 지정해주기위해 전달했다.
        } = monitor.getItem();
        
        //↓ monitor.getDifferenceFromInitialOffset()는 드래그 이벤트가 시작한 지점으로부터
        //얼마나 차이가 발생했는지 오브젝트로 반환해준다. ex) {x:140,y:-40}
        const { x, y } = monitor.getDifferenceFromInitialOffset();
        let [newColumn, newRow] = [column, row];
        //컬럼당 너비와 로우당 높이
        const [wPerColumn, hPerRow] = [w / column, h / row];
        //리사이징의 반응이 너무 민감해서 x,y값에 0.8을 곱해주었다.
        const SENS = 0.8;
        //새로운 컬럼 x값을 wPerColumn 나눈 값을 반올림해서 더해주면,
        //x값이 컬럼당너비의 반이상이 될때 +1의 값을 더하고 마이너스의 경우에도 -1을 더하게된다.
        newColumn += Math.round((x * SENS) / wPerColumn);
        newRow += Math.round((y * SENS) / hPerRow);
        
        //컬럼과 로우의 최대 값을 10으로 제한. 
        newColumn > 10 && (newColumn = 10);
        newRow > 10 && (newRow = 10);
        
        //네모 내부에 비디오가 한개는 있어야 하기 때문에 최소 컬럼로우의 값을 3과2로 설정
        const gridRatio = isLargerSize ? 3 : 2;
        newColumn < gridRatio && (newColumn = gridRatio);
        newRow < gridRatio && (newRow = gridRatio);
        
        //컬럼과 로우를 수정하는 함수를 쓰로틀처리 하였지만, 컬럼과 로우가 하나도 변경되지 않았다면, 
        //수정함수를 호출하지않도록한다.
        if (newColumn !== column || newRow !== row){
          throttleGrid({ column: newColumn, row: newRow });
        }
      },
    }),
    []
  );

return(
      //페이지영역에 드래그가 드랍될 dropRef를 지정해준다.
      //페이지 영역 밖으로 드래그 하면 동작하지 않음. 원하는 영역이 더 넓다면 더 상위 컴포넌트에서 지정해주면 된다. 위에서 선언된 함수도 같이 이동해야함.
      <Page ref={resizeDrop}>
        <div className={styles.grid}>
          {findPage &&
            order &&
            order.map((chId, index) => (
              <Nemo
                key={chId}
                index={index}
                id={chId}
                nemo={findPage.nemos[chId]}
                ...
              />
            ))}
        </div>
      </Page>
  )

마치며...

처음에는 리사이징 되게 만들자! 이렇게 간단하게 시작했는데, 그 안의 표시될 컨텐츠 갯수도 설계해야 했고,
그 컨텐츠가 보기 좋게 꽉차도록 보여야 하는 등, 다양한 문제에 부딪혔다.
문제를 해결하며 내 프로젝트에 꽤 만족스러운 드래그 리사이징 기능이 추가되어 만족감이 컸다.

포스팅을 하면서 과연 다른사람들이 내가 하는 설명을 이해할 수 있을지에 대한 의문이 계속 들었다.
나름 그림까지 만들어가며 설명했지만 여전히 설명은 어렵다. 😭
계속해서 포스팅 하며 설명하는 능력을 길러야겠다.💪🏻

그리고 설명하려고 적다보니 변수명이나 코드를 수정하게 되는 것이 좋은 리뷰가 된다.
시간이 될 때 프로젝트 전체를 다시 만들며 리뷰 해 볼 생각이다. 그 때 마다 포스팅은 업데이트 하겠습니다.🤞🏻

1개의 댓글

comment-user-thumbnail
2023년 10월 5일

해당 프로젝트를 참고하려고 하는데 왜 현재는 페이지가 DND 나 sizing이 모두 적용이 안될까요??

답글 달기