파크로그
article thumbnail
Published 2022. 8. 31. 21:05
Dropdown 구현 (2) Project/Co-Studo

Dropdown (2)

Dropdown 을 만들어보고있다.

이전에 포스팅 했던 Dropdown 의 컨셉을 그대로 가져와서 사용해보려고 하고 있는데,

Trigger 는 외부로 부터 받아와 button 의 children 으로 사용하고자 했다.

 

const DropdownTrigger = (props: { trigger: ReactNode }) => {
  const [, dispatch] = useDropdownContext();
  const { trigger } = props;

  const handleTrigger = () => {
    dispatch({ type: 'TOGGLE_OPEN' });
  };

  return (
    <button type="button" onClick={handleTrigger}>
      {trigger}
    </button>
  );
};

// UserInfoCircle

const UserInfoCircle: React.FC = () => {
  const { data } = useFetchMeInterval();
  
  return (
    <Dropdown>
      <Dropdown.Trigger
        trigger={
          <img
            css={css`
              width: 2.4rem;
              height: 2.4rem;
              border-radius: 1.2rem;
            `}
            src={data?.results?.avatarUrl}
            alt={data?.results?.nickname}
          />
        }
      />
      <Dropdown.List>
        <Dropdown.Item>안녕하세요</Dropdown.Item>
        <Dropdown.Item>무야호</Dropdown.Item>
      </Dropdown.List>
    </Dropdown>
  );
};

여기서 Dropdown List 의 position 을 지정해주지 않으면 당연히 List 가 생기면서 Element 가 밀릴 것이다.

그래서 DropdownList 가 기본적으로 position absolute 를 갖도록 하였다.

 

const DropdownList = (props) => {
  const [{ isOpen }] = useDropdownContext();
  const { children } = props;

  return isOpen ? (
    <ul
      css={css`
        position: absolute;
      `}
      {...props}
    >
      {children}
    </ul>
  ) : null;
};

비참한 결과물

이제 List element 에 top, left 등의 값을 지정해줘야 하는데,

Trigger 의 자식으로 태그를 넣어서 사용하면 relative, absolute 관계를 이용해 쉽게 위치를 조정할 수 있겠지만 List 가 Trigger 의 자식태그라는 관계는 절대 아니라고 생각했다.

 

기존에 Context Provider 의 역할로만 사용했던 Dropdown 컴포넌트에 layout 을 잡기위한 부모태그를 하나 생성하여 사용하였다.

 

const Dropdown = ({ children }) => {
  const dropdownReducer = useReducer(reducer, initState);
  return (
    <DropdownContext.Provider value={dropdownReducer}>
      <div css={{ position: 'relative' }}>{children}</div>
    </DropdownContext.Provider>
  );
};

 

이제 top 과 left 값을 계산해야 하는데, top 은 trigger 로 사용중인 element 의 height 만큼 띄우면 될 것이고, left 는 element 의 왼쪽을 기준으로 띄우던지, 오른쪽을 기준으로 띄우던지 선택할 수 있어야겠다고 생각했다. (추후 width를 기준으로 center 도 만들 수 있지 않을까?)

 

그래서 trigger 의 height 값이 필요하다고 생각이 들어, useRef 를 통해 triggerRef 를 설정했다.

 

const DropdownTrigger = (props: { trigger: ReactNode }) => {
  const triggerRef = useRef<HTMLButtonElement>(null);
  const [, dispatch] = useDropdownContext();
  const { trigger } = props;

  const handleTrigger = () => {
    dispatch({ type: 'TOGGLE_OPEN' });
  };

  useEffect(() => {
    if (triggerRef.current) {
      dispatch({
        type: 'UPDATE_TRIGGER_POSITION',
        height: triggerRef.current.offsetHeight,
      });
    }
  }, [dispatch]);

  return (
    <button type="button" onClick={handleTrigger} ref={triggerRef}>
      {trigger}
    </button>
  );
};

triggerRef 는 렌더링 된 이후 설정이 되니까 useEffect 에서 trigger 의 height 정보를 dispatch 하도록 접근하였다.

그런데 생각해보니 Trigger 는 렌더링이 될 것이고, handleTrigger 핸들러는 trigger 가 생성된 이후 실행될 것이므로

TOGGLE_OPEN action 에서 height 를 지정해줘도 되겠다는 생각이 들었다.

 

그러니까, 초기 로딩에서 앱 렌더링 -> (useEffect 에서 trigger height dispatch -> 리렌더링) 에서 리렌더링(괄호)을 하나 줄이는 것이지..

 

const DropdownTrigger = (props: { trigger: ReactNode }) => {
  const triggerRef = useRef<HTMLButtonElement>(null);
  const [, dispatch] = useDropdownContext();
  const { trigger } = props;

  const handleTrigger = () => {
    if (!triggerRef.current) return;
    dispatch({ type: 'TOGGLE_OPEN', height: triggerRef.current.offsetHeight });
  };

  return (
    <button type="button" onClick={handleTrigger} ref={triggerRef}>
      {trigger}
    </button>
  );
};

 

일단은 잘 된다. 그런데 테스트해보는 과정에서 layout shift 라는 것을 발견하는데... 

 

Dropdown click away

dropdown 의 클릭 이벤트 중, 드롭다운 이외의 영역을 클릭 시 isOpen state 가 변경되도록 하는 로직을 만들었다.

CSS 의 가상요소를 활용했는데, Open State 를 변경하기위한 별도의 태그를 만들기 보다 해당 방법이 의도하는 바를 알기 쉽다고 생각하여 선택했다.

 

const Dropdown = ({ children }) => {
  const dropdownReducer = useReducer(reducer, initState);
  const [state, dispatch] = dropdownReducer;
  return (
    <DropdownContext.Provider value={dropdownReducer}>
      <div
        css={css`
          position: relative;
          &::before {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            content: '';
            z-index: 90;
            display: ${state.isOpen ? 'block' : 'none'};
          }
        `}
        onClick={() => {
          if (state.isOpen) {
            dispatch({ type: 'TOGGLE_OPEN' });
          }
        }}
      >
        {children}
      </div>
    </DropdownContext.Provider>
  );
};

trigger 의 위치를 기반으로 position 을 잡기위해 만들었던 div 태그에 ::before 가상요소를 만들어 Dropdown 의 상태를 Toggle 하도록 하였다.

 

그리고 Click Away 라는 상황을 Dropdown 이외에서도 다양하게 쓰여질 수 있다고 생각하여 해당 로직을 분리하였다. 

 

 

+) positioning 을 위해 다르게 시도해본 방법

 

mui 는 anchorEl 이라는 element 의 위치를 getBoundingClientRect 를 통해 얻어 position 을 주는 것 처럼 보였다. 링크

Popover 라는 컴포넌트를 선언하여 사용하고, anchorEl 이라는 state 를 만들어 trigger 로 만들어 사용하는 것으로 보여졌다.

 

나처럼 Dropdown 이라는 Container 같은 component 를 만들어 사용하는 법도 있을 것이고, 저런 방법도 있겠구나 싶었다.

저 방법으로 시도해 보았을 때에는 relative 와 absolute 관계를 맺기가 힘들어 직접 top, left 를 정해 지정해주는 식으로 사용했었는데

viewport 의 크기가 달라지면 대응하기 위한 로직을 추가로 작성해야했다.

 

그래서 일단은 현재 방식대로 쓰는걸로,

안녕히계세요 트리거~

 

profile

파크로그

@파크park

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!