Router 페이지 애니메이션(react-transition-group)

박찬영·2024년 3월 25일
0

사용 이유

framer-motion 라이브러리는 대표적인 애니메이션 라이브러리입니다.
이 라이브러리를 통해 페이지 전환 효과를 만들수도 있습니다.
하지만 단점은 뒤로가기 ( 예를 들어 useNavigate(-1) ) 를 통해 이전 페이지로 이동했을 때 재대로 작동이 되지 않는 경우가 있었습니다.

이를 개선하기 위해 react-transition-group 라이브러리를 사용해 보겠습니다.

react-transition-group

react-transition-group 은 DOM 이 나타나거나 없어질 때 상태 변화를 관리하는 것을 통해서 React 애플리케이션에서 내이메이션 전환 효과를 다루는 것을 도와줍니다.

CSSTransition
className 기반으로 자식 컴포넌트에 애니메이션을 적용해 줍니다.
TransitionGroup
여러 CSSTransition 컴포넌트를 함께 그룹화하여 관리하는 역할을 합니다.

🐧 CSSTransition

React Transition Group 라이브러리에서 제공하는 컴포넌트 중 하나로, CSS를 사용하여 애니메이션을 적용하는 데에 사용됩니다. 주로 UI 요소의 등장, 사라짐, 이동 등의 변화에 애니메이션을 적용할 때 유용하게 활용됩니다.

// 공식 예제
function App() {
  const [inProp, setInProp] = useState(false);
  const nodeRef = useRef(null);
  return (
    <div>
      <CSSTransition nodeRef={nodeRef} 
                     in={inProp} 
                     timeout={200} 
                     classNames="my-node">
        <div ref={nodeRef}>
          CSSTransition 테스트중 입니다.
        </div>
      </CSSTransition>
      <button type="button" onClick={() => setInProp(true)}>
        Click to Enter
      </button>
    </div>
  );
}

nodeRef 속성은 해당 애니메이션을 적용할 DOM 요소에 대한 참조를 전달합니다.
in 속성은 애니메이션을 시작할지 여부를 결정하며,
timeout 속성은 애니메이션 지속 시간을 설정합니다.
classNames 속성은 CSS 클래스 이름을 지정하며,
이를 통해 애니메이션 효과를 정의하는 CSS 클래스를 적용합니다.

위와 같이 div를 자식으로 갖는CSSTransition은 다음과 같이 작동을 합니다.

  • inProp이 true가 될 때
    🍎 이 때 my-node 라고 정의한 클래스를 참고하여 my-node-enter className을 붙여준다.
    🍎 timeout에 명시된 시간동안 my-node-enter-active className을 붙여준다.
    🍎 timeout에 명시된 시간이 모두 지나면 *-active className을 제거하고, my-node-enter-done className을 붙여준다.

  • inProp이 false가 될 때
    🍎 my-node-enter-done className을 제거하고, my-node-exit을 붙여준다.
    🍎 timeout에 명시된 시간 동안 my-node-exit-active className을 붙여준다.
    🍎 timeout에 명시된 시간이 모두 지나면 div는 언마운트된다.
    🍎 즉 css에서 *-active에 애니메이션을 정의하는 것을 통해 페이지 트랜지션 애니메이션을 구현할 수 있다.

    🐧 TransitionGroup

    CSSTransition 컴포넌트를 함께 그룹화하여 관리하는 역할을 합니다.
    마운트 상태에 따른 inProp을 자동으로 전달해 줍니다.

import { TransitionGroup, CSSTransition } from "react-transition-group";

function App() {
  const location = useLocation();

  return (
    <TransitionGroup>
      <CSSTransition 
        key={location.pathname} 
        timeout={5000}
        className="page-transition"
        > 
        <Routes location={location}>
          <Route path="/A" element={<A />} />
          <Route path="/B" element={<B />} />
          <Route path="/C" element={<C />} />
        </Routes>
      </CSSTransition>
    </TransitionGroup>
  );
}

export default App;
.page {
  position: absolute; /* 필수 */
  width: 100vw;
  height: 100vh;
}

