웹사이트를 SPA로 만들 때의 장점 중 하나는 자바스크립트의 실행 컨텍스트가 날아가지 않기 때문에 화면이 전환될 때에도 애니메이션 처리를 할 수 있다는 점입니다.
GatsbyJS
GatsbyJS는 리액트 정적 웹사이트 생성기입니다.
정적파일로 떨궈주기 때문에 S3에 올리기만 하면 웹사이트 배포가 끝난다는 장점이 있고,
여러가지 기능들을 마법같이 붙여주는 플러그인 시스템이 잘 갖춰져 있어서 저희 웹사이트 개발에 많이 사용됐죠.
그리고, 대부분의 SPA 접근처럼 GatsbyJS도 History API를 활용하여 페이지 히스토리를 처리하기 때문에 자바스크립트 실행 컨텍스트가 유지되어 페이지 전환 애니메이션 처리가 가능합니다.
Next.js 등의 프레임웍을 활용해도 같은 처리가 가능하지만, 페이지 전환 애니메이션은 프레임웍마다 작성해야 하는 코드가 조금씩 다르므로 이 글에서는 GatsbyJS 기준으로 이야기 하겠습니다.
페이지 준비
우 선 src/pages
폴더에 페이지 두 개를 만듭니다.
(page-a.js
, page-b.js
내용은 적당히 바꿔서 만듭니다.)
import React from 'react';
import { Link } from 'gatsby';
export default () => <>
<p>A 페이지 입니다.</p>
<Link to='/page-b'>B 페이지 가기</Link>
</>;
페이지 전환 애니메이션을 구현하려면 <a>
태그 대신 Link 컴포넌트를 사용해야합니다.
Link 컴포넌트는 내부적으로 <a>
태그를 감싸서,
클릭 시 브라우저 기본동작을 건너뛰고 History API를 사용하는 방식으로 동작합니다.
애니메이션 처리를 따로 하지 않더라도 페이지 전환 속도 등 이점이 있기 때문에, 웹사이트 내부 페이지 링크는 전부 Link 컴포넌트를 사용하시면 좋습니다.
레이아웃 준비
GatsbyJS v2에서는 레이아웃 구성을 페이지 컴포넌트 안에서 Layout 엘리먼트로 감싸는 방식을 권장하지만, 레이아웃을 이렇게 구성하면 페이지가 전환될때 페이지 인스턴스와 함께 레이아웃 인스턴스가 언마운트돼서 애니메이션을 유지할 수가 없습니다.
페이지 전환 애니메이션 처리를 위한 레이아웃 컴포넌트의 인스턴스는 페이지 전환간에 언마운트되지 않는 단계에 있어야 하는데,
GatsbyJS에서는 이 단계에 개입할 수 있는 wrapPageElement
api를 제공합니다.
이 API를 직접 사용할 수도 있지만 같은 코드를 gatsby-browser.js
에서 한 번, gatsby-ssr.js
에서 한 번씩 적어야 해서 약간 번거롭습니다.
gatsby-plugin-layout 플러그인을 사용하면 그 처리를 조금 더 간편하게 할 수 있습니다.
다음과 같이 플러그인을 설정하고 Layout 컴포넌트를 작성해줍니다.
gatsby-config.js
module.exports = {
plugins: [
{
resolve: 'gatsby-plugin-layout',
options: {
component: require.resolve('./src/components/Layout.js'),
},
},
],
};
src/components/Layout.js
import React from 'react';
// `page-a.js`, `page-b.js`에서 반환한 내용이
// <Layout> 컴포넌트에 `children` prop으로 들어옵니다.
// 이 레이아웃 컴포넌트는 페이지가 전환돼도 마운트된 상태가 유지됩니다.
export default ({ children }) => <>
<div className='page'>
{children}
</div>
</>;
애니메이션을 넣어봅시다
애니메이션은 Layout 컴포넌트에서 구현합니다.
페이지 전환 애니메이션을 구현하는 것은 기본적으로 항목 개수가 변화하는 목록의 애니메이션을 구현하는 것과 같습니다.
리액트 커뮤니티에서는 이러한 전환 애니메이션을 구현하기 위해 보통 react-transition-group
라이브러리를 사용합니다.
react-transition-group
라이브러리를 사용해서 전환 애니메이션을 구현하려면
여기서 제공하는 TransitionGroup 컴포넌트의 엘리먼트 자식으로 Transition 엘리먼트들을 넣으면 됩니다.
TransitionGroup 컴포넌트는 엘리먼트 자식으로 넘겨준 Transition 엘리먼트가 갑자기 자식 목록에서 빠지더라도 애니메이션이 종료되기 전까지 Transition 인스턴스의 마운트 상태를 유지시켜줍니다.
Transition 엘리먼트는 전환 상태를 인자로 받는 렌더 함수를 자식으로 받습니다.
src/components/Layout.js
import React from 'react';
import { Transition, TransitionGroup } from 'react-transition-group';
import './Layout.css';
// `location` prop으로는 현재 브라우저의 주소 상태를 알 수 있는 객체가 들어옵니다.
// gatsby-plugin-layout 플러그인이 주입해준 값입니다.
export default ({ children, location }) => <>
<TransitionGroup
// `component` prop에 `null`을 넘겨주지 않으면 `<div>`로 한단계 감싸집니다.
component={null}>
<Transition
// 현재 pathname을 key prop으로 넘겨주어
// 페이지가 전환될 때 이전 페이지가 TransitionGroup에서 빠지고
// 새 페이지가 들어온 것으로 간주되도록 합니다.
key={location.pathname}
// timeout prop으로 넘겨준 시간(ms 단위)에 따라 `status`가 변화합니다.
timeout={{ enter: 300, exit: 300 }}>
{ status => (
// 전환 상태(`status` 인자)는
// `unmounted`, `exited`, `entering`, `entered`, `exiting`
// 이렇게 들어오는데, Transition 엘리먼트가 아예 자식 목록에서 빠져버리면
// `unmounted` 상태가 들어올 일이 없습니다.
// 따라서 뒤쪽의 네가지 상태만 들어온다고 생각하면 됩니다.
<div className={`page ${status}`}>
{children}
</div>
) }
</Transition>
</TransitionGroup>
</>;
src/components/Layout.css
.page {
top: 0;
transition: opacity 0.3s;
}
.page.entering,
.page.entered {
position: relative;
opacity: 1;
}
.page.exiting,
.page.exited {
position: absolute;
opacity: 0;
}
짜잔~
완성된 코드는 CodeSandbox에 올려두었습니다: https://codesandbox.io/s/n08qvvxpj0