Skip to content

네비게이션 바 애니메이션

dannysir edited this page Nov 22, 2024 · 1 revision

네비게이션 바 애니메이션

아래 보이는 기존 코드를 보면 로직이 다음과 같이 진행된다.

  • searchParams를 이용해 버튼 클릭시 param을 변경한다.
  • 만약 버튼이 현재 선택된 param과 같은 값이면 boarder를 추가한다.

기존 코드

import { useSearchParams } from "react-router-dom";

 type MarketType = "전체" | "코스피" | "코스닥" | "나스닥";

 export default function Nav() {
   const [searchParams, setSearchParams] = useSearchParams();
   const currentMarket = searchParams.get("top") || "전체";

   const markets: MarketType[] = ["전체", "코스피", "코스닥", "나스닥"];

   const handleMarketChange = (market: MarketType) => {
     if (market === "전체") {
       searchParams.delete("top");
       setSearchParams(searchParams);
     } else {
       setSearchParams({ top: market });
     }
   };

   return (
     <div className="flex text-xl font-bold gap-1 px-3">
       {markets.map((market) => (
         <button
           key={market}
           onClick={() => handleMarketChange(market)}
           className={`py-2 px-2 ${
             currentMarket === market
               ? "border-b-4 border-juga-grayscale-black"
               : ""
           }`}
         >
           {market}
         </button>
       ))}
     </div>
   );
 }

기존 코드를 이용하면 각각의 버튼에 속해있는 boarder 이기 때문에 다른 영역으로 이동하는 애니메이션을 구현하기 어렵다. 따라서 아래 표시선이 이동하는 슬라이딩 애니메이션을 구현하기 위해서는 표시선(막대선)을 위한 별도의 엘리먼트가 필요하다.

따라서 아래와 같이 표시선을 위한 엘리먼트를 생성하고 전체 버튼 영역을 독립적으로 움직일 수 있게 만들었다. 또한 useRef를 이용해 조작을 하기위해 ref를 지정했다.

const indicatorRef = useRef<HTMLDivElement>(null);

      //... 생략

      <div
        ref={indicatorRef}
        className='absolute bottom-0 h-1 bg-juga-grayscale-black transition-all duration-300'
        style={{ height: '4px' }}
      />

useEffect를 이용한 애니메이션

앞선 표시선 엘리먼트를 선언하며 스타일로 transition을 주었기 때문에 서서히 변경되는 부분이 적용되었다.

이제 버튼을 클릭했을 때 바뀌는 currentMarket을 dependency로 이용해 버튼을 클릭할 때마다 표시선의 스타일을 변경하는 로직을 구현했다. 로직은 다음과 같은 순서로 진행된다.

  • buttonRefs 를 이용해 각각의 버튼을 배열로 가지고 있게 만든다.
  • useEffect 실행.
    • currentMarket 에 해당하는 buttonRefs 를 가져온다.
    • indicatorRef 로 표시선을 가져온다.
    • indicatorRef 의 스타일을 buttonRefs 에서 가져온 값을 바탕으로 수정한다.
      • left값 변경.
      • 넓이 변경.
  // currentMarket 선언 참고
  const currentMarket = searchParams.get("top") || "전체";

	const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
  
  useEffect(() => {
    const currentButton = buttonRefs.current[markets.indexOf(currentMarket as MarketType)];
    // 표시선 엘리먼트
    const indicator = indicatorRef.current;

    if (currentButton && indicator) {
      indicator.style.left = `${currentButton.offsetLeft}px`;
      indicator.style.width = `${currentButton.offsetWidth}px`;
    }
  }, [currentMarket]);

전체 코드

import { useSearchParams } from 'react-router-dom';
import { useEffect, useRef } from 'react';

type MarketType = '전체' | '코스피' | '코스닥' | '나스닥';

export default function Nav() {
  const [searchParams, setSearchParams] = useSearchParams();
  const currentMarket = searchParams.get('top') || '전체';
  const indicatorRef = useRef<HTMLDivElement>(null);
  const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);

  const markets: MarketType[] = ['전체', '코스피', '코스닥', '나스닥'];

  const handleMarketChange = (market: MarketType) => {
    if (market === '전체') {
      searchParams.delete('top');
      setSearchParams(searchParams);
    } else {
      setSearchParams({ top: market });
    }
  };

  useEffect(() => {
    const currentButton =
      buttonRefs.current[markets.indexOf(currentMarket as MarketType)];
    const indicator = indicatorRef.current;

    if (currentButton && indicator) {
      indicator.style.left = `${currentButton.offsetLeft}px`;
      indicator.style.width = `${currentButton.offsetWidth}px`;
    }
  }, [currentMarket]);

  return (
    <div className='relative flex gap-1 px-3 text-xl font-bold'>
      <div
        ref={indicatorRef}
        className='absolute bottom-0 h-1 bg-juga-grayscale-black transition-all duration-300'
        style={{ height: '4px' }}
      />

      {markets.map((market, index) => (
        <button
          key={market}
          ref={(el) => (buttonRefs.current[index] = el)}
          onClick={() => handleMarketChange(market)}
          className={`relative px-2 py-2`}
        >
          {market}
        </button>
      ))}
    </div>
  );
}

📜 개발 일지

⚠️ 트러블 슈팅

❗ 규칙

🗒️ 기록

기획
회의록
데일리스크럼
그룹 멘토링
그룹 회고

😲 개별 멘토링

고동우
김진
서산
이시은
박진명
Clone this wiki locally