
칸반 보드를 구현하면서 드래그 앤 드롭 기능을 다음과 같이 만들고자 했다:
카드를 잡고 움직이면 같은 컬럼 내에서 순서가 바뀌기
컬럼 간 이동 시 상태가 변경되기 (Todo ↔ OnProgress ↔ Done)

처음에는 카드 전체를 드래그할 수 있도록 구현했는데, 확인해보니 팝오버의 토글 버튼이 동작하지 않는 문제가 발생했다. 클릭해도 팝오버가 열리지 않고 대신 드래그 앤 드롭이 실행되는 것이다.
이전에 팀 프로젝트에서도 비슷한 문제를 겪은 적이 있었고, 그때는 e.stopPropagation()을 추가해서 해결했었다. 이번에도 같은 방법을 시도했지만, 여전히 팝오버가 정상적으로 동작하지 않았다. 여러 방법을 시도하고 검색해봤지만 쉽게 해결되지 않았다.
그래서 이걸 어떻게 해결하지? 싶어서 생각한 방법이

카드 전체를 드래그 가능하게 만드는 대신, 별도의 드래그 버튼을 추가하고 팝오버의 위치를 변경하는 방법을 고려했다.

카드 전체에 드래그 이벤트 리스너를 추가하는 대신, 팝오버를 제외한 요소들에만 드래그 리스너를 부착하는 방식도 시도했다.

팝오버 토글 버튼을 absolute로 설정해 카드 영역에서 분리하면 해결될 수도 있을 것 같았다.
<카드 relative>
<카드내용 드래그리스너/>
<카드내용/>
<팝오버토글 absolute/>
<카드/>
이 세 가지 방법을 모두 시도해봤지만, 여전히 근본적인 해결책이 아니라 우회하는 느낌이 들어 찝찝했다.
그러다 GPT에게 문제를 설명하고 검색 키워드를 추천받아 구글링을 하던 중, stackoverflow에서 이런 글을 발견했다.
이 방법을 적용해 본 결과, 원하는 대로 동작했다!!!
핵심 아이디어는 특정 요소에서 드래그 앤 드롭을 비활성화하는 것이다. data-no-dnd 속성을 추가하고, 이를 감지하는 로직을 만들었다.
* DOM 트리를 순회하면서 드래그 앤 드롭 처리 여부를 확인하는 함수
* 입력받은 DOM 요소(element)부터 부모 요소까지 순회하며, 각 요소에 data-no-dnd라는 데이터 속성이 있는지 확인합니다.
* data-no-dnd 속성이 발견되면 false를 반환하여 해당 요소와 그 자식 요소에서는 드래그 앤 드롭을 비활성화합니다.
* 순회를 마치고도 data-no-dnd 속성이 발견되지 않으면 true를 반환하여 드래그 앤 드롭이 활성화된 상태임을 나타냅니다.
* @param element - 검사할 DOM 요소
* @returns {boolean} - 드래그 앤 드롭 처리 여부 (true: 처리함, false: 처리하지 않음)
*/
function shouldHandleEvent(element: HTMLElement | null): boolean {
let cur = element; // 현재 검사 중인 요소
while (cur) { // DOM 트리를 순회
if (cur.dataset && cur.dataset.noDnd) { // 'data-no-dnd' 속성 확인
return false; // 드래그 앤 드롭 비활성화
}
cur = cur.parentElement; // 부모 요소로 이동
}
return true; // 드래그 앤 드롭 활성화
}
/**
* 이 클래스는 마우스를 이용한 드래그 앤 드롭 동작을 제어하는 커스텀 센서입니다. LibMouseSensor라는 기본 센서를 확장(상속)하여 특정 조건에서 동작을 제한합니다.
* activators라는 정적 속성에 이벤트 핸들러를 정의합니다.
* 여기서 onMouseDown 이벤트가 발생할 때, shouldHandleEvent 함수를 호출하여 현재 이벤트가 발생한 대상(event.target)이 드래그 앤 드롭 가능한지 확인합니다.
* 만약 shouldHandleEvent가 false를 반환하면 해당 요소에서 드래그 앤 드롭이 차단됩니다.
*/
class CustomMouseSensor extends LibMouseSensor {
static activators = [
{
eventName: "onMouseDown" as const, // 마우스 클릭 이벤트
handler: ({ nativeEvent: event }: MouseEvent) => {
return shouldHandleEvent(event.target as HTMLElement); // 이벤트 대상 검사
},
},
];
}
/**
* 키보드 드래그 앤 드롭을 위한 커스텀 센서 클래스
* LibKeyboardSensor라는 기본 센서를 확장(상속)하여 특정 조건에서 동작을 제한합니다.
* activators라는 정적 속성에 이벤트 핸들러를 정의합니다.
* 여기서 onKeyDown 이벤트가 발생할 때, shouldHandleEvent 함수를 호출하여 현재 이벤트가 발생한 대상(event.target)이 드래그 앤 드롭 가능한지 확인합니다.
* 만약 shouldHandleEvent가 false를 반환하면 해당 요소에서 키보드 기반의 드래그 앤 드롭이 차단됩니다.
*/
class CustomKeyboardSensor extends LibKeyboardSensor {
static activators = [
{
eventName: "onKeyDown" as const,// 키보드 입력 이벤트
handler: ({ nativeEvent: event }: KeyboardEvent) => {
return shouldHandleEvent(event.target as HTMLElement); // 이벤트 대상 검사
},
},
];
}
/**
* 위에서 정의한 커스텀 센서를 활용하여 실제로 드래그 앤 드롭 동작을 제어하기 위해 센서를 등록합니다.
* useSensors는 여러 센서를 결합하여 사용할 수 있도록 도와주는 훅(Hook)입니다.
* 여기서는 useSensor를 사용하여 각각의 커스텀 센서(CustomMouseSensor, CustomKeyboardSensor)를 생성하고, 이를 sensors라는 변수에 결합합니다.
* 이렇게 설정된 센서는 이후 컴포넌트에서 드래그 앤 드롭 라이브러리와 함께 사용됩니다.
*/
const sensors = useSensors(
useSensor(CustomMouseSensor),
useSensor(CustomKeyboardSensor),
);
위 코드는 특정 DOM 요소에서 드래그 앤 드롭 기능을 제어한다. DOM 트리를 탐색하여 드래그 앤 드롭을 활성화할지 여부를 판단하고, 이를 기반으로 마우스 및 키보드 드래그 앤 드롭 센서를 설정할 수 있다.
이제 data-non-dnd 속성을 팝오버 토글 버튼에
<PopoverContext.Provider value={providerValue}>
<div ref={dropdownRef} className="relative" data-no-dnd>
{children}
</div>
</PopoverContext.Provider>
이렇게 작성해주면 내가 원하는 방식으로 동작한다.
이 문제를 해결한 Stack Overflow의 답변이 정말 큰 도움이 됐다. 쉽게 해결될 줄 알았는데 예상보다 시간이 오래 걸렸지만, 결국 깔끔한 해결책을 찾을 수 있어서 행복했다. 앞으로 비슷한 문제가 생기면 바로 적용할 수 있을 듯ㅎㅎㅎ 야호~ 🎉