프론트엔드에서 애니메이션 주입은 필수적이다.
framer-motion 등 많은 애니메이션 라이브러리가 있지만 이번에는 DOM을 조작하여 애니메이션을 부여할 수 있는 react-transition-group 라이브러리를 활용하였다.
크게 4가지 컴포넌트가 있다.
- Transition
- CSSTransition
- SwitchTransition
- TransitionGroup
주로 사용할 컴포넌트는 CSSTransition과 TransitionGroup 이다.
CSSTransition
CSS로 변화를 주어 애니메이션을 실행시키고 싶다면 `CSSTransition 컴포넌트를 활용한다.
이 컴포넌트는 Transition 컴포넌트의 모든 요소를 상속받는다.
나는 CSSTransition 컴포넌트를 활용하여 Toast 기능을 구현하였다.
import { useRef } from "react";
import { CSSTransition } from "react-transition-group";
interface ToastProps {
open: boolean;
text: string;
}
export default function Toast({ open, text }: ToastProps) {
const ref = useRef<HTMLDivElement>(null);
return (
<CSSTransition
nodeRef={ref}
classNames={"toast"}
timeout={300}
in={open}
mountOnEnter
unmountOnExit
>
<div
ref={ref}
className="flex justify-center items-center absolute bottom-[5rem] left-1/2 transform -translate-x-1/2 rounded-[8px] h-[3.8rem] px-[2.4rem] py-[1.2rem] min-w-[19.8rem] bg-[#212121CC] text-white whitespace-nowrap body2-medium z-50"
>
{text}
</div>
</CSSTransition>
);
}
위는 Toast 코드이다. 사용한 props 에 대해 간단히 살펴보자.
nodeRef
애니메이션을 주입시킬 DOM 요소를 참조한다. 나는 애니메이션을 주입시킬 컴포넌트 내 태그에 ref를 선언해주었고, 이를 CSSTransition 컴포넌트가 nodeRef를 통해 참조할 수 있도록 하였다.
필수는 아니지만 이를 빼먹으면 개발 모드에서 strictMode 로 인해 에러가 난다. 기능에는 문제가 없지만 그래도 찝찝하니 웬만하면 추가하는게...ㅋ
timeout
변화, 즉 애니메이션이 실행될 시간을 정의할 때 사용한다.
classNames
애니메이션 관련 베이스 CSS 클래스 이름이 들어간다 (뒤에서 자세히) .
in
boolean 값으로 true 가 넘어올 시 위의 classNames와 관련된 클래스 이름을 찾아 애니메이션을 실행시킨다(이것도 뒤에서 자세히).
mountOnEnter
해당 prop이 선언되어있지 않으면 자식 컴포넌트는 자동으로 바로 mount 된다. 만약 in이 true로 넘어올 타이밍에 lazy하게 mount 되길 원한다면 해당 prop을 선언해줘야 한다.
unmountOnExit
해당 prop이 선언되어있지 않으면 애니메이션이 끝난 후에도 자식 컴포넌트가 mount된 상태로 유지된다. 만약 애니메이션이 끝날 때 자식 컴포넌트를 unmount시키고 싶으면 해당 prop을 선언해줘야 한다.
이 밖에도 addEndListener 등의 콜백 함수도 prop 으로 선언할 수 있는데 공식문서에 자세히 나와있다. (https://reactcommunity.org/react-transition-group/transition)
그럼 classNames와 in prop에 대해 좀만 더 자세히 알아보자.
나는 classNames 에 'toast' 를 넣어주었다. 그럼 이게 어떻게 동작하는 것일까??
.toast-enter {
opacity: 0;
}
.toast-enter-active {
opacity: 1;
transition: opacity 300ms ease-in-out;
}
.toast-exit {
opacity: 1;
}
.toast-exit-active {
opacity: 0;
transition: opacity 300ms ease-in-out;
}
Toast 관련 CSS 클래스를 정의한 파일이다.
in prop 이 true로 넘어올 타이밍에 자식 컴포넌트는 "{base 클래스 이름}-enter" 로 정의된 클래스를 받게 된다. 바로 다음 tick에 "{base 클래스 이름}-enter-active" 로 정의된 클래스를 받게된다.
공식문서에는 해당 단계가 reflow를 강제시키고, 애니메이션이 트리거되는 필수적인 요소라고 나와있다.
내 코드에서는 in={open} 으로 되어있는데, open 은 외부에서 useState 로 관리되고 있다. set 함수를 통해 open이 true로 넘어오면 toast-enter 클래스가 자식 컴포넌트에 들어가게 되고, 바로 다음 tick에 toast-enter-active 클래스가 들어가게 된다. 이 사이에 reflow가 일어나서 정의한 애니메이션이 실행되는 것이다.
마찬가지로 in prop이 다시 false로 넘어오게 되면 자식 컴포넌트는 해당 타이밍에 "{base 클래스 이름}-exit" 로 정의된 클래스를 받게되고, 바로 다음 tick에 "{base 클래스 이름}-exit-active" 로 정의된 클래스를 받게 되어 unmount될 때 애니메이션이 트리게되게 된다.
위와 같이 Toast가 잘 동작하는 것을 확인할 수 있다!
TransitionGroup
공간기록 기능을 구현할 때, 유저는 총 4단계의 기록을 거쳐야 했다. 단계별로 url은 유지되지만 새로운 화면을 보여줘야 했다. 이때 새로운 화면 전환 시 단순히 전환하는 것이 아니라 다음 단계로 넘어가는 것을 확실히 UI로 보여주고 싶었고, 이를 위해 한 화면이 나가고 한 화면이 들어올 때 양방향 트랜지션 구현이 필요했다.
이처럼 동시에 여러 child 컴포넌트에 애니메이션을 부여할 때 TransitionGroup을 사용한다.
<TransitionGroup
component={null}
childFactory={(child) => {
return cloneElement(child, {
classNames: `record-jump-${nextDirection}`,
});
}}
>
{indicatorIndex === 0 && (
<CSSTransition key={0} timeout={300}>
<SelectKeyword
placeType={placeType}
name={name}
indicatorIndex={indicatorIndex}
handleIndicatorIndex={handlers.changeIndicatorIndex}
cafeKeywordData={cafeKeywordData}
restaurantKeywordData={restaurantKeywordData}
handleKeyword={handlers.changeKeyword}
/>
</CSSTransition>
)}
{indicatorIndex === 1 && (
<CSSTransition key={1} timeout={300}>
<SelectEvaluation
placeType={placeType}
indicatorIndex={indicatorIndex}
handleIndicatorIndex={handlers.changeIndicatorIndex}
cafeKeywordData={cafeKeywordData}
restaurantKeywordData={restaurantKeywordData}
handleKeyword={handlers.changeKeyword}
/>
</CSSTransition>
)}
...
</TransitionGroup>
주요 props를 간단히 살펴보자.
component
TransitionGroup을 선언하면 자동으로 <div> 요소가 렌더링된다. <div>가 렌더링 되는것을 원치 않으면 해당 prop을 null로 설정하면 된다.
childFactory
해당 prop을 사용하면 애니메이션이 적용되는 child 모두를 update 할 수 있다. 주로 cloneElement 메소드를 통해 작업이 이루어진다.
cloneElement(element, props, ...children)
다음은 cloneElement 메소드의 레퍼런스이다.
위 코드에서는 각 애니메이션이 들어갈 각 child를 cloneElement를 통해 재정의 해주었다. props 는 원래 있던 props와 재정의 한 props 가 shallow copy되고 children은 기존거를 대체하여 넘겨진다.
props가 얕게 복사되므로 만약 복사한 후 props를 직접 변경하면 원본 element에 영향이 가겠지만, 반환되는 element 자체는 원본에 영향을 미치지 않는다.
그리고 뇌피셜이지만 동작 방식을 보니 매 애니메이션 사이클마다 childFactory로 넘어가는 child는 무조건 원본 child가 넘어가는 듯 하다.(안그러면 위에서 className 이 계속 쌓일텐데 그냥 교체된다.)
위 코드를 보면 외부에 정의되어있는 nextDirection 변수를 통해 className을 구분하였다.
다음 버튼을 누르면 nextDirection 변수는 'forward' 로, 이전 버튼을 누르면 'back' 값으로 설정되도록 구현하였다.
.record-jump-forward-enter {
transform: translateX(100%);
}
.record-jump-forward-enter-active {
transform: translateX(0);
transition: transform 300ms ease-in-out;
}
.record-jump-forward-exit {
transform: translateX(0);
}
.record-jump-forward-exit-active {
transform: translateX(-100%);
transition: transform 300ms ease-in-out;
}
.record-jump-back-enter {
transform: translateX(-100%);
}
.record-jump-back-enter-active {
transform: translateX(0);
transition: transform 300ms ease-in-out;
}
.record-jump-back-exit {
transform: translateX(0);
}
.record-jump-back-exit-active {
transform: translateX(100%);
transition: transform 300ms ease-in-out;
}
nextDirection이 forward 일때는 화면이 오른쪽으로 자연스럽게 넘어가고, back 일때는 왼쪽으로 자연스럽게 넘어가야 하므로 두가지 상황에 맞게 CSS 클래스 설정을 해주었다.
마지막으로로 child인 CSSTransition 컴포넌트에 각각 다른 key 값을 설정해줘야한다.
나의 경우 무조건 4단계로 이루어진 기능이였으므로 그냥 key값을 정적인 각각 다른 숫자로 지정해주었다.
위처럼 같이 방향을 결정하는 변수에 따라 className이 각각 다르게 부여되면서 애니메이션도 해당 방향에 따라 잘 적용되는 것을 확인할 수 있다!!
'FrontEnd > React' 카테고리의 다른 글
Suspense 효율적으로 사용하기 (4) | 2024.03.21 |
---|---|
서비스 내에 나만의 지도 띄우기 (Feat : NAVER MAP) (4) | 2024.02.27 |
ref 속성 파헤치기 (4) | 2024.02.10 |
스토리북(Storybook) 도입하기 (2) | 2024.02.05 |
Vite + Yarn berry 프로젝트 배포하기 (0) | 2023.12.24 |