중첩된 컴포넌트에서의 이벤트 트러블 슈팅

중첩된 컴포넌트에서의 이벤트 트러블 슈팅

이슈 1: Ref 노드 선택 안됨

기능 설명

Custom Collection 노드 기능은 기존에 만들어둔 컴포넌트를 Collection으로 전환하여 다른 페이지에도 사용할 수 있는 기능이다. 노드를 Collection으로 전환하면 Global Collection이 생성되고 해당 노드는 Collection을 참조하는 Ref 노드가 된다.

문제 상황

노드를 Collection으로 전환해도 App에서 Ref 노드로 변경되지 않고 여전히 기존의 노드가 노출된다.

가설 1: 노드를 Collection으로 전환하는 로직이 잘못 되었다.

로직을 확인해본 결과, App - Collection - 원본 노드로 이어지는 트리구조가 올바르게 저장되었고, 원본 노드 자리에 Ref 노드가 입력된 것까지 App Dom에는 문제가 없었다.

가설 2: 렌더링이 제대로 되지 않고 있다.

Ref 노드가 어떻게 렌더링 되고 있는지 정확히 알지 못했기 때문에 확인해보았다.

export function RenderedNode({ nodeId }) {
  const { dom } = useAppStateContext();
  const node = getNode(dom, nodeId);

  if (isRef(node)) {
      return  (
          <RenderedNodeHud node={node}>
            <RenderedNode nodeId={node.attributes.refId} />
          </RenderedNodeHud>
      )
  }

  if (isCollection(node)) {
    const childNodeGroups = getChildNodes(dom, node);
    return (
      <>
        {childNodeGroups?.map((child) => (
          <RenderedNode nodeId={child.id} />
        ))}
      </>
    );
  }

  if (isElement(node)) {
    const childNodeGroups = getChildNodes(dom, node);

    return (
      <RenderedNodeContent
        node={node}
        childNodeGroups={childNodeGroups}
        Component={Component}
      />
    );
  }
}

노드를 렌더링하는 부분은 재귀 구조로 이루어져있다. Ref 노드를 렌더링할 때는 DnD 및 부가기능을 가지고 있는 RenderedNodeHud로 Ref의 노드를 렌더링하고, node.attributes.refId를 통해 Collection 노드를 호출한다.

Collection 노드는 가지고 있는 자식 노드를 다시 RenderedNode로 호출하고, 여기서 Element 노드인 자식 노드들이 RenderedNodeContent를 호출하며 최종적으로 렌더링된다.

이 매커니즘을 기반으로 렌더 트리를 확인한 결과, Ref 노드 안에 Collection 노드, 즉 원본 노드가 제대로 렌더링 되고 있었다.

가설 3: 노드 선택이 잘못 되고 있다.

노드를 선택하고 Drag and Drop 하는 로직이 RenderedNodeHud에 있는데, 여기서 선택되는 로직에 이상이 있는지 확인해보았다.

export function RenderedNodeHud({node, children}) {
    return (
        <Box
           onMouseDown={(e) => {
              e.stopPropagation();

              selectNode(node.id);
           }}
          >
            {children}
        </Box>
    )
}

노드를 선택하는 로직이다. 여러 개의 노드들이 중첩되어 있고, 이를 클릭했을 때 가장 안쪽의 노드가 선택된다. 이 때문에 Ref 노드가 아닌 원본 노드가 선택되고 있던 것이다.

그렇다면 이를 어떻게 해결할 수 있을까?

처음에는 타겟노드를 잡아서 document를 기준으로 부모 노드를 탐색한 뒤 Ref 노드가 있으면 해당 노드를 선택하는 방식을 생각했다. 하지만 document에 접근하는 방식보다 세련된 방식을 사용하길 원했다. 그래서 이벤트 전파를 이용했다.

stopPropagation은 이벤트 버블링을 막는다. 이벤트 버블링은 이벤트가 발생한 타겟 노드에서부터 상위 노드로 이벤트가 전파되는 것이다.
일반적인 경우에는 잘 사용하지 않지만, 이 케이스를 보자. stopPropagation이 없으면 가장 안쪽에서 selectNode가 실행되고, 상위 노드로 이벤트가 전파되어 다시 selectNode가 실행되면 타겟노드의 최상위 노드가 선택된다. 그럼 영원히 최상위의 Page 노드만 선택되는 것이다.

