framer motion이란?
framer motion은 리액트 기반의 라이브러리로
애니메이션을 다루는 데에 큰 도움을 주는 라이브러리입니다.
여러가지 도움이 많이 되는 라이브러리이지만 가장 코어한 기능이 뭐냐라고 하면
저는 바로 리액트에서 구현하기 힘든 exit motion을 쉽게 구현할 수 있게 해주는 것이라고 생각합니다.
코드를 작성하다보면 무언가가 등장할 때 주는 애니메이션도 필요하지만
무언가가 사라질 때 주어야하는 애니메이션도 존재하기 마련인데
리액트에서는 이 사라지는 애니메이션을 구현해주기가 매우 까다롭습니다.
리액트 컴포넌트 트리에 존재하지 않는 돔 요소들과의 상호작용이 필요하기 때문인데요
framer motion은 향후 좀 더 깊이 다루어 볼 생각이지만
우선은 radix와의 통합 방법을 알아보겠습니다.
그냥 두개 같이 쓰면 되는거 아님?? 이라고 하실수도 있지만
그렇게 했을때는 지금 이야기했던 exit motion이 구현되지 않습니다.
framer motion에서 exit 모션 구현하는 방법
import { AnimatePresence } from "framer-motion";
<AnimatePresence>
</AnimatePresence>
framer motion에서는 exit motion을 구현하기 위한 도구로
AnimatePresence라는 컴포넌트를 제공합니다
일종의 프로바이더라고도 생각할 수 있는데요
이 안에 있는 motion 요소들은 언마운트되어야하는 상황이 되었을 때
DOM 트리에서 사라지는 것을 유예하고 퇴장 애니메이션을 수행한뒤 DOM 트리에서 제거됩니다.
그런데 여기에서 생각해야하는 것은 AnimatePresence가
"퇴장하는 요소들을 식별하고 애니메이션을 관리"하는 역할을 잘 수행하기 위해서는
퇴장하는 요소들을 식별할 수 있어야 한다는 것입니다.
따라서 AnimatePresence 가 퇴장할 요소들을 잘 식별할 수 있도록
자식 요소들에게 유니크한 키값을 부여해주어야합니다.
<AnimatePresence>
{open && (
<motion.div
className={`w-[100px] h-[100px] bg-primary-100`}
exit={{ y: [0, 100] }}
key={"ㅇㅁㄴ"}
initial={{
y: 0,
}}
animate={{
y: [100, 0],
}}
transition={{
duration: 0.4,
}}
>
<div className=""></div>
</motion.div>
)}
</AnimatePresence>
initial, exit, animate, tranistion은 framer motion에서 사용하는 프로퍼티라고 생각하면 되겠습니다.
중요한것은 key 값을 잘 전달해주었다는 것입니다.
이렇게 키값을 잘 전달하고 퇴장 애니메이션도 잘 지정을 해줬으니
일반적인 환경에선 퇴장 애니메이션이 잘 수행되는 것을 확인할 수 있었습니다.
그러나.. 이것을 Radix와 함께 사용하려고하니 퇴장 애니메이션에서 문제가 펑펑 터지기시작합니다.
열심히 구글링해봤는데 한글자료는 거의 전무하다시피 한 상황이더군요
문제 1 퇴장 애니메이션이 아예 동작하질 않는다.
왜 동작하지 않는 것일까요?
개발자도구를 켜보면서 고민해본 결과 한가지 결론에 도달했습니다.
이 동영상은 일반적인 경우에서 프레이머모션을 사용했을 때의 개발자도구상태입니다.
잘 살펴보면 버튼 클릭 이벤트로 인하여 상태가 변했음에도 불구하고
AnimatePresence 로 인해 dom tree에서 즉시 제거되지 않으며
애니메이션이 다 수행되기까지 기다렸다가 dom tree에서 사라지는 것을 확인할 수 있습니다.
그러나 radix의 내부에서 사용하게 되면 AnimatePresence가 제대로 동작하지 않는 것으로 보이는데요
이는 radix의 unmount 메커니즘이 따로 존재하기 때문으로 보여집니다.
https://github.com/radix-ui/primitives/discussions/1058
이에 대하여 좀 더 찾아보던 중 깃허브 이슈를 찾아볼 수 있었는데요
대체 어느 문서에 있다는건진 모르겠지만 아무튼 문서에서 시키는대로 해도
잘 동작하지 않는다는 내용이었습니다.
저는 그런 문서가 있는 줄도 몰랐으니 우선 저사람이 강조하는 요소들을 살펴보았습니다.
중요한것은 forceMount와 asChild 속성을 주었느냐 주지않았느냐라고 하는데요
이 둘을 Toast.Root에 적용시켜주었습니다.
하지만 여기서 끝이 아닌데요
여러번의 실험 끝에 AnimatePresence의 위치또한 중요한것을 확인했으며
radix는 내부적으로 ref를 이용하여 하위 컴포넌트들을 관리하는데
AnimatePresence 컴포넌트에게 React.forwardRef 처리가 되어있지도 않으며
이 처리를 사용자가 직접 해주기에도 애매한 부분이 있다는 것을 확인했습니다.
그래서 AnimatePresence를 Radix 컴포넌트들의 직계 자손으로 두면 이런 에러를 만나게될 수 있습니다.
그러면 결론적으로 어떻게 해야되는가?
<Toast.Provider swipeDirection="down" duration={1000}>
<div className="">하이염</div>
<ButtonAtom onClick={handler}>토스트버튼</ButtonAtom>
<Toast.Root
className="ToastRoot"
forceMount
asChild
open={open}
onOpenChange={setOpen}
>
<div className="">
<AnimatePresence>
{open && (
<motion.div
className={`w-[100px] h-[100px] bg-primary-100`}
exit={{ y: [0, 100] }}
key={"ㅇㅁㄴ"}
initial={{
y: 0,
}}
animate={{
y: [100, 0],
}}
transition={{
duration: 0.2,
}}
ref={ab}
>
<div className=""></div>
<Toast.Close>안녕히가세요</Toast.Close>
</motion.div>
)}
</AnimatePresence>
</div>
</Toast.Root>
<Toast.Viewport className="fixed bottom-0 left-0 right-0 mx-auto flex flex-col gap-[10px] w-[100px] max-w-[100vw] z-[2147483647] outline-none" />
</Toast.Provider>
이게...최선일까요....네...제게는 최선입니다.
ref를 잘 건네받을 수 있는 기본 html 태그로 AnimatePresence를 래핑해주고
Root에는 asChild와 forceMount 속성을 줍니다.
그리고 AnimatePresence는 조건부 렌더링이 되지 않으면서도
최대한 motion 요소들의 가까이에 배치해야합니다.
마치며
정말 눈물나네요..
'frontend' 카테고리의 다른 글
제목은 일단 스토리북으로 하겠습니다 그런데 고민을 곁들인 (2) | 2023.10.31 |
---|---|
프론트엔드에서 좋은 폴더 구조는 무엇일까? (1) | 2023.10.25 |
radix 의 기본 focus css 제거하기 (0) | 2023.09.30 |
hotfix branch 전략으로 빠른 버그수정하기 (0) | 2023.09.23 |
웹뷰(WebView)란 무엇일까 (0) | 2023.09.13 |