[FM] Animation - Layout

0

Framer-motion

목록 보기
4/5
post-thumbnail

Framer motion으로 레이아웃을 생성하고 공용 레이아웃 애니메이션을 주는 방법을 알아봅니다.

Layout animations

CSS로 레이아웃 애니메이션을 주는것은 어렵기도 하고 성능 저하를 일으킨다고 알려져 있습니다. 예를 들어, height값을 100px에서 500px로 바꾸면서 그 사이 애니메이션을 준다고 할 때, 매 애니메이션 프레임마다 브라우저가 레이아웃을 그려야 하기 때문에 성능저하를 일으키게 되겠죠?
Framer motion은 브라우저의 기본 레이아웃 시스템 대신 고성능의 transform을 이용하여 어떠한 CSS 레이아웃이던 애니메이션을 주기 쉽게 해줍니다. 예를 들어 다음 토글버튼은 justify-content속성을 flex-start에서 flex-end로 바꾸는 애니메이션이 적용되어 있습니다.

Framer-motion으로 레이아웃 변경 시 애니메이션을 주려면 다음과 같이 간단히 layout prop을 motion 컴포넌트에 주입하면 됩니다!

<motion.div layout />

이렇게 되면 모든 재렌더링을 유발하는 레이아웃 변경사항은 애니메이션이 적용되게 됩니다. 다음은 애니메이션이 재생될 레이아웃 변경사항의 예시들입니다.

  • 리스트 요소들의 재배열(순서 변경)
  • widthposition과 같은 특정 컴포넌트의 스타일 속성 변경
  • 부모 요소의 레이아웃 변경(flexgrid)
  • 그 외 모든 컴포넌트의 레이아웃 관련 변경사항

Scale correction

모든 레이아웃 애니메이션은 transform 속성을 이용해서 재생됩니다. 그 결과 아주 스무스한 애니메이션 효과를 확인할 수 있습니다.
하지만, 때때로 transform 속성으로 적용한 애니메이션은 자식 요소들의 모양을 찌그러트리거나 왜곡시킬 수 있습니다. 이런 현상을 방지하기 위해서 레이아웃 애니메이션이 적용된 요소의 첫 번째 자식요소에도 layout prop을 부여할 수 있습니다.

아래 예시에서 핑크색 원 요소에 layout prop이 있는 상태와 없는 상태에서 부모 요소의 layout 애니메이션 차이를 확인해보세요!

transformboxShadowborderRadius값 또한 왜곡시킬 수 있습니다. 그러나 위 두 속성값이 motion value로 세팅되어 있다면 motion 컴포넌트가 자동으로 이러한 왜곡현상을 바로잡아줍니다.
만약 이 두 속성값에 애니메이션을 주지 않고 왜곡현상을 없애려면 style속성을 통해 이 값들을 세팅하는 것이 가장 쉬운 방법이라고 권장하고 있습니다.

<motion.div layout style={{ borderRadius: 20 }} />

Customizing layout animations

transition 속성을 통해 레이아웃 애니메이션을 커스텀할 수도 있습니다.

<motion.div layout transition={{ duration: 0.3 }} />

다음과 같이 layout transition을 특정하여 오직 layout 애니메이션만 커스텀할 수도 있습니다.

<motion.div
  layout
  animate={{ opacity: 0.5 }}
  transition={{
    opacity: { ease: "linear" },
    layout: { duration: 0.3 }
  }}
/>

Animating within scroll containers

스크롤이 있는 요소에 레이아웃 애니메이션을 정확히 주기 위해서는 해당 요소들에 layoutScroll prop을 부여해야 합니다.

<motion.div
  layoutScroll
  style={{ overflow: "scroll" }}
/>

layoutScroll prop을 주면 Framer-motion이 스크롤 오프셋값을 계산하여 정확한 애니메이션을 구사하게 됩니다.

Coordinating layout animations

레이아웃 애니메이션은 컴포넌트가 재렌더링 될 때 레이아웃 관련 속성이 바뀌면 재생됩니다.
예를 들어서, 다음과 같은 아코디언 컴포넌트가 있다고 해보죠. 코드를 보면 클릭시 isOpen이라는 변수가 토글되어 요소의 높이가 100px500px을 왔다갔다 합니다.

function Accordion() {
  const [isOpen, setOpen] = useState(false)
  
  return (
    <motion.div
      layout
      style={{ height: isOpen ? "100px" : "500px" }}
      onClick={() => setOpen(!isOpen)}
    />
  )
}

그런데 만약, 아래와 같이, 동시에 재렌더링되지 않지만 각자의 재렌더링 타이밍에 서로의 레이아웃에 영향을 주는 두개 이상의 컴포넌트가 있다면 어떻게 될까요?

