framer-motion 라이브러리는 대표적인 애니메이션 라이브러리입니다.
이 라이브러리를 통해 페이지 전환 효과를 만들수도 있습니다.
하지만 단점은 뒤로가기 ( 예를 들어 useNavigate(-1) ) 를 통해 이전 페이지로 이동했을 때 재대로 작동이 되지 않는 경우가 있었습니다.
이를 개선하기 위해 react-transition-group 라이브러리를 사용해 보겠습니다.
react-transition-group 은 DOM 이 나타나거나 없어질 때 상태 변화를 관리하는 것을 통해서 React 애플리케이션에서 내이메이션 전환 효과를 다루는 것을 도와줍니다.
CSSTransition
className 기반으로 자식 컴포넌트에 애니메이션을 적용해 줍니다.
TransitionGroup
여러 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
에 애니메이션을 정의하는 것을 통해 페이지 트랜지션 애니메이션을 구현할 수 있다.
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를 명시하는 것이 좋습니다.
이렇게 설정을 하면 뒤로가기 버튼을 눌렀을 때 똑같은 애니메이션이 적용이 됩니다.
즉 뒤로 가기 버튼을 눌렀을 때 처음에 왼쪽에서 오른쪽으로 이동했다면 다시 오른쪽에서 왼쪽으로 이동해야 합니다.
뒤로가기 애니메이션을 위해서는 유저가 현재 페이지에 어떻게 도달했는지에 따라 서로 다른 애니메이션을 적용해야 합니다.
react-router-dom이 제공하는 useNavigationType 훅은 현재 페이지에 어떻게 도달했는지에 대한 상태 정보를 반환합니다.
NavigationType = "POP" | "PUSH" | "REPLACE";
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;
}
여기까지 설정하면 재대로 작동되는 모습을 볼 수 있습니다.