React Native 의 애니메이션 구현 방법 (5. Layout Animations)

eeennsu·2026년 5월 23일

React Native

목록 보기
43/92

이번 글에서는 React의 렌더링 라이프사이클과 Yoga(레이아웃 엔진)의 계산 결과를 중간에서 가로채어 애니메이션을 주입하는 기술을 살펴보자.

핵심은 두 가지다.

  1. Layout Animation : 컴포넌트의 등장/퇴장/레이아웃 변경을 자동으로 애니메이트
  2. Shared Element Transition : 화면 간 동일 요소를 자연스럽게 연결

 

1. 기존 React 언마운트의 한계와 Reanimated의 해결책

한계점

일반적인 React에서 {isVisible && <View />} 코드가 false가 되면, React는 해당 노드를 unmount하고 Fabric 렌더러에 "이 C++ Shadow Node를 즉시 제거하라"고 명령한다. 화면에서 순식간에 사라지므로 퇴장(Exit) 애니메이션을 줄 시간이 없다.

과거에는 이 문제를 우회하려고 직접 opacity를 줄인 뒤 setTimeout으로 unmount하는 식의 번거로운 패턴을 썼다.

Reanimated의 개입

<Animated.View exiting={...}>를 사용하면, Reanimated는 Fabric의 언마운트 명령을 중간에서 가로챈다(Intercept). React 측에서는 컴포넌트가 unmount되었지만, C++ 네이티브 단에서는 뷰를 살려두고 워클릿을 통해 퇴장 애니메이션을 끝마친 뒤에야 진짜로 뷰를 메모리에서 해제한다.

[RN 기본 애니메이션]
setState(false) → React unmount → Shadow Node 즉시 제거 → 화면에서 사라짐
                                  (애니메이션 불가)

[Reanimated]
setState(false) → React unmount 신호 → Reanimated가 가로챔
              → 네이티브 뷰는 유지 + exiting 워클릿 실행
              → 애니메이션 종료 후 Shadow Node 제거

 

2. 핵심 애니메이션 prop : entering, exiting, layout

선언적으로 prop만 부여하면 된다. 3가지 prop이 핵심이다.

Prop시점대표 프리셋
entering컴포넌트가 마운트될 때FadeIn, SlideInRight, ZoomIn, FadeInUp
exiting컴포넌트가 언마운트될 때FadeOut, SlideOutLeft, ZoomOut, FadeOutDown
layout형제/부모 변화로 내 위치가 변할 때LinearTransition, SequencedTransition, FadingTransition

빌더 패턴으로 세부 조정

모든 프리셋은 빌더 패턴으로 세부 설정이 가능하다.

FadeInUp
  .duration(300)        // 0.3초 동안
  .delay(100)           // 0.1초 지연 후 시작
  .springify()          // 스프링 기반 애니메이션으로
  .damping(14);         // 스프링 감쇠 계수

withDelay로 리스트 아이템을 하나씩 순차적으로 등장시키는 트릭도 자주 쓰인다.

{items.map((item, index) => (
  <Animated.View
    key={item.id}
    entering={FadeInUp.delay(index * 50)} // 50ms씩 시차를 두고 등장
  />
))}

 

3. 실무 예시: 동적 리스트

가장 많이 쓰이는 '아이템이 삭제될 때 밑에 있던 아이템들이 스르륵 위로 올라오는' 리스트 예시이다.

import React, { useState } from 'react';
import { Text, TouchableOpacity, ScrollView, StyleSheet } from 'react-native';
import Animated, {
  FadeInUp,
  FadeOutDown,
  LinearTransition,
} from 'react-native-reanimated';

export default function DynamicListExample() {
  const [items, setItems] = useState([1, 2, 3, 4, 5]);

  const removeItem = (idToRemove: number) => {
    // JS 스레드에서 상태만 변경하면 끝.
    // Reanimated가 exiting과 layout 애니메이션을 자동으로 처리해준다.
    setItems((prev) => prev.filter((id) => id !== idToRemove));
  };

  return (
    <ScrollView style={styles.container}>
      {items.map((id) => (
        <Animated.View
          key={id}
          // 1. 등장할 때 위에서부터 부드럽게 나타남
          entering={FadeInUp.duration(300)}

          // 2. 삭제될 때 아래로 스르륵 사라짐 (Fabric 언마운트 지연)
          exiting={FadeOutDown.duration(300)}

          // 3. 누군가 삭제되어서 내 위치가 바뀔 때,
          //    뚝 끊기지 않고 새 위치로 부드럽게 이동 (Yoga 레이아웃 결과 가로채기)
          layout={LinearTransition.springify().damping(14)}

          style={styles.listItem}
        >
          <Text style={styles.text}>아이템 번호: {id}</Text>
          <TouchableOpacity onPress={() => removeItem(id)}>
            <Text style={styles.deleteBtn}>삭제</Text>
          </TouchableOpacity>
        </Animated.View>
      ))}
    </ScrollView>
  );
}

const styles = StyleSheet.create({
   ... 
});

layout 이 유용한 이유

형제 노드가 사라지면 Yoga 엔진이 모든 자식의 새로운 좌표를 다시 계산한다. layout prop이 없으면 이 새 좌표가 즉시 적용되어 아이템들이 뚝 끊기듯 위로 점프한다. Reanimated는 Yoga의 이전 좌표와 새 좌표를 모두 캡처한 뒤, 그 사이를 매 프레임 보간(interpolate)해서 부드러운 이동을 만들어낸다.

 

4. 화면 간 요소 연결 방법

목록 화면(Screen A)의 작은 썸네일을 눌렀을 때, 상세 화면(Screen B)의 큰 이미지로 이미지가 날아가며 변하는 애니메이션이다. iOS의 'Photos' 앱이나 인스타그램에서 흔히 볼 수 있는 UX다.