function List() {
  return (
    <>
      <Accordion />
      <Accordion />
    </>  
  )
}

위 예시는 아코디언 컴포넌트가 2개 존재하는 리스트 컴포넌트입니다. 각 아코디언은 동시에 재렌더링되는 구조는 아닙니다. 하지만 위의 아코디언을 클릭하면 길이가 늘어나면서 아래 아코디언의 위치도 밑으로 내려가게 됩니다. 레이아웃에 영향을 받는것이죠. 하지만 이런 경우 아래의 아코디언은 재렌더링되는것은 아니기 때문에 레이아웃의 변경사항을 감지하지 못하게 됩니다.

이러한 문제는 이 두 컴포넌트를 다음과 같이 LayoutGroup 컴포넌트 안에 감싸서 해결할 수 있습니다.

import { LayoutGroup } from "framer-motion"

function List() {
  return (
    <LayoutGroup>
      <Accordion />
      <Accordion />
    </LayoutGroup>  
  )
}

이렇게 해주면 앞으로 그룹 안에 속한 모든 컴포넌트들은 추가적인 재렌더링 없이 정상적인 레이아웃 애니메이션이 전부 적용되게 됩니다!

Shared layout animations

레이아웃 애니메이션도 고유의 id를 가질 수 있습니다.

layoutId라는 고유 레이아웃 애니메이션의 id값을 특정 요소에 부여할 수 있습니다. 말 그대로 고유한 id이므로, 하나만 존재할 수 있습니다.
만약 a라는 layoutId를 가진 요소가 있는데, 같은 a라는 아이디를 가진 요소가 추가된다면, 기존 요소에서 자동적으로 animate out되게 됩니다.
다음과 같이 underline이라는 layoutIdselected상태에서만 부여됩니다. 기존에 선택되어 있던 탭이 있기에 기존 요소에 있던 underline이라는 layoutId를 가진 div가 새로운 탭을 선택하게 되면 사라지는 애니메이션이 적용된 것을 볼 수 있습니다.

만약 새로운 요소가 추가될 때 기존 요소도 따로 언마운트되는 구조가 아니라면, 자동으로 crossfade 형식으로 기존 -> 새로운 요소 방향으로 애니메이션이 적용됩니다.

Troubleshooting

레이아웃 애니메이션과 관련하여 몇 가지 유의할 점

컴포넌트가 애니메이션이 적용되지 않아요

  • display:inline 속성이 없어야 합니다.

SVG 레이아웃 애니메이션이 깨져보여요

  • SVG 컴포넌트는 현재로서 Framer-motion의 layout animation을 지원하지 않습니다. cx와 같은 속성들을 직접적으로 애니메이트 시키는 것을 권장합니다.

Skew transform이 먹통이에요

  • skew transform은 layout animation기능을 지원하지 않습니다.

내용물이 의도치 않게 길게 늘어져요

  • 텍스트가 포함된 컴포넌트에 흔히 있는 일입니다. widthheight를 scale관련해서 애니메이트 시킬때 자연스레 발생하는 side effect입니다. 이런 경우엔 보통 layout="position"을 주어 position이 변할 때만 애니메이트 시키는 것을 권장합니다.

boderRadius와 boxShadow가 이상해요

  • scale을 애니메이트하는 것은 잘 동작하지만 위와 같은 몇몇 속성은 어색하게 애니메이트될 수 있습니다. Framer motion이 이러한 왜곡 현상을 자동으로 바로잡아 주지만 이 속성들이 pixel단위로 정의되어 있을때로 한정되어 있습니다.

Sticky 요소들이 의도한대로 애니메이트되지 않아요

  • position: sticky의 특성상 레이아웃 변경 감지가 거의 불가능합니다. 그 대신 sticky 요소들의 자식들이 애니메이트되지 않을 때는 방법이 있습니다. sticky 요소에 layout, layoutRoot prop을 부여하면 해당 요소를 루트로 레이아웃 계산이 수행되게 되어 자식들은 정상적으로 애니메이트 될 것입니다.

    <motion.header layout layoutRoot style={{ 	position: "sticky" }}>
       <motion.h1 layout />
     </motion.header>

주변 내용이 바뀔때 의도치 않은 애니메이션이 적용돼요

  • 이 현상 또한 layout, layoutRoot prop을 부모에 부여하면, 모든 자식들의 애니메이션이 해당 부모 기준으로 레이아웃 계산에 의해 애니메이션 될 겁니다.

    <motion.div className="switch-container" layout layoutRoot>
      <motion.div className="switch-handle" layout />
    </motion.div>

1개의 댓글

comment-user-thumbnail
2023년 12월 31일

벌써 최애가 생겼군요,,

답글 달기