본문 바로가기
  • 프론트엔드 개발자 세오세오 | Frontend dev Seo
Learn to Code

[프로젝트] React component: 마운트(mount)될 때와 언마운트(unmount)될 때 각기 다른 애니메이션 적용하기 (feat. onanimationend 프로퍼티)

by CEOSEO 2021. 9. 1.
728x90
반응형

Tablet 사이즈 이하인 경우 사용할 수 있는 메뉴

 

멋진 웹사이트들을 보면 메뉴 버튼을 클릭했을 때 촤라락~🌼하면서 나왔다가 촤라락~🌼하면서 사라지는 경우를 자주 볼 수 있다. 내 프로젝트에도 메뉴가 토글될 때 그러한 애니메이션을 적용하고 싶어서, 가장 처음 시도했던 건 아래와 같이 slide-in 애니메이션을 만들어서 토글되는 메뉴에 적용시키는 것이었다.

 

 

// Styled-Components의 keyframes이다.
// 토글되는 메뉴의 width 길이 만큼 오른쪽에 숨겨놨다가 나오도록 했다

const slideIn = keyframes`
  from {
    right: -500px;
  }
  to {
    right: 0;
  }
`;

 

 

 

 

 

그런데, 이렇게 단순히 slideIn 애니메이션만을 추가하면, 한가지 문제가 발생한다. 바로, 메뉴를 토글해서 메뉴 컴포넌트가 마운트될 때엔 올바르게 slide-in하지만, 메뉴를 닫았을 땐 (=메뉴 컴포넌트가 언마운트될 때엔) 그 어떠한 애니메이션도 없이 하고 사라지기 때문이다. 내가 원하는 최선의 상황은 메뉴가 나타날 때 slide-in을 하고, 사라질 땐 slide-in 하면서 왔던 길을 고스란히 다시 나가는 것이었다.

 

그래서 검색을 해봤더니 역시나, 해외의 여러 선배님들께서 나와 동일한 상황에 사용할 수 있는 방법들을 친절히 작성하여 포스트해주셨고, 나는 그 글들을 읽어보며 내 프로젝트에 알맞게 적용시킬 수 있도록 코드를 작성하면 되었다.

 

 

 

Identifying the problem

검색을 할 때, 현재 내가 해결하고 싶은 문제가 정확히 무엇인지부터 인지한 상태여야 원하는 정보를 담은 글들을 찾을 수 있다. menu toggle animation이나 slide-in, fade-in animation같은 키워드로 검색하면 내가 직면한 상태와의 연관도가 많이 떨어진 검색 결과들밖에 나오지 않는다. 

 

결국, 내가 처한 정확한 상황을 나타내주는 직접적인 키워드를 사용해야만 원하는 결과를 찾을 수 있게 된다는 것이다. 나의 경우엔, 그 매직 키워드가 바로 how to add animation to unmounting component in React 였다. 위 코드와 같이 slideIn 애니메이션 하나만 더하면, 마운트할 때만 적용이 되는 것이고 언마운트될 때엔 (1) slide-out과 같은 별도의 애니메이션을 추가하지 않았고 (2) 이미 slideIn 애니메이션이 적용되어 있기 때문에 그 이상의 어떠한 일도 발생하지 않기 때문이다.

 

 

Solution

그렇게 찾은 여러 글들을 읽어보고, 직접 적용도 해보면서 이와 같은 문제를 해결하기 위해선 우선 다음과 같은 작업이 필요하다는 것을 알게 되었다:

 

1. 메뉴를 토글하는 상태가 필요하다.

2. 메뉴 토글 상태에 따라서, 메뉴에 적용되는 애니메이션을 바꿔주는 또 다른 상태가 필요하다.

 

아무 애니메이션도 적용되지 않은 메뉴라면, 1번에 해당하는 상태 하나만으로도 충분하다. 메뉴를 불러오는 버튼에다가 onClick 이벤트를 걸어주고, 콜백함수에서 그 상태를 true/false로 바꿔서 랜더링 될지 안될지를 지정해주기만하면 되기 때문이다.

 

반면에, 애니메이션을 추가하려면 - 그것도 마운트될 때와 언마운트될 때 각기 다른 애니메이션을 - 메뉴가 나오지 않은 상태일 때 적용하는 애니메이션메뉴가 나와있는 상태일 때 적용하는 애니메이션이 달라야한다. 전자일 경우엔 slide-in이 될 것이고, 후자일 경우가 slide-out이 된다.

 

 

 

