Long Press Menu (w. framer motion): Refactoring

김 주현·2023년 12월 19일
0

UI Component 개발

목록 보기
8/11

이전 포스트에서 Long Press Menu를 구현해보았다. 그런데,, 실제로 써먹으려고 하니까 좀 많이 손을 봐야해서 아예 다시 리팩토링을 하려고 한다.


리팩토링 목표

애니메이션과 Context Menu 기능의 분리

  • 현재는 이 두 개가 같이 엮여서 구현되어 있는데, 이는 유연성을 가지지 못한다.

Context Menu 기능과 Presentation의 분리

  • TriggerButton과 ContextMenu가 주어진 List에 의존적이었고, UI가 고정되어 있었다.
  • 근데 주어진 목록에서 선택된 걸 보여주는 게 아니라, TriggerButton은 다른 걸 보여주어야 하는 상황.
  • 그러면서도 선택의 상태는 반영이 되어야 했다.
  • 그러므로,, 기능만 제공하고 UI는 부모가 처리하도록 만드는 것이 좋겠다.

PressableContextMenu로 변경

  • LongPressMenu보단 PressableContextMenu가 이름이 더 가까운 것 같다.

LikeWithList로 확장

  • PressableContextMenu는 기능에 대한 것만 제공하는 unstyled component로 놔두고, 이를 이용해서 써먹는 LikeWithList를 만들기로 했다.
  • VerticalLayout으로 PageLayout을 만든 것과 같은 느낌이랄까.
  • 사실 이게 이번 포스팅의 골자.

코드

PressableContextMenu.tsx

/** React */
import { useState } from 'react';

/** Hook */
import { usePressableContextMenu } from './usePressableContextMenu';

/** Type */
import type { PointerEvent } from 'react';
import type { PressableContextMenuProp } from './PressableContextMenu.types';

export const PressableContextMenu = ({
  onSelect,
  triggerButton,
  contextMenu,
}: PressableContextMenuProp) => {
  const { buttonRef, startLongMenu, moveLongMenu, cleanUpLongMenu } = usePressableContextMenu();

  const [isPointerDown, setIsPointerDown] = useState(false);
  const [hoverIndex, setHoverIndex] = useState(0);

  const handlePointerDown = (event: PointerEvent<HTMLDivElement\>) => {
    startLongMenu(event);
    setIsPointerDown(true);
  };

  const handlePointerMove = (event: PointerEvent<HTMLDivElement\>) => {
    moveLongMenu(event, { onHover: setHoverIndex });
  };

  const handlePointerUp = () => {
    cleanUpLongMenu();
    setIsPointerDown(false);
    onSelect(hoverIndex);
  };

  return (
    <div style={{ position: 'relative' }}>
      <div
        ref={buttonRef}
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
     >
        {triggerButton(isPointerDown)}
      </div>

      {contextMenu(isPointerDown, hoverIndex)}
    </div>
  );
};

아주 심플해진 ContextMenu. ContextMenu에 필요한 것만 남기고 보니 간단해졌다. 해당 컴포넌트의 역할은 이렇다.

PressableContextMenu의 역할

  • 버튼 Press 상태일 때 contextMenu를 띄운다.
  • 버튼의 자식과 ContextMenu의 자식에게 필요한 상태를 넘겨준다.

조금 포인트 되는 부분은 Prop으로 받은 트리거 버튼과 컨텍스트 메뉴를 렌더하는 부분이겠다.

함수 렌더

{triggerButton(isPointerDown)}
{contextMenu(isPointerDown, hoverIndex)}

일반적인 경우엔 {triggerButton} 으로 렌더하겠지만~ 따져보니 트리거 버튼에게 현재 눌렸는지 어쨌는지 정도 알려줘야 그에 대한 UI feedback을 해줄 것 같아 버튼 눌림에 대한 상태를 넘겨주었다. 컨텍스트 메뉴 역시 필요한 정보를 넘겨주었다.

이렇게 되면 상위 컴포넌트에선 이렇게 불러주면 된다.

상위 컴포넌트에서 Prop 넘겨주기

  const returnTriggerButton = (isPressed: boolean) => {
    return (
      <S.Button>
        // ...
      </S.Button>
    );
  };

  const returnContextMenu = (isOpened: boolean, hoverIndex: number) => {
    return (
      <S.Ul>
        // ...
      </S.Ul>
    );
  };

  return (
    <PressableContextMenu
      onSelect={handleSelect}
      triggerButton={returnTriggerButton}
      contextMenu={returnContextMenu}
    />
  );

이런 느낌이다. 결국 이렇게 되면 triggerButton이나 contextMenu는 받은 상태들로 어떻게 나타낼지만 신경쓰면 된다.

CustomHook으로도 만들어서 해볼까 싶었는데~ 어떻게 구현하든 둘 다 비슷할 것 같긴 했다.


후기

생각해보면 나는 비즈니스 로직과 UI를 같이 짜는 경향이 있다. 애니메이션도 뭔가 '동작'하는 거라 생각이 돼서 같이 넣곤 하는데,,, 앞으로는 좀 더 분리하도록 노력해야겠다.

profile
FE개발자 가보자고🥳

0개의 댓글