React Native 애니메이션 선택 기준 정리

React Native 애니메이션 선택 기준 정리

2026년 08월 30일

들어가기 전에

지난 글에서 Shared Value의 구조를 정리했습니다. 그럼 애니메이션은 항상 Shared Value로 만들어야 할까요. 버튼 하나 페이드인 시키는 데도 useSharedValueuseAnimatedStyle을 꺼내야 하는지 생각해 볼 만합니다.

이 글은 Reanimated 4 / React Native New Architecture 기준입니다. React Native에 내장된 Animated API는 다루지 않습니다.

선택지는 세 갈래다

먼저 층위를 정리하는 게 좋겠습니다. 셋이 나란히 놓이는 게 아닙니다.

react-native-reanimated
├─ ① CSS Transitions / Animations   ← 선언적. 결과만 적는다
└─ ② Shared Value + Worklet         ← 명령적. 값을 직접 굴린다

③ react-native-ease                 ← 별도 라이브러리. 플랫폼에 맡긴다

①과 ②는 모두 Reanimated가 제공합니다. 이름이 CSS라서 웹 것이거나 별도 라이브러리처럼 보이지만 아닙니다.

① 선언적으로: CSS Transitions와 Animations

웹의 transition@keyframes를 스타일 객체로 옮겨 놓은 것입니다. 애니메이션을 굴리는 대신 원하는 결과를 적어 둡니다.

값이 바뀔 때: Transitions

지켜보던 값이 바뀌면 그 사이를 채웁니다.

const [expanded, setExpanded] = useState(false);
 
return (
  <Animated.View
    style={{
      width: expanded ? 260 : 120,
      backgroundColor: expanded ? "#fa7f7c" : "#87cce8",
      transitionProperty: ["width", "backgroundColor"],
      transitionDuration: 500,
    }}
  />
);

애니메이션 코드가 없습니다. setExpanded로 state를 바꾸면 widthbackgroundColor가 새 값을 받고, 그 사이를 Reanimated가 채웁니다. 지켜볼 속성을 transitionProperty에 적어 두는 것이 전부입니다.

타임라인을 재생: Animations

이쪽은 keyframe을 정해 두고 재생합니다.

const pulse = {
  from: { transform: [{ scale: 0.8 }] },
  to: { transform: [{ scale: 1.2 }] },
};
 
<Animated.View
  style={{
    animationName: pulse,
    animationDuration: "1s",
    animationIterationCount: "infinite",
  }}
/>;

state도 버튼도 없습니다. 마운트되면 스스로 시작합니다. 웹과 달리 전역 스타일시트가 없어서 이름 대신 keyframes 객체를 그대로 넘깁니다.

언제 시작하나성격
Transitions값이 바뀔 때A에서 B로 한 번
Animations마운트되면 스스로타임라인, 반복 가능

공식 문서가 여기서 시작하라는 이유

공식 문서는 이쪽을 기본 출발점으로 잡습니다. 검토해 볼 만한 선택지 정도가 아닙니다.

"This covers most everyday UI animations, and it's the recommended place to start."
CSS Transitions

이유도 밝혀 두었습니다. 훅과 Shared Value를 엮을 필요가 없어 코드가 짧고, JS 스레드 밖 Reanimated 코어에서 돌아갑니다. 무엇이 움직이는지 라이브러리가 미리 압니다. 워클릿은 실행 전까지 무엇을 반환할지 알 수 없지만 선언적 API는 "opacity가 500ms 동안 0에서 1로 간다"를 처음부터 알고 있습니다. 최적화 여지가 여기서 생깁니다.

섞어 쓸 때 걸리는 것

Transition은 리렌더로 값이 바뀔 때만 발동합니다.

"Passing an inline shared value won't trigger a transition, because updating a shared value doesn't re-render the component."
CSS Transitions

Shared Value를 transitionProperty 대상에 물려도 아무 일이 일어나지 않습니다. 두 방식을 섞어 쓰다 걸리기 쉬운 자리입니다.

② 직접 제어하기: Shared Value와 Worklet

프레임 단위로 값을 다뤄야 할 때 씁니다.

const progress = useSharedValue(0);
 
const style = useAnimatedStyle(() => ({
  opacity: progress.value,
  transform: [{ translateY: (1 - progress.value) * 12 }],
}));

자유도가 이 방식의 값어치입니다. 제스처 입력, 스크롤 오프셋, 보간, 파생값, 스프링 물리를 직접 다룰 수 있습니다.