Code

위 두가지 상태는 모두 메뉴의 상위 컴포넌트에 만든다. 사각사각 프로젝트 같은 경우 아래와 같이 menuOn과 aniMode 두 가지 다른 상태를 만들었고, 토글되는 메뉴인 MobileMainNav에 props로 전달했다.

 

해더에서 메뉴를 토글하는 버튼을 눌렀을 땐, menuOn과 aniMode 둘 다 true가 된다. menuOn이 true가 되는 순간, 해더가 리렌더링 되면서 자식 컴포넌트인 MobileMainNav가 가시성을 얻게 된다. 

 

export default function Header () {
  const [menuOn, setMenuOn] = useState(false);
  const [aniMode, setAniMode] = useState(false);
      
  ...
  
  const toggleMenus = () => {
    setMenuOn(!menuOn);
    setAniMode(true);
  };
      
  render (
    ...
    <button onClick={toggleMenus}>메뉴</button>
    ...
    {menuOn && (
     <MobileMainNav aniMode={aniMode} setAniMode={setAniMode} />
    )}
    ...
  )
}

 

 

그러면 이제 MobileMainNav가 화면에 나타나게 되는데, 이때 useEffect를 사용해서 마운트 직전에 자식 컴포넌트인 메뉴 안에 또 다른 상태 shouldRender를 aniMode에 맞춰 변경해준다. 그리고, 이 shouldRender가 true인 경우에만 메뉴가 랜더링되도록 코드를 작성한다.

 

export default function MobileMainNav ( {aniMode, setAniMode} ) {
  const [shouldRender, setShouldRender] = useState(aniMode);
  
  useEffect(() => {
    if (aniMode) setShouldRender(true);
  }, [aniMode]);
  
  return (
    shouldRender && (
      ...
      <MobileNav> ...
    )
  )
}

 

 

그 다음으로 해주어야하는 작업은, aniMode에 따라 다른 애니메이션이 적용되도록 해주는 것이다. 그런 다음엔 onAnimationEnd를 메뉴에 걸어서 shouldRender의 상태를 바꾸어 주어야 한다. 이는 다른 말로 하면, 원래 자식 컴포넌트인 메뉴는 부모 컴포넌트의 상태가 on/off냐에 따라서 랜더링이 되거나 되지 않거나 상태가 바뀌어야 하지만, 자식 컴포넌트인 메뉴 내부에서 shouldRender라는 변수를 또 다시 만듬으로써 이 shouldRender로 메뉴의 마운트 여부를 결정하게 된다는 것을 뜻한다.

 

메뉴 닫기 버튼이 클릭된다

-> aniMode가 false로 바뀐다

-> 적용된 애니메이션이 바뀐다

-> onAnimationEnd를 통해 shouldRender가 false로 바뀐다

-> 메뉴 컴포넌트가 언마운트 된다

 

 

코드는 아래와 같이 작성할 수 있다:

 

export default function MobileMainNav ( {aniMode, setAniMode} ) {
  const [shouldRender, setShouldRender] = useState(aniMode);
  
  const closeMenu = useCallback(() => {
    setAniMode(false);
  }, [setAniMode]);

  const onAnimationEnd = () => {
    if (!aniMode) {
      setShouldRender(false);
    }
  };
  
  useEffect(() => {
    if (aniMode) setShouldRender(true);
  }, [aniMode]);
  
  return (
    shouldRender && (
      ...
      <MobileNav aniMode={aniMode} onAnimationEnd={onAnimationEnd}>
      ...
      </MobileNav>
    )
  )
}

 

 

<MobileNav>는 Styled-Components에서 사용한 변수명이다. 여기에 aniMode에 따라 적용한 애니메이션은 아래와 같다.

 

const slideIn = keyframes`
  from {
    right: -500px;
  }
  to {
    right: 0;
  }
`;

const slideOut = keyframes`
  from {
    right: 0;
  }
  to {
    right: -500px;
  }
`;

const MobileNav = styled.nav`
  animation: ${(props) => props.aniMode ?
    css`${slideIn} ease-in-out 0.3s`:
    css`${slideOut} ease-in-out 0.3s`};
`

 

 

 

728x90
반응형

댓글