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 탭에서 같은 문제를 겪은 동지들을 발견하였다.
먼저 왜 이런 문제가 발생했는지 먼저 살펴보도록 하자.
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이 발생한 것이다.
이 문제를 어떻게 해결할까?
가장 중요한 컨셉은 Draggable 요소가 Drag상태 일 때만 transform
요소를 가지고 있는 부모에서 다른 부모로 갔다 오는 것이다.
코드와 함께 하나하나씩 살펴보자.
우선, draggable 요소가 drag될 때 도망갈 또 다른 div
를 index.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
역시 개고수