## 작동 순서
1. 초기 렌더링
- 앱이 시작될 때 HashRouter -> PageTransition -> Routes -> Home 순으로 컴포넌트가 마운트 된다.
- TransitionContainer ( == PageTransition 내부의 styled.div ) 는 마운트 되면서 animation : slideIn 0.5s가 자동으로 실행된다.
- 화면에 Home 페이지가 오른쪽 바깥에서 왼쪽으로 슬라이드 인 되면서 나타남
2. 페이지 이동 ( 언마운트 트리거 )
- 사용자가 #/today 링크를 클릭하면 useLocation()이 새 경로를 감지한다.
- location.pathname 이 바뀌면 React는 key={location.pathname} 이 달린 TransitionContainer를
1) 기존 엘리먼트 언마운트 ( 제거 )
2) 새 엘리먼트 마운트 ( 생성 ) 순으로 처리하려고 준비한다.
3. 퇴장 애니메이션 실행
- 우리의 useEffect가 location.pathname 변화를 감지해서
setIsExiting(true);
setTimeout(() => setIsExiting(false), 500);
이렇게 isExiting을 true로 바꾸고
- className="slide-out"이 붙으면 css slideout 키 프레임이 0.5초동안 실행되면서 페이지가 왼쪽으로 빠져나가듯 사라진다.
4. 실제 언마운트 & 새 마운트
- 0.5 초 뒤 ( setTime 후 ) isExiting = false로 리셋되고, 리액트는
ㄴ 기존 TransitionContainer를 실제로 DOM에서 제거하고 ( 언마운트 )
ㄴ key가 바뀐 새로운 TransitionContainer를 다시 생성한다. ( 마운트 )
- 새로 마운트 될 때는 className에 slide-out이 없으니, 기본 animation : slideIn이 다시 붙어 오른쪽에서 슬라이드 인
5. 새 페이지 완전 노출
- 슬라이드 인 애니메이션 0.5초이 끝나면 화면에 최종적으로 Today 컴포넌트가 고정된다.
## 애니메이션 시각화
[ Home 화면 노출 ] --(링크 클릭)-->
[ Home 화면 slide-out ] --(0.5s)-->
[ Home 언마운트 & Today 마운트 ] --(자동 slide-in)-->
[ Today 화면 노출 ]
왜 이렇게 짰냐면?
- key 프로퍼티
- React에서 같은 타입 컴포넌트라도 key가 바뀌면 “새 컴포넌트”로 취급해서 언마운트→마운트 과정을 거쳐.
- useEffect + 상태(isExiting)
- 경로가 바뀌는 순간 퇴장 애니메이션을 주기 위해, 상태를 토글해서 .slide-out 클래스를 붙였다 뗐다 함.
- CSS 애니메이션
- styled.div에 @keyframes 두 개를 정의해 두고, 클래스 토글만으로 입·퇴장 모두 처리.
이 구조 덕분에 각 페이지 컴포넌트는 전환 로직 신경 안 쓰고, PageTransition만 감싸주면 알아서 슬라이드 효과가 적용돼
// ▶ 리액트, 훅, 스타일러 불러오기
import React, { useEffect, useState } from "react"; // React 및 훅
import { useLocation } from "react-router"; // 현재 경로 얻는 훅
import styled from "@emotion/styled"; // emotion styled
// ▶ Props 타입 정의
interface PageTransitionProps {
children: React.ReactNode; // 감싸질 페이지 컴포넌트들
}
// ▶ PageTransition 컴포넌트
const PageTransition: React.FC<PageTransitionProps> = ({ children }) => {
const location = useLocation(); // ① 현재 URL 경로 가져오기
const [isExiting, setIsExiting] = useState(false); // ② 퇴장 애니메이션 상태
useEffect(() => {
// 경로가 바뀌면 퇴장 애니메이션 트리거
setIsExiting(true);
const timeout = setTimeout(() => {
// 애니메이션 끝나면 진입 상태로 리셋
setIsExiting(false);
}, 500); // 애니메이션 지속시간(ms)
return () => clearTimeout(timeout); // 언마운트 시 타임아웃 정리
}, [location.pathname]); // 경로가 바뀔 때마다 실행
return (
<TransitionContainer
key={location.pathname} // ③ key가 바뀌면 React가 새로 마운트/언마운트
className={isExiting ? "slide-out" : ""} // ④ isExiting에 따라 퇴장 클래스 토글
>
{children} // ⑤ 실제 페이지 내용 렌더
</TransitionContainer>
);
};
// ▶ 애니메이션 스타일 정의
const TransitionContainer = styled.div`
position: absolute; /* 페이지를 서로 겹치게 함 */
top: 0; left: 0;
width: 100%;
height: 100%;
animation: slideIn 0.5s ease-in-out forwards; /* 기본 진입 애니메이션 */
&.slide-out {
animation: slideOut 0.5s ease-in-out forwards; /* 퇴장 애니메이션 */
}
/* 진입 애니메이션 키프레임 */
@keyframes slideIn {
from { transform: translateX(100%); } /* 오른쪽 바깥에서 시작 */
to { transform: translateX(0); } /* 제자리로 이동 */
}
/* 퇴장 애니메이션 키프레임 */
@keyframes slideOut {
from { transform: translateX(0); } /* 제자리에서 */
to { transform: translateX(-100%); } /* 왼쪽 바깥으로 나감 */
}
`;
export default PageTransition; // 사용하려면 export
// ▶ 리액트와 라우터 컴포넌트 불러오기
import React from "react";
import { HashRouter, Routes, Route } from "react-router";
// ▶ 페이지·스타일·애니메이션 컴포넌트
import Home from "./pages/Home";
import Today from "./pages/Today";
import "./styles/global/global.css";
import PageTransition from "./components/animation/PageTransition";
function App() {
return (
<HashRouter> {/* # 기반 라우터로 감싸기 */}
<PageTransition> {/* 여기 안의 페이지들에 애니메이션 적용 */}
<Routes> {/* 경로별 렌더되는 컴포넌트 설정 */}
<Route path="/" element={<Home />} />
<Route path="/today" element={<Today />} />
</Routes>
</PageTransition>
</HashRouter>
);
}
export default App; // 외부에서 사용 가능하게 export