.page-transition-enter {
  z-index: 1;
  transform: translateX(100%);
}
.page-transition-enter-active {
  transform: translateX(0);
  transition: transform 300ms;
}
.page-transition-exit {
  z-index: 0;
  transform: translateX(0);
}
.page-transition-exit-active {
  transform: translateX(-100%);
  transition: transform 300ms;
}
  • 렌더링 될 요소의 root에 position: absolute 명시하기
    앞선 게시글에서 설명했 듯, 한 화면에 두 요소가 동시에 렌더링 되는 것이므로 이 두 요소가 겹쳐서 나오게 하려면 포지션을 고정해야 합니다.

  • overflow로 인한 스크롤 생성
    마찬가지로 한 화면에 두 요소가 동시에 렌더링 됨으로 인해, 뷰포트를 넘어서도록 요소가 렌더링이 될 수 있다.
    이로 인해 생기는 스크롤을 필요하다면 적절히 처리해주어야 합니다.

  • z-index 명시하기
    애니메이션 의도에 따라 다르겠지만, 새로 마운트 되는 페이지는 일반적으로 언마운트 될 페이지 보다 앞에 표시되어야 하므로 z-index를 명시하는 것이 좋습니다.

이렇게 설정을 하면 뒤로가기 버튼을 눌렀을 때 똑같은 애니메이션이 적용이 됩니다.
즉 뒤로 가기 버튼을 눌렀을 때 처음에 왼쪽에서 오른쪽으로 이동했다면 다시 오른쪽에서 왼쪽으로 이동해야 합니다.

🐧 뒤로가기 애니메이션 적용하기

뒤로가기 애니메이션을 위해서는 유저가 현재 페이지에 어떻게 도달했는지에 따라 서로 다른 애니메이션을 적용해야 합니다.

useNavigationType

react-router-dom이 제공하는 useNavigationType 훅은 현재 페이지에 어떻게 도달했는지에 대한 상태 정보를 반환합니다.

NavigationType = "POP" | "PUSH" | "REPLACE";

  • PUSH
    history에 새 스택이 쌓일 경우
  • POP
    history 스택을 변경한 경우 (뒤로가기, 앞으로가기)
  • REPLACE
    history의 현재 값을 변경한 경우
    즉 이 방법으로는 '뒤로가기', '앞으로가기'를 동일하게 인식한다.

childFactory

TransitionGroup 컴포넌트가 제공하는 함수로, React의 cloneElement와 같이 자식 요소를 업데이트 하는데 사용됩니다.
childFactory의 특장점은 바로 언마운트 될 요소에도 접근하여 반응형 업데이트를 할 수 있다는 것입니다.

// ...
import { useLocation, useNavigationType } from "react-route-dom"
import { TransitionGroup, CSSTransition } from "react-transition-group";

function App() {
  const location = useLocation();
  const navigationType = useNavigationType()

  return (
    <TransitionGroup
      childFactory={(child) =>
          React.cloneElement(child, {
            className:
              navigationType === "POP"
                ? "page-transition--pop"
                : "page-transition--push",
          })
        }>
      <CSSTransition 
        key={location.pathname} 
        timeout={300}
        > 
        <Routes location={location}>
          <Route path="/A" element={<A />} />
          <Route path="/B" element={<B />} />
          <Route path="/C" element={<C />} />
        </Routes>
      </CSSTransition>
    </TransitionGroup>
  );
}

export default App;
.page-transition--push-enter {
  z-index: 1;
  transform: translateX(100%);
}
.page-transition--push-enter-active {
  transform: translateX(0);
  transition: transform 300ms;
}
.page-transition--push-exit {
  z-index: 0;
  transform: translateX(0);
}
.page-transition--push-exit-active {
  transform: translateX(-100%);
  transition: transform 300ms;
}

.page-transition--pop-enter {
  z-index: 1;
  transform: translateX(-100%);
}
.page-transition--pop-enter-active {
  transform: translateX(0);
  transition: transform 300ms;
}
.page-transition--pop-exit {
  transform: translateX(0);
}
.page-transition--pop-exit-active {
  transform: translateX(100%);
  transition: transform 300ms;
}

여기까지 설정하면 재대로 작동되는 모습을 볼 수 있습니다.

profile
오류는 도전, 코드는 예술

0개의 댓글

관련 채용 정보