React-Beautiful-DnD에서 offset이 발생할 때

FeRo 페로·2023년 10월 22일
1
post-thumbnail

React-Beautiful-DnD(RBD)를 사용하는 과정에서 위 화면과 같은 버그가 발생했다. 상황은 다음 예시 코드와 같다.


<div>
    <div style="height: 100px;"></div>
    <div style="transform: translateY(0);">
        <!-- RBD의 Draggable이 여기 위치했을 때 문제가 발생! -->
    </div>
</div>

부모 요소에 transform 요소가 추가되어 있다면, drag 발생 시 draggable 요소에 offset이 발생한다.

시간을 들여서 해결을 하려고 했지만 마땅한 해결책을 찾을 수 없던 무렵 RBD의 issue 탭에서 같은 문제를 겪은 동지들을 발견하였다.

먼저 왜 이런 문제가 발생했는지 먼저 살펴보도록 하자.


position:fixed

We leave elements in place when dragging. We apply position: fixed on elements when we are moving them around.


이유는 draggable 요소를 drag할 때, 해당 요소에 position: fixed가 적용되기 때문이었다. 그래서 부모 요소의 transform 요소에 영향을 받아 offset이 발생한 것이다.

이 문제를 어떻게 해결할까?


Drag할 때만 잠깐 다른 곳으로 가자!

가장 중요한 컨셉은 Draggable 요소가 Drag상태 일 때만 transform 요소를 가지고 있는 부모에서 다른 부모로 갔다 오는 것이다.

코드와 함께 하나하나씩 살펴보자.


또 다른 div를 만들자

우선, draggable 요소가 drag될 때 도망갈 또 다른 divindex.html에 만든다. 이때 꼭 다음과 같은 style 값을 주어야 한다.


<div id="draggable" style="position: fixed; pointer-events: none; width: 100%; height: 100%; top: 0;"></div>

top이 반드시 있어야 한다. 이걸 넣지 않은 코드가 원본 코드인데, 그 부분 때문에 root div 아래에 id가 draggable인 div가 있어서 매우 불편함을 느끼게 된다.

Draggable 요소가 drag 상태일 때 여기 위치하고, drag 상태가 아닐 때 원래의 위치로 돌아갈 것이다.


분기 처리를 해주자

drag가 될 때 새로 만들어 둔 div에 위치할 수 있도록 조건문을 작성해 주자. 앞서 살펴본 것처럼 draggable 요소가 drag상태 일 때 해당 요소에 position:fixed가 할당된다는 것을 이용해서 분기를 한다.


// 새로 만든 div를 변수에 할당
const dragEl = document.getElementById('draggable');

const optionalPortal = (style, element) => {
  // drag상태 일 때 position이 fixed로 할당되는 것을 활용해서 분기처리
  if (style.position === 'fixed') {
    // drag상태 일 때는 createPortal로 새로 만든 div로 옮겨준다.
    return createPortal(element, dragEl);
  }
  return element;
};

// jsx에서 optionalPortal을 통해 Draggable 요소를 렌더링
<Draggable draggableId={id} index={idx} key={id}>
  {(provided) =>
    optionalPortal(
      provided.draggableProps.style,
      (
        <DraggableContainer
          {...provided.draggableProps}
          ref={provided.innerRef}
          >
          ...
        </DraggableContainer>
      )
    )
  }
</Draggable>

코드에도 나와 있지만 createPortal을 사용해서 새로 만든 div로 옮겨준다. 즉, 새로운 div에 drag되고 있는 모달창이 되는 것이다.


결론

github issue를 뒤적이다 만난 사막의 오아시스. 그런 코멘트들을 적용해 보고, 더 나은 방법으로 조금 커스텀을 해서 최종적으로 커스텀 훅을 만들어서 사용하고 있다.


// typescript version
const useDraggableInPortal = () => {
  const element = useRef<HTMLDivElement>(
    document.getElementById('draggable') as HTMLDivElement,
  ).current;

  return (render: (provided: DraggableProvided) => ReactElement) =>
    (provided: DraggableProvided) => {
      const result = render(provided);
      const style = provided.draggableProps.style as DraggingStyle;
      if (style.position === 'fixed') {
        return createPortal(result, element);
      }
      return result;
    };
};

// component.tsx

const optionalPortal = useDraggableInPortal();

...

<Draggable draggableId={id)} index={idx} key={id}>
  {optionalPortal((provided) => (
    <DraggableContainer
      {...provided.draggableProps}
      ref={provided.innerRef}
      >
      {/* some react code */}
    </DraggableContainer>
  ))}
</Draggable>

RBD의 draggable의 특징과 이를 이용한 해결책은 정말 놀라웠다. gpt나 다른 블로그, RBD 공식 레퍼런스(github markdown 문서들)에서도 해결책이 없어서 긴 시간의 삽질을 했다. 이 글이 같은 문제를 만난 개발자들에게 조금의 insight라도 되었으면 하는 마음이다.



<참고자료 및 출처>
https://github.com/atlassian/react-beautiful-dnd/issues/1422
https://github.com/atlassian/react-beautiful-dnd/issues/128
https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/reparenting.md

profile
주먹펴고 일어서서 코딩해

1개의 댓글

comment-user-thumbnail
2023년 12월 13일

역시 개고수

답글 달기