이 경우엔, 원본 노드에서는 이벤트를 전파하고, Ref 노드에 와서 이벤트 전파를 막으면 selectNode가 Ref 노드에서 멈춘다.

export function RenderedNodeHud({node, children}) {
    return (
        <Box
           onMouseDown={(e) => {
              if (!isCollection(getParent(dom, node))) {
                e.stopPropagation();

                selectNode(node.id);
              }
           }}
          >
            {children}
        </Box>
    )
}

여기서 추가적으로 Ref 편집 기능 정상화 요청이 들어왔다. Ref 노드에서 편집 모드로 전환하면 원본 노드에 접근할 수 있고, 여기서 편집할 시 Collection 노드에 반영된다.

  const isLocked = useMemo(() => {
    const editingNode = DomHelper.getNode(dom, editingNodeId);

      const ancestors = DomHelper.getAncestorNodes(
    dom,
    DomHelper.getNode(dom, nodeId),
  );

  // 부모 중에 Collection이 있거나, editing 중이 아닌 경우 lock
    return ancestors.some(
      (ancestor) =>
        DomHelper.isCollection(ancestor) &&
        (!editingNodeId ||
          (DomHelper.isRef(editingNode) &&
            editingNode?.attributes?.refId !== ancestor.id)),
    );
  }, [dom, node, editingNodeId]);

isLocked라는 변수를 만들어서 Ref 노드의 잠금 여부를 관리했다. Ref 노드 안쪽에는 여러가지 노드들이 중첩되어 쌓여있을 수 있기 때문에, 이를 판단하기 위해 조상 노드 중에서 Collection이 있는지 확인한다. 그리고 editing 중인지, editing 중인 Ref 노드가 조상인 Collection 노드를 참조하고 있는지 확인한다.

isLocked가 참이면 이벤트를 전파하지 않고, 거짓이면 전파하도록 한다.

if (!isLocked) {
  e.stopPropagation();

  selectNode(node.id);
}

이렇게 하면 lock 상태에서는 Ref 노드만 접근 가능하고 lock을 해제한 경우 원본 노드를 모두 접근 가능하다.

이슈 2: 노드 선택 및 hover 시 border 노출 안됨

기능 설명

노드를 선택하거나 hover했을 때 border로 해당 노드를 표시해주어야 한다.

문제 상황

처음엔 NodeOverlay라는 컴포넌트를 만들어, 상태에 따라 border를 노출하도록 했다. border가 앞으로 나와야 하기 때문에 z-index를 설정해주었는데, 이렇게 하면 해당 노드를 선택했을 때 그 안쪽의 노드가 선택이 안되는 이슈가 있었다.

<NodeOverlay
  isEmptyContainerActive={
    isActive &&
      isEmptyCanHaveChildrenNode(dom, props.node) &&
      !isAroundActive
  }
  isDropActive={isDropActive}
  isAroundActive={isAroundActive}
  isDragging={isDragging}
  isSelected={isSelectedNode}
  />
const NodeOverlay = ({
  isEmptyContainerActive,
  isAroundActive,
  isDropActive,
  isDragging,
  isSelected,
}) => {
  return (
    <Box
      sx={(theme) => ({
        zIndex: isDropActive || isDragging || isSelected ? 1000 : -1,
        border:
          (isSelected || isDropActive) && !isDragging
            ? `solid 2px ${theme.colors.primary.light}`
            : '',
      })}
    ></Box>
  );
};

이러한 이슈로 NodeOverlay는 걷어내고 가상 선택자 before을 사용했는데, 테두리 안쪽으로 border가 생겨 노드가 background 색상을 가지고 있는 경우 border가 보이지 않는 이슈가 있었다. 마찬가지로 z-index를 설정하면 내부 노드를 선택하지 못하는 이슈가 발생했다.

<Box
  sx={(theme) => ({
    '&::before': {
      border: isHover
      ? `solid 2px ${theme.colors.primary.light}`
      : (isSelectedNode || isDropActive) && !isDragging && !isRoot
      ? `solid 2px ${theme.colors.primary.light}`
      : '',
    },
  })}
  >

