홈으로

[React] 페이지 전환 애니메이션 구현하기

2020년 03월 26일

개요

웹사이트에서 라우팅을 구현할 때 페이지 전환효과를 주고싶은 경우가 있다. React-Router로 라우팅을 구현하고 있다면 어떻게 해당 효과를 줄 수 있을까? React-Router는 현재 경로가 바뀌게 되면 즉시 DOM에서 제거하기 때문에 history가 바뀌기 전에 애니메이션을 실행시켜야 한다. 이 말은, 제거될 컴포넌트를 기억하고 애니메이션을 실행시킨 뒤에 애니메이션이 끝나면 바뀐 history의 컴포넌트에 애니메이션을 적용한다는 뜻이다. 생각 해보면, 직접 구현하게 될 경우 구현이 까다롭고 비효율적인 부분이 많아진다는 것을 알 수 있다. 따라서, 공식적인 솔루션은 react-transition-group 을 사용할 것을 권장한다. 결과는 아래화면과 같고 이제부터 그 방법을 알아보도록 하자.

react-transition-group

먼저 사용할 패키지의 사용법에 대해 알아야 한다.

  • <TransitionGroup>

    • 자식 컴포넌트의 마운트/언마운트를 제어하는 컴포넌트로, 자식으로 <Transition><CSSTransition> 컴포넌트를 가질 수 있다. 실제 애니메이션 효과를 정의하진 않지만 자식 컴포넌트들의 애니메이션 동작시점을 제어한다.
  • <CSSTransition>

    • CSS의 transition/animation을 적용할 때 사용하는 컴포넌트로 appear , enter , exit 을 접미사로 가진 클래스 이름을 통해 동작한다.
    • 이 컴포넌트의 classNames props로 fade 라고 했으면 CSS로 .fade-appear , .fade-enter , .fade-exit 과 같이 스타일을 정의하는 방식이다.
  • 시점

    • appear는 페이지를 로드했을 때
    • enter는 컴포넌트를 마운트 했을 때
    • exit은 컴포넌트를 언마운트 했을 때
    • 각각의 시점에 접미사로 -active-done 이 붙는데, -active 는 시점이 활성화되고 있을 때이고 -done 은 해당 시점이 끝났을 때이다. 접미사가 안 붙는 경우는 활성화 되기 직전이다.

프로젝트 초기화

CRA를 통해서 프로젝트를 생성한 뒤에 필요한 패키지들을 설치해주자.

npx create-react-app react-transition-example
cd react-transition-example
yarn add react-router-dom react-transition-group

내비게이션 컴포넌트 생성

링크를 누르면 라우팅 전환이 일어날 수 있도록 내비게이션 컴포넌트를 생성하자.

import React from "react";
import { Link } from "react-router-dom";
import "./Nav.css";

const Nav = () => {
  return (
    <ul>
      <li>
        <Link to="/">아이린</Link>
      </li>
      <li>
        <Link to="/seulgi">슬기</Link>
      </li>
      <li>
        <Link to="/yeri">예리</Link>
      </li>
      <li>
        <Link to="/joy">조이</Link>
      </li>
      <li>
        <Link to="/wendy">웬디</Link>
      </li>
    </ul>
  );
};

export default Nav;
ul {
  margin: 0;
  display: flex;
  justify-content: center;
  margin-bottom: 2rem;
}

a {
  color: black;
  text-decoration: none;
}

li {
  list-style-type: none;
  margin-right: 1rem;
  font-size: 2rem;
  font-weight: bold;
}

페이지 컴포넌트 생성

레드벨벳 멤버들의 이미지를 props로 받아서 컴포넌트를 리턴하는 페이지 컴포넌트를 만들자.

Page.jsx

import React from "react";
import "./Page.css";

const Page = ({ image }) => {
  return (
    <div className="container">
      <img src={image} alt="멤버 사진" />
    </div>
  );
};

export default Page;

Page.css

.container {
  position: absolute;
  left: 50%;
  top: 0;
  width: 1000px;
  transform: translateX(-50%);
}

img {
  width: 100%;
  vertical-align: middle;
}

트랜지션 컴포넌트 생성

이제 트랜지션 효과를 적용시킬 컴포넌트를 만들텐데 주의깊게 봐야 한다.

Transition.jsx

import React from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import { Route, Switch, useLocation } from "react-router-dom";
import Page from "./Page";
import "./Transition.css";

const irene =
  "https://raw.githubusercontent.com/baeharam/Redvelvet-Fansite/master/images/about-irene.jpg";
const seulgi =
  "https://raw.githubusercontent.com/baeharam/Redvelvet-Fansite/master/images/about-seulgi.jpg";