⚠️ 2026년 5월 현재 상태

Shared Element Transitions는 Reanimated 4.2.0(2025년 12월 출시)부터 New Architecture(Fabric)에서 다시 지원된다. 다만 다음 조건이 모두 필요하다.

  • react-native-reanimated 4.2.0 이상
  • react-native-screens 4.19+ (native stack 필수)
  • @react-navigation/native-stack 사용
  • ENABLE_SHARED_ELEMENT_TRANSITIONS feature flag 활성화 필요
  • Reanimated 팀 공식 안내: 아직 production-ready 아님 (experimental)

Tab navigator, JS stack(@react-navigation/stack), 투명 모달 등에서는 동작하지 않거나 제한이 있다. 실서비스 도입은 안정화 후 검토하는 것이 안전하다.

사용법

양쪽 화면의 컴포넌트에 동일한 sharedTransitionTag 문자열만 넣어주면 된다.

// Screen A (목록 화면)
<Animated.Image
  source={{ uri: 'photo.jpg' }}
  style={{ width: 100, height: 100 }}
  sharedTransitionTag="profile-image-123" // 고유한 태그 부여
/>

// Screen B (상세 화면)
<Animated.Image
  source={{ uri: 'photo.jpg' }}
  style={{ width: '100%', height: 300 }}
  sharedTransitionTag="profile-image-123" // 동일한 태그 매칭
/>

동작 원리

화면 전환이 발생하는 찰나에 Reanimated는 다음 과정을 수행한다.

  1. 동일한 sharedTransitionTag를 가진 두 컴포넌트의 절대 좌표(originX, originY)와 크기(width, height)를 캡처한다.
  2. React Navigation의 화면 전환 위에 투명한 오버레이를 깐다.
  3. 그 위에서 가짜(clone) 이미지를 A 화면의 좌표/크기에서 B 화면의 좌표/크기로 워클릿을 통해 보간하여 날려 보낸다.
  4. 전환이 끝나면 오버레이를 제거하고 실제 B 화면을 노출한다.

커스텀 트랜지션

sharedTransitionStyle로 동작 방식을 조정할 수 있다. 양쪽 화면에 동일한 스타일을 적용해야 한다.

import { SharedTransition, withSpring } from 'react-native-reanimated';

const bouncyTransition = SharedTransition.custom((values) => {
  'worklet';
  return {
    width: withSpring(values.targetWidth),
    height: withSpring(values.targetHeight),
    originX: withSpring(values.targetOriginX),
    originY: withSpring(values.targetOriginY),
  };
});

// 양쪽 화면 모두에 적용
<Animated.Image
  sharedTransitionTag="profile-image-123"
  sharedTransitionStyle={bouncyTransition}
/>

기본값으로는 width, height, originX, originY, transform을 500ms withTiming으로 보간한다. 커스텀 함수는 부분적으로만 지원되고, withSpring/withTiming 기반으로만 정의 가능하다.

 

5. 주의할 점

❌ 부모 unmount 시 자식의 exiting은 무시된다

자식 뷰에 exiting을 걸어두었는데 부모 뷰 전체가 조건부 렌더링으로 사라지면, 자식의 exiting 애니메이션은 실행되지 않고 즉시 파괴된다. Reanimated는 부모가 사라지는 순간의 자식 좌표를 추적할 수 없기 때문이다.

// ❌ Bad: 부모가 사라지면 자식의 exiting은 무시됨
{isOpen && (
  <View>
    <Animated.View exiting={FadeOut} />
  </View>
)}

// ✅ Good: 자식 자체를 조건부로 렌더링
<View>
  {isOpen && <Animated.View exiting={FadeOut} />}
</View>

퇴장 애니메이션을 보장하려면 사라지는 노드 자체가 Animated.View여야 한다.

key 변경은 unmount+mount다

배열 인덱스를 key로 쓰면 아이템 순서가 바뀔 때 React는 이를 기존 노드 제거 + 새 노드 추가로 해석한다. layout 애니메이션이 의도대로 동작하지 않으므로, 안정적인 ID를 key로 써야 한다.

FlatList/FlashList와의 호환성

가상화 리스트(virtualized list)에서는 entering/exiting이 의도대로 동작하지 않을 수 있다. 화면 밖으로 스크롤된 아이템이 unmount되면서 매번 등장 애니메이션이 재생되기 때문이다. 짧은 리스트는 ScrollView+map을, 긴 리스트는 itemLayoutAnimation prop(Reanimated가 제공하는 FlatList 래퍼)을 활용하자.

import Animated, { LinearTransition } from 'react-native-reanimated';

<Animated.FlatList
  data={items}
  renderItem={renderItem}
  itemLayoutAnimation={LinearTransition} // 아이템 위치 변경만 애니메이트
/>

❌ Layout Animation은 position: 'absolute'와 상성이 나쁘다

절대 위치로 배치된 뷰는 Yoga의 레이아웃 흐름 바깥에 있어서 layout prop이 의도대로 작동하지 않는다. 이런 경우엔 useAnimatedStyle로 직접 transform을 다루는 게 낫다.

 

Reanimated는 강력하지만 만능은 아니다. 단순한 페이드인/아웃 정도라면 RN 내장 Animated API나 LayoutAnimation으로도 충분하고, 더 복잡한 물리 시뮬레이션이나 SVG 모핑은 react-native-skiareact-native-svg 같은 도구를 함께 쓰는 게 낫다. 도구의 선택지를 넓혀두고, 상황에 맞는 도구를 고르는 안목이 결국 좋은 애니메이션을 만드는 핵심이다.

profile
이력서 https://resume.eunsu.pro

0개의 댓글