다음 중 하나라도 해당하면 이쪽입니다.

  • pan, drag, pinch, swipe처럼 제스처를 따라가야 한다
  • 스크롤 위치에 따라 헤더나 캐러셀이 연동되어야 한다
  • interpolateuseDerivedValue 같은 계산이 필요하다
  • 애니메이션 중간값을 다른 로직에서 읽어야 한다
  • cancel, sequence, repeat 같은 세밀한 제어가 필요하다
  • layout animation이나 entering/exiting이 필요하다

progress.value = ... 같은 변경을 컴포넌트 렌더 중에 실행하면 안 됩니다. 이벤트 핸들러나 useEffect, 제스처 워클릿처럼 렌더 바깥에서 바꿔야 합니다.

③ 플랫폼에 넘기기: react-native-ease

Reanimated 밖의 선택지입니다. react-native-easeEaseViewanimatetransition을 넘기는 방식입니다.

import { EaseView } from "react-native-ease";
 
<EaseView
  animate={{ opacity: visible ? 1 : 0 }}
  transition={{ type: "timing", duration: 300 }}
/>;

모양은 CSS Transitions와 비슷하지만 누가 애니메이션을 굴리느냐가 다릅니다. JS 애니메이션 루프도, 워클릿도, Shared Value도 쓰지 않고 플랫폼 네이티브 API에 맡깁니다. iOS는 CAAnimation 계열, Android는 ObjectAnimatorSpringAnimation입니다.

범용 대체재는 아닙니다.

"Layout animations — Animating width/height/layout changes is not supported."
"Old architecture — Fabric (new architecture) only."
react-native-ease

레이아웃을 바꾸는 애니메이션이 안 되고 New Architecture에서만 동작합니다. 제스처 연동이나 복잡한 체이닝도 Reanimated의 영역입니다.

이득은 뷰가 많아질수록 커집니다. 만든 쪽이 공개한 측정값입니다.

뷰 개수react-native-easeReanimated CSSShared Value
10개0.210.991.15
500개0.605.508.31

Android 릴리스 빌드에서 프레임당 UI 스레드 시간(ms)이고 낮을수록 좋습니다. 10개일 때는 차이가 크지 않지만 500개에서 벌어집니다. 라이브러리를 만든 쪽 측정이고 에뮬레이터 환경이라는 점은 감안해야 합니다. 자세한 수치와 iOS 결과는 저장소 벤치마크에 있습니다.

네이티브 API로 넘긴다고 비용이 사라지는 건 아닙니다. 워클릿과 Shared Value 부담은 덜지만 네이티브 뷰 렌더링과 컴포지팅 비용은 그대로입니다.

무엇을 움직이는지도 본다

어떤 도구를 고르든 움직이는 속성이 무엇인지가 비용을 가릅니다.

구분대표 속성바꾸면 벌어지는 일
non-layoutopacity, transform, backgroundColor, borderRadius그 뷰만 다시 그린다
layoutwidth, height, top, margin, flex주변 요소의 배치까지 다시 계산한다

transformtop은 눈에 똑같아 보여도 이만큼 다릅니다. 애니메이션에서는 차이가 더 벌어집니다. 60fps면 16.67ms마다 새 화면을 만들어야 하는데, 그때마다 배치를 다시 계산하면 프레임 예산을 넘기기 쉽습니다.

같은 움직임이라면 non-layout으로 바꿔 볼 만합니다.

하려던 것대신
width 늘리기scaleX
top으로 옮기기translateY
나타났다 사라지기요소를 넣고 빼는 대신 opacitytransform

플랫폼에 맡기는 react-native-ease가 레이아웃 애니메이션을 지원하지 않는 것도 같은 맥락입니다. 네이티브 애니메이션 API가 건드리는 건 iOS CALayer의 키 패스와 Android View의 속성입니다. widthheight처럼 레이아웃을 다시 계산해야 하는 값은 애초에 그 대상이 아닙니다.

그래서 무엇을 쓸까

이럴 때선택
손가락을 따라 실시간으로 움직여야 한다 (드래그, 스와이프, 스크롤 연동)Shared Value + Worklet
진행 중인 값을 읽거나, 도중에 멈추거나, 여러 개를 차례로 실행해야 한다Shared Value + Worklet
상태가 바뀌면 알아서 전환되면 된다 (열림과 닫힘, 나타남과 사라짐, 스피너)CSS Transitions / Animations
위 경우인데 레이아웃을 바꾸지 않고, 같은 전환이 수많은 뷰에서 반복된다react-native-ease 검토

참고 문서