[React] 드롭다운 메뉴 만들기, 정렬 기능

게코젤리·2023년 4월 3일
0

'최신 순', '오래된 순'과 같이 리스트의 정렬을 사용자가 선택할 수 있게 하는 기능이 필요했다. html의 select, option 태그로 구현할 수 있겠지만 그 경우 option을 커스텀 할 수 없다는 단점이 있다. 그 대신 리액트의 상태 관리를 활용해 커스텀 스타일링이 가능한 드롭다운 메뉴를 만들어 사용자가 정렬을 선택하고 그에 따라 리스트를 재랜더링 할 수 있도록 했다.

구조, 스타일링

const Wrapper = styled.div`
  position: relative;
  width: 150px;
  font-size: 14px;
  font-weight: 300;
`;

const DropdownBtn = styled.div<{ isOpen: boolean }>`
  position: relative;
  height: 34px;
  background-color: ${(props) => props.theme.gray};
  border-radius: ${(props) => (props.isOpen ? '4px 4px 0 0' : '4px')};
  padding: 8px;
  cursor: pointer;

  .arrow {
    position: absolute;
    right: 10px;
  }
`;

const DropdownMenu = styled.ul`
  position: absolute;
  top: 100%;
  left: 0;
  width: 100%;
  background-color: ${(props) => props.theme.gray};
  border-radius: 0 0 4px 4px;
  overflow: hidden;
  z-index: 1000;
`;

const DropdownOption = styled.li`
  border-top: 1px solid ${(props) => props.theme.black.middle};
  padding: 8px;
  cursor: pointer;
  &:hover {
    background-color: ${(props) => props.theme.purpleDark};
  }
`;

<Wrapper>
  <DropdownBtn>
    담은 순 <span className='arrow'></span>
  </DropdownBtn>
  <DropdownMenu>
    <DropdownOption>담은 역순</DropdownOption>
    <DropdownOption>평점 높은 순</DropdownOption>
    <DropdownOption>평점 낮은 순</DropdownOption>
  </DropdownMenu>
</Wrapper>

DropdownBtn에는 현재 값이 표시된다. 클릭하면 DropdownMenu가 나타나고 DropdownOption을 선택하면 DropdownMenu가 사라지면서 DropdownBtn에 선택한 값이 담긴다.

타입, 함수

export type ISortMovies = IFavoriteMovie | IRatedMovie;

interface ISortMoviesData {
  [key: number]: ISortMovies;

export type ISortType =
  | 'newest'
  | 'oldest'
  | 'lowAveRate'
  | 'highAveRate'
  | 'lowMyRate'
  | 'highMyRate';

export const sortName = {
  newest: '담은 순',
  oldest: '담은 역순',
  lowAveRate: '평균 별점 낮은 순',
  highAveRate: '평균 별점 높은 순',
  lowMyRate: '내 별점 낮은 순',
  highMyRate: '내 별점 높은 순',
};
export const sortMovies = (
  movie: ISortMoviesData,
  sortType: ISortType
): ISortMovies[] => {
  const movieArr = Object.values(movie);
  if (movieArr.length === 0) return [];

  switch (sortType) {
    case 'newest':
      return movieArr.sort(
        (a, b) =>
          new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
      );
    case 'oldest':
      return movieArr.sort(
        (a, b) =>
          new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
      );
    case 'lowAveRate':
      return movieArr.sort((a, b) => a.vote_average - b.vote_average);
    case 'highAveRate':
      return movieArr.sort((a, b) => b.vote_average - a.vote_average);
    case 'lowMyRate':
      return movieArr.sort((a, b) => a.rate - b.rate);
    case 'highMyRate':
      return movieArr.sort((a, b) => b.rate - a.rate);
    default:
      return movieArr;
  }
};

필요한 Type과 함수를 선언해주었다. sortName은 화면에 보여줄 sort 옵션의 이름을 객체로 분리했다. 드롭 다운 메뉴는 현재 sortType의 상태만 관리한다. 때문에 실제로 리스트를 수정할 함수가 필요하다. sortMovies은 정렬할 리스트와 드롭 다운 메뉴에서 선택한 sortType을 인자로 받아 리스트를 수정해 return한다.

구현

function SortOption({ sortTypeArr, setSortType }: ISortOption) {
  const dropDownMenuRef = useRef<HTMLUListElement>(null);
  const [isOpen, setIsOpen] = useState(false);
  const [currName, setCurrName] = useState(sortName[sortTypeArr[0]]);
  // 외부 클릭 감지
  useEffect(() => {
    const onClickOutside = (event: Event) => {
      if (
        dropDownMenuRef.current &&
        !dropDownMenuRef.current.contains(event.target as Node)
      ) {
        setIsOpen(false);
      }
    };
    document.addEventListener('mousedown', onClickOutside);
    document.addEventListener('touchstart', onClickOutside);
    return () => {
      document.removeEventListener('mousedown', onClickOutside);
      document.removeEventListener('touchstart', onClickOutside);
    };
  }, []);

  const onClickOption = (sortType: ISortType) => () => {
    setCurrName(sortName[sortType]);
    setIsOpen(false);
    setSortType(sortType);
  };

  return (
  <Wrapper>
    <DropdownBtn onClick={() => setIsOpen((prev) => !prev)} isOpen={isOpen}>
      {currName} <span className='arrow'></span>
    </DropdownBtn>
    {isOpen && (
      <DropdownMenu ref={dropDownMenuRef}>
        {sortTypeArr.map((sortType) => {
          if (sortName[sortType] !== currName) {
            return (
              <DropdownOption
                key={sortType}
                onClick={onClickOption(sortType)}
                >
                {sortName[sortType]}
              </DropdownOption>
            );
          }
        })}
      </DropdownMenu>
    )}
  </Wrapper>
  )
}

// prop으로 받는 sortTypeArr
 const sortTypeArr: ISortType[] = [
    'newest',
    'oldest',
    'lowAveRate',
    'highAveRate',
  ];
  1. isOpen state로 DropdownMenu의 상태를 관리한다. DropdownBtn을 클릭했을 때 나타나고 DropdownOption을 선택했을 때 사라진다. 그리고 사용자 경험을 위해 DropdownMenu 바깥을 클릭했을 때 DropdownMenu가 사라지는 기능도 추가했다. 현재 컴포넌트가 마운트될 때 'mousedown', 'touchstart'(모바일) 이벤트 리스너를 추가하고 클릭한 곳이 DropdownMenu가 아닐 때 setIsOpen(false)로 한다.

  2. sortTypeArr를 매핑해서 DropdownOption을 return한다. currName이 sortType의 이름과 같을 때는 return 하지 않는다.

  3. DropdownOption을 클릭했을 때 클릭한 sortType으로 부모의 sortType을 state를 변경한다.(변수명이 좀 더 명확할 필요는 있을 듯.) 그리고 currName을 변경해 DropdownBtn에 표시되도록 한다.

0개의 댓글