FEB:)DAIN

[refactor] Ref를 이용해 만든 팝업 메뉴 CSS만으로 구현하기 본문

기록/프로젝트 카페인

[refactor] Ref를 이용해 만든 팝업 메뉴 CSS만으로 구현하기

얌2 2023. 8. 26. 01:24
728x90

0. 리팩터링을 진행한 이유와 요구사항

팝업 메뉴를 구현한 팀원의 코드를 보니 ref를 쓰지 않고, css만으로도 간단하게 구현할 수 있을 것 같아 리팩터링을 진행했다.
팀원의 요구사항은 다음과 같았다.
1) 트리거의 위치가 바뀌어도 팝업 메뉴가 그 트리거 옆에 있어야 한다.
  1-1) 얼마나 떨어져 있을지도 정할 수 있어야 한다.
2) 팝업 메뉴 크기는 자식 메뉴에 맞춰 유동적으로 바뀌어야 한다.
  2-1) 자식 메뉴 수는 줄이거나 늘릴 수 있다. 메뉴 수에 따라 팝업 메뉴 높이가 바뀌어야 한다.
  2-2) 자식 메뉴 글자 수가 길어지면 그 길이에 맞춰 팝업 메뉴 너비가 늘어나야 한다.

1. 코드 비교

1) Ref를 이용한 팝업 메뉴 - 리팩터링 전

// Menu.tsx
<PopupMenu trigger={<UserCircleIcon width="2.8rem" stroke="#333" />} menus={loginMenus} />

// PopupMenu.tsx
import type { PropsWithChildren, ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';

import Box from '@common/Box';
import FlexBox from '@common/FlexBox';

import Menus from './Menus';

interface Props {
  trigger: ReactNode;
  menus: PropsWithChildren<{ onClick: () => void }>[];
}

const PopupMenu = ({ trigger, menus }: Props) => {
  const triggerRef = useRef<HTMLButtonElement>(null);
  const [isOpen, setIsOpen] = useState(false);
  const [triggerWidth, setTriggerWidth] = useState(0);

  const handleToggleMenu = () => {
    setIsOpen((prev) => !prev);
  };

  const handleCloseMenu = () => {
    setIsOpen(false);
  };

  useEffect(() => {
    if (triggerRef.current) setTriggerWidth(triggerRef.current.offsetWidth);
  }, []);

  return (
    <FlexBox css={container}>
      <button ref={triggerRef} onClick={handleToggleMenu}>
        {trigger}
      </button>
      <Box css={getMenuContainerCss(triggerWidth)}>
        {isOpen && <Menus menus={menus} closeMenu={handleCloseMenu} />}
      </Box>
    </FlexBox>
  );
};

const container = css`
  position: relative;
`;

const getMenuContainerCss = (triggerWidth: number) => {
  return css`
    position: absolute;
    top: -2rem;
    left: calc(${triggerWidth}px + 2rem);
  `;
};

2) CSS로 만든 팝업 메뉴 - 리팩터링 후

// Menu.tsx
<PopupMenu menus={loginMenus} />

// PopupMenu.tsx
import { UserCircleIcon } from '@heroicons/react/24/outline';
import { css } from 'styled-components';

import type { PropsWithChildren } from 'react';
import { useState } from 'react';

import FlexBox from '@common/FlexBox';

import Menus from './Menus';

interface Props {
  menus: PropsWithChildren<{ onClick: () => void }>[];
}

const PopupMenu = ({ menus }: Props) => {
  const [isOpen, setIsOpen] = useState(false);

  const handleToggleMenu = () => {
    setIsOpen((prev) => !prev);
  };
  
  const handleCloseMenu = () => {
    setIsOpen(false);
  };

  return (
    <FlexBox css={container}>
      {isOpen && <Menus menus={menus} closeMenu={handleCloseMenu} />}
      <button onClick={handleToggleMenu}>
        <UserCircleIcon width="2.8rem" stroke="#333" />
      </button>
    </FlexBox>
  );
};

const container = css`
  position: relative;
  display: inline-block;
  
  & > ul:first-child {
    position: absolute;
    top: -18px;
    left: calc(100% + 20px);
  }
`;

2. 성능 비교 (with 크롬 브라우저의 performance, lighthouse)

css
ref

 

✨ 결과

코드가 간결해져 가독성이 훨씬 좋아졌다!😄 불필요한 Ref, 상태관리도 사라지고 디자인을 위한 코드는 CSS에만 둘 수 있게 됐다.

짧은 시간 내에 성능도 개선되었다.
리팩터링하면서 반응형 디자인을 위해 미디어 쿼리도 적용했는데, 성능이 나빠지기는 커녕 더 좋아졌다.

1. TBT (Total Blocking Time) 38.3% 감소

TBT란? FCP(First Content Paint, 화면에 콘텐츠가 처음 그려지는  시간)와 TTI(상호작용 시작 시간) 사이의 총 시간

2. Lighthouse 총 점수 10점 증가


3. 레이아웃 재계산 시간 50% 이상 감소

투자한 시간(30분 정도)에 비해 훨씬 더 좋은 결과를 얻었다.

728x90