해결

구글링을 하던 중 z-index로 인한 겹쳐진 영역에서 이벤트 처리라는 글을 보았고, pointer-events라는 CSS 속성을 알게 됐다.

pointer-events 속성을 none으로 설정하면 해당 엘리먼트에서는 마우스 이벤트가 발생하지 않는다. before 가상선택자는 z-index 우선순위가 높아도 마우스 이벤트 타겟에서 제외되기 때문에 중첩된 노드를 문제 없이 잡을 수 있다.

<Box
  sx={(theme) => ({
    '&::before': {
      border: isHover
      ? `solid 2px ${theme.colors.primary.light}`
      : (isSelectedNode || isDropActive) && !isDragging && !isRoot
      ? `solid 2px ${theme.colors.primary.light}`
      : '',
      zIndex: 100,
      pointerEvents: 'none',
    },
  })}
  >

"배워서 남주는 개발자 김채은입니다 ( •̀ .̫ •́ )✧"

김채은,프론트엔드 개발 @ 나두모두

Read more

나두아이오 새 기능을 소개합니다!      레이아웃 변경은 진짜 강추!

나두아이오 새 기능을 소개합니다! 레이아웃 변경은 진짜 강추!

홈페이지를 만들다 보면 "내용은 좋은데 디자인이 살짝 아쉽네?" 혹은 "다른 스타일로 바꾸면 어떨까?" 고민될 때가 많으시죠? 이제 그런 고민은 더이상 하실 필요 없습니다! 이번 주 새로 출시된 '레이아웃 변경' 기능만 있다면 누구나 감각적인 페이지를 완성할 수 있거든요. ✅ 뚝딱뚝딱, 더 빨라진 섹션 추가 1분만에 AI가

By 나두아이오
이제 우리 홈페이지에 "퀵 버튼"을 달 수 있어요!

이제 우리 홈페이지에 "퀵 버튼"을 달 수 있어요!

두둥! 나두아이오가 홈페이지 방문자의 편의를 높이기 위해 '플로팅 위젯'이라는 기능을 새롭게 출시했습니다! 이제 복잡한 코딩 없이 클릭 몇 번으로 비즈니스에 필요한 핵심 버튼을 홈페이지 하단에 상시 노출할 수 있어요! 1. 플로팅 위젯이란? 홈페이지 화면 하단에 고정되어 방문자가 가장 필요로 하는 메뉴가 항상 따라다니는 버튼입니다. 홈페이지에서 메뉴를 찾아다닐

By 나두아이오
내 브랜드의 기초, 나두아이오의 이메일 포워딩으로 시작하세요!

내 브랜드의 기초, 나두아이오의 이메일 포워딩으로 시작하세요!

사업을 시작할 때 홈페이지만큼 중요한 것이 바로 비즈니스 메일 주소입니다. 고객 입장에서 contact@gmail.com보다는 hello@mybrand.com이라는 주소로 메일을 받을 때 훨씬 큰 신뢰를 느끼기 때문이죠. 오늘은 나두아이오에서 제공하는 '이메일 포워딩 서비스'를 활용해 쉽고 저렴하게 브랜드 메일을 운영하는 방법을 소개해 드리겠습니다! 1. 이메일 포워딩이란? 이메일 포워딩은

By 나두아이오
해외 바이어를 위한 인보이스 생성 및 발송 관리 서비스

해외 바이어를 위한 인보이스 생성 및 발송 관리 서비스

수출 업무를 하시는 분이라면 인보이스 관리가 얼마나 중요한지 아실 거예요. 한국에서는 홈택스와 연동된 전자세금계산서를 발행하면 모든 게 전자화되어 있어서 관리가 문제 없는데, 해외 거래처와는 인보이스가 중요한 문서이기 때문에 잘 관리를 해야 합니다. 우리 나라에서는 인보이스 (청구서)를 자동으로 생성하고 발송해주는 서비스가 없어서 많이들 엑셀을 사용하시는데요, 엑셀로 인보이스를 만들면 이력 및

By 나두아이오