const yeri =
  "https://raw.githubusercontent.com/baeharam/Redvelvet-Fansite/master/images/about-yeri.jpg";
const joy =
  "https://raw.githubusercontent.com/baeharam/Redvelvet-Fansite/master/images/about-joy.jpg";
const wendy =
  "https://raw.githubusercontent.com/baeharam/Redvelvet-Fansite/master/images/about-wendy.jpg";

const Transition = () => {
  const PageIrene = <Page image={irene} />;
  const PageSeulgi = <Page image={seulgi} />;
  const PageYeri = <Page image={yeri} />;
  const PageJoy = <Page image={joy} />;
  const PageWendy = <Page image={wendy} />;

  const location = useLocation();

  return (
    <TransitionGroup className="transition-group">
      <CSSTransition key={location.pathname} classNames="fade" timeout={500}>
        <Switch location={location}>
          <Route exact path="/" children={PageIrene} />
          <Route path="/seulgi" children={PageSeulgi} />
          <Route path="/yeri" children={PageYeri} />
          <Route path="/joy" children={PageJoy} />
          <Route path="/wendy" children={PageWendy} />
        </Switch>
      </CSSTransition>
    </TransitionGroup>
  );
};

export default Transition;

먼저 각 이미지로 페이지 컴포넌트를 생성하는 것까지는 이해가 될 것이니 그 이후부터 보자.

  • 각 페이지의 마운트/언마운트에 따른 애니메이션을 제어하기 위해 <TransitionGroup> 으로 묶는다.
  • 애니메이션 효과를 정의하기 위해 <CSSTransition> 으로 묶고, 클래스의 접두사를 fade 로 설정한다. 또한 페이지가 바뀔 때마다 애니메이션을 동작할 것이므로 key 값으로 location.pathname 을 주고 0.5초의 딜레이가 발생하도록 해준다.
  • 이제 각 라우트들을 <Switch> 로 감싸서 한 페이지만 렌더링되게 하는데 여기서 location 을 넘겨준다. 넘겨주는 이유는 언마운트 되는 이전의 페이지 애니메이션을 동작시키기 위함이다.
  • 마지막으로 각각의 라우트를 설정한다.

Transition.css

body {
  overflow: hidden;
}

.transition-group {
  position: relative;
}

.fade-enter {
  opacity: 0;
  transform: translateX(100%) rotateZ(45deg);
}

.fade-enter-active {
  opacity: 1;
  transform: translateX(-50%) rotateZ(0);
  transition: all 1s ease-in;
}

.fade-exit {
  opacity: 1;
  transform: translateX(-50%) rotateZ(0);
}

.fade-exit-active {
  opacity: 0;
  transform: translateX(-100%) rotateZ(-45deg);
  transition: all 1s ease-in;
}
  • <CSSTransition>classNames 로 넘긴 fade 를 기준으로 enterexit 접미사를 붙였고 각 시점에 따라 active 를 붙였다. 이걸 통해서 다음 시점의 효과들을 구현할 수 있다.

    • fade-enter : 마운트 되기 직전
    • fade-enter-active : 마운트 활성화 될 때
    • fade-exit : 언마운트 되기 직전
    • fade-exit-active : 언마운트 활성화 될 때
  • .transition-groupposition:relative 를 주었는데, 각 페이지 컴포넌트가 겹쳐지지 않으면 층처럼 쌓이는 효과가 일어나기 때문에 각 페이지에 position:absolute 를 주기 위해 설정해놓은 것이다. 위에서 본 Page.css 코드의 .container 스타일에서 position:absolute 를 제거하면 왜 이런 스타일을 정의하는지 이해할 수 있다.
  • 전환효과로 transform 을 사용하기 때문에 스크롤바가 생기는 경우가 있기 때문에 bodyoverflow:hidden 을 주어 없앴다.

라우팅 설정

라우팅이 동작할 수 있도록 설정해주자.

App.js

import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import Nav from './components/Nav';
import Transition from './components/Transition';

function App() {
  return (
    <Router>
      <Nav />
      <Transition />
    </Router>
  );
}

export default App;

메뉴를 누르면 전환이 일어나는 것을 볼 수 있다. 여기선 enteractive 만 적용했지만 <TransitionGroup> 의 props로 appear 를 주면 CSS에서 appear 스타일 또한 적용할 수 있다.

마무리

지금까지 라우팅할 때 전환 효과를 어떻게 구현하는지에 대해 알아보았다. react-transition-group을 사용하면 편하게 그 효과를 구현할 수 있으니 이 작업이 필요할 때 사용하도록 하자. 이 포스팅의 모든 코드는 깃헙 에서 확인할 수 있다.

참고

Loading script...