모달 제목 애니메이션 구현하기 - boostcampwm-2022/web33-Mildo GitHub Wiki

모달 제목에 애니메이션을 구현해야 했던 이유

  • ‘반응형 웹’을 구현하고자 했었기 때문에, 모든 디바이스에서 편하게 서비스를 이용할 수 있어야 했습니다.
  • 그러나 데스크탑이 아닌 모바일 화면에서는 모달의 제목이 모바일 화면보다 더 커서 잘리게 되는 현상이 발생합니다.

스크린샷 2022-12-11 오후 7.27.37.png

  • 다음 화면은 갤럭시 폴드의 예시입니다. 모달 제목의 글씨 길이가 너무 길어서 화면에 잘리는 현상이 발생하는 것을 알 수 있습니다.

어떻게 구현할 수 있을까?

이제 본격적으로 모달창에 애니메이션을 적용하는 과정에 대해 생각해 보아야 했습니다.

  • 가장 먼저 어떤 경우에 애니메이션을 주지 않아도 전혀 문제 없이 사용할 수 있고, 어떤 경우에 애니메이션을 주어아 하는 지에 대한 명확한 기준을 세워야 했습니다. 그 기준은 바로 ‘실제 콘텐츠와 모달의 길이를 비교해서 콘텐츠의 길이가 더 길다면 애니메이션을 주자!’ 였습니다.
  • 실제 콘텐츠(제목)의 너비 → useRef.current의 clientWidth 속성
  • 모달 너비 속성 → window.innerWidth(어차피 애니메이션 구간은 모달 너비와 전체 화면 너비가 일치했을 경우에만 적용되기 때문입니다.)
  • 그런 다음에는, 애니메이션의 슬라이딩을 무한 반복처럼 보이게 해야 했습니다. 즉, @keyframes를 적용했을 때 처음(0%, from)과 끝(100%, to) 부분이 서로 맞아야 했습니다. 이를 위해서는 다음과 같이 작업을 해야 했습니다.
    • 애니메이션을 주어야 하는 경우에 한해 두 개의 Title 컴포넌트를 넣어주었으며, 이를 Flexbox로 설정
    • @keyframes의 0%와 100%의 transform의 x값을 현재 제목의 길이를 기반으로 설정
    • gap을 50%로 설정

구현 과정

Title 속성에 width 제거하기

export const Title = styled.h1`
  ...
  /* width: 100%; */
	...
`;
  • 정확한 길이를 알아내기 위해 width: 100%를 제거하였습니다.

제목 길이 구하기

const titleWidthRef = useRef<HTMLHeadingElement>(null);
...
useEffect(() => {
  if (titleWidthRef && titleWidthRef.current) {
    setTitleWidth(titleWidthRef.current.clientWidth);
  }
}, [firstLevelInfo]);
  • 제목의 길이를 구하기 위해 useRef를 사용하였습니다.
  • useEffect를 사용해, 모달 제목이 바뀌는 경우에만 useEffect 내부의 함수를 실행시킬 수 있도록 했습니다. 이 때, setTitleWidth()를 통해 상태값을 바꿀 수 있도록 했습니다. setTitleWidth()는 useState()를 사용하여 상태를 바꿀 수 있는 dispatch 함수입니다.
  • 여기에 Ref 엘리먼트 내부의 값인 clientWidth를 넣습니다.

모달 길이(화면 전체 너비) 구하기

const [windowWidth, setWindowWidth] = useState<number>(window.innerWidth);

useEffect(() => {
    const checkViewportWidth = () => {
      setWindowWidth(window.innerWidth);
    };

    window.addEventListener('resize', checkViewportWidth);

		// 클린업 함수
    return () => {
      window.removeEventListener('resize', checkViewportWidth);
    };
  }, []);
  • 모달의 너비는 애니메이션이 존재한다는 전제 하에 화면의 너비가 바뀔 시 따라서 바뀝니다.
  • ‘resize’ 이벤트 핸들러를 통해 화면의 너비가 바뀌면 checkViewportWidth 함수를 실행시킬 수 있도록 합니다.
  • 컴포넌트가 언마운트되는 시점과 재렌더링이 되는 시점 사이에 이벤트 핸들러를 해제시켜 메모리 누수를 방지합니다.

두 길이를 기반으로 애니메이션 여부 설정하기

const [slidable, setSlidable] = useState<boolean>(true);
...
useEffect(() => {
  if (titleWidth + 50 > windowWidth) {
    setSlidable(true);
    return;
  }
  setSlidable(false);
}, [titleWidth, windowWidth]);
  • titleWidth와 windowWidth라는 두 속성을 기반으로 비교해서 slidable의 여부를 설정합니다.

최종 애니메이션 적용

const marquee = (width: number) => keyframes`
0% {
  transform: translate(${width}px, 0);
}
100% {
  transform: translate(-${width}px, 0);
}
`;

...

<Title ref={titleWidthRef} slide={slidable} textWidth={titleWidth}>
  현재&nbsp;
  <TitleLocation
    populationLevel={firstLevelInfo[1].populationLevel}>
    {firstLevelInfo[0]}
  </TitleLocation>
  {INFO_DETAIL_TITLE[firstLevelInfo[1].populationLevel]}
</Title>
{slidable ? (
  <Title
    ref={titleWidthRef}
    slide={slidable}
    textWidth={titleWidth}>
    현재&nbsp;
    <TitleLocation
      populationLevel={firstLevelInfo[1].populationLevel}>
      {firstLevelInfo[0]}
    </TitleLocation>
    {INFO_DETAIL_TITLE[firstLevelInfo[1].populationLevel]}
  </Title>
) : (
  <></>
)}

...

export const Title = styled.h1<TitleTypes>`
  display: block;
  text-align: center;
  font-size: 1rem;
  white-space: nowrap;
  will-change: transform;
  animation: ${props =>
    props.slide
      ? css`
          ${marquee(props.textWidth)} 10s linear infinite
        `
      : ''};
`;
  • 가장 복잡한 ‘최종 애니메이션 적용’ 부분입니다. 적용 여부에 따라 Title의 개수를 다르게 둡니다.
  • 두 개의 Title 컴포넌트는 현재 gap이 50%인 flexbox에 감싸여져 있습니다.
  • 애니메이션의 @keyframes의 처음과 끝은 해당 텍스트의 크기를 기반으로 translate 값이 설정됩니다.
  • 결과적으로 처음과 끝이 서로 맞물리는 듯한 애니메이션을 구현할 수 있기 때문에 자연스러운 애니메이션이 만들어질 수 있습니다.

스크린샷 2022-12-11 오후 8.13.02.png

  • 결과적으로 다음과 같이 Title과 Gap을 구성할 수 있습니다.

결과

최종적으로 다음과 같은 화면을 구현할 수 있게 됩니다.

ezgif.com-gif-maker (1).gif

⚠️ **GitHub.com Fallback** ⚠️