[RN] 스크롤에 따라 컴포넌트를 동적으로 변화시켜보자

임찬혁·2022년 12월 29일
2

React Native

목록 보기
2/6
post-thumbnail

어플리케이션을 쓰다보면 스크롤에 따라 헤더나 텍스트의 위치 및 크기가 바뀌는 UI를 제공하는 경우가 종종 있습니다.
이런 UI는 제한된 모바일 화면의 크기 안에서 비교적 많은 양의 정보를 보여줌과 동시에 좀 더 사용자와 상호작용을 하고있다는 느낌을 줍니다.

Animated 는 무엇일까?

React Native에는 Animated API 가 있습니다. Animated 를 사용하여 쉽게 컴포넌트에 애니메이션 효과를 줄 수 있습니다.

  • React Native 공식 문서에 있는 예제 코드
import React, { useRef } from "react";
import { Animated, Text, View, StyleSheet, Button, SafeAreaView } from "react-native";

const App = () => {
  // fadeAnim: 컴포넌트의 opacity 속성에 적용할 것입니다. 초기값은 0입니다.
  const fadeAnim = useRef(new Animated.Value(0)).current;

  const fadeIn = () => {
    // fadeAnim 값이 1이 되도록 5초동안 변화시킵니다.
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 5000
    }).start();
  };

  const fadeOut = () => {
    // fadeAnim 값이 0이 되도록 3초동안 변화시킵니다.
    Animated.timing(fadeAnim, {
      toValue: 0,
      duration: 3000
    }).start();
  };

  return (
    <SafeAreaView style={styles.container}>
      <Animated.View
        style={[
          styles.fadingContainer,
          {
            // fadeAnim 값을 opacity에 적용합니다.
            opacity: fadeAnim
          }
        ]}
      >
        <Text style={styles.fadingText}>Fading View!</Text>
      </Animated.View>
      <View style={styles.buttonRow}>
        <Button title="Fade In View" onPress={fadeIn} />
        <Button title="Fade Out View" onPress={fadeOut} />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center"
  },
  fadingContainer: {
    padding: 20,
    backgroundColor: "powderblue"
  },
  fadingText: {
    fontSize: 28
  },
  buttonRow: {
    flexBasis: 100,
    justifyContent: "space-evenly",
    marginVertical: 16
  }
});

export default App;


간단한 예제를 확인했으니 이제는 실제로 스크롤에 따라 컴포넌트를 동적으로 변화시키는 방법을 구현해보겠습니다. 여기에서는 헤더 컴포넌트를 예로 들어서 설명합니다.

ScrollView 컴포넌트에 Animated 적용하기

ScrollViewAnimated 를 적용하기 위해서는 RN에서 제공되는 기본 ScrollView 컴포넌트 대신에 Animated.ScrollView 컴포넌트를 사용해야 합니다.
스크롤의 위치는 onScroll 이벤트에서 받아볼 수 있고, 너무 많은 이벤트가 trigger 되는 것을 방지하기 위해 scrollEventThrottle 옵션을 추가합니다.

  1. AnimatedReact 를 import합니다. ReactuseRef 만 import해도 됩니다.
import React, { useRef } from 'react';
import { Animated } from 'react-native';
  1. ScrollView 의 현재 스크롤 위치를 저장해둘 변수를 선언합니다.
const scrolling = React.useRef(new Animated.Value(0)).current;
// or
const scrolling = useRef(new Animated.Value(0)).current;
  1. onScroll 이벤트의 콜백함수를 정의합니다. 이 함수에서는 위에서 선언한 scrolling 변수에 스크롤의 현재 위치값을 저장합니다.
const onScroll = (e) => {
  const position = e.nativeEvent.contentOffset.y;
        
  scrolling.setValue(position);
};
  1. Animated.ScrollView 컴포넌트를 구성합니다. 컴포넌트 내부가 비어있으면 스크롤이 생기지않으니 더미 컴포넌트로 채워줍니다.
<Animated.ScrollView scrollEventThrottle={10} onScroll={onScroll}>
  {
    Array(20).fill(0).map((_, index) => {
      return (
        <View key={index} style={{ height: 100, marginVertical: 10, backgroundColor: 'lightgray' }}>
          <Text style={{color: 'black'}}>{`item${index}`}</Text>
        </View>
      );
    })
  }
</Animated.ScrollView>

여기까지의 전체 코드는 아래처럼 작성할 수 있습니다.

import React from 'react';
import { Animated } from 'react-native';

const App = () => {
  // scrolling: 현재 스크롤의 위치입니다. 초기값은 0입니다.
  const scrolling = React.useRef(new Animated.Value(0)).current;
  
  const onScroll = (e) => {
    // position: 스크롤의 y축(세로) 위치입니다.
    const position = e.nativeEvent.contentOffset.y;
        
    scrolling.setValue(position);
  };
  
  return (
    // 10 프레임당 한 번 onScroll 이벤트를 발생시킵니다.
    <Animated.ScrollView scrollEventThrottle={10} onScroll={onScroll}>
      {
        Array(20).fill(0).map((_, index) => {
          return (
            <View key={index} style={{ height: 100, marginVertical: 10, backgroundColor: 'lightgray' }}>
              <Text style={{color: 'black'}}>{`item${index}`}</Text>
            </View>
          );
        })
      }
    </Animated.ScrollView>
  );
}

export default App;

Header 컴포넌트 구성하고 Animated 적용하기

Header 컴포넌트는 View 컴포넌트를 이용해서 구성할 수 있습니다. 하지만 Animated 를 적용하려면 ScrollView 의 경우와 마찬가지로, RN에서 기본으로 제공되는 View 컴포넌트를 사용하는 대신에 Animated.View 컴포넌트를 사용해야 합니다.

  1. Header 컴포넌트의 height style에 적용할 변수를 선언합니다. 위에서 선언한 scrolling 변수의 interpolate 메소드로 선언합니다. inputRangescrolling 값의 범위를 표현하는 배열이고 outputRangescrolling 값에 따라 생성될 값의 범위를 표현하는 배열입니다. 아래의 코드에서는 scrolling 이 0인 경우에 height 는 200이 되고, scrolling 이 200인 경우에 height 는 50이 됩니다. ( 따라서 Scrolling 이 100인 경우에는 height 에 200과 50의 중간값이 적용됩니다. )
const height = scrolling.interpolate({
  inputRange: [0, 200],
  outputRange: [200, 50],
  extrapolate: 'clamp',
});
  1. Header 컴포넌트를 구성합니다. 위에서 선언한 height 를 style에 적용합니다. 컴포넌트의 style은 StyleSheet 를 이용하여 적용해도 됩니다.
<Animated.View style={{ height, backgroundColor: 'orange' }}>
</Animated.View>

여기까지의 전체 코드는 아래처럼 작성할 수 있습니다.

import React from 'react';
import { Animated } from 'react-native';

const App = () => {
  // scrolling: 현재 스크롤의 위치입니다. 초기값은 0입니다.
  const scrolling = React.useRef(new Animated.Value(0)).current;
  // height: 스크롤의 위치에 따른 Header 컴포넌트의 높이값입니다. 스크롤 위치가 0부터 200으로 변함에 따라 높이값은 200부터 50으로 비율에 맞춰 변합니다.
  const height = scrolling.interpolate({
    inputRange: [0, 200],
    outputRange: [200, 50],
    extrapolate: 'clamp',
  });
  
  const onScroll = (e) => {
    // position: 스크롤의 y축(세로) 위치입니다.
    const position = e.nativeEvent.contentOffset.y;
        
    scrolling.setValue(position);
  };
  
  return (
    <View>
      <Animated.View style={{ height, backgroundColor: 'orange' }}>
      </Animated.View>
      // 10 프레임당 한 번 onScroll 이벤트를 발생시킵니다.
      <Animated.ScrollView scrollEventThrottle={10} onScroll={onScroll}>
        {
          Array(20).fill(0).map((_, index) => {
            return (
              <View key={index} style={{ height: 100, marginVertical: 10, backgroundColor: 'lightgray' }}>
                <Text style={{color: 'black'}}>{`item${index}`}</Text>
              </View>
            );
          })
        }
      </Animated.ScrollView>
    </View>
  );
}

export default App;

Header 컴포넌트에 Text Animated 추가하기

AnimatedView 뿐만 아니라 Image, Text, FlatList, SectionList 등에 적용할 수 있습니다. 여기에서는 Text 에 적용해보겠습니다.

  1. TextfontSize style에 적용할 변수를 선언합니다. 위에서 선언한 height 와 같은 방식으로 선언하면 됩니다. height 와 마찬가지로, scrolling 이 0인 경우에 fontSize 는 32이고 scrolling 이 200인 경우에 fontSize 는 16입니다.
const fontSize = scrolling.interpolate({
  inputRange: [0, 200],
  outputRange: [32, 16],
  extrapolate: 'clamp',
});
  1. Header 컴포넌트의 child로 Animated.Text 컴포넌트를 추가합니다. 위에서 선언한 fontSize 변수를 style에 적용합니다. 컴포넌트의 style은 StyleSheet 를 이용하여 적용해도 됩니다. ( 정렬을 위해 Header 컴포넌트에 alignItemsjustifyContent 속성을 center 로 설정하였습니다. )
<Animated.View style={{ height, alignItems: 'center', justifyContent: 'center', backgroundColor: 'orange' }}>
  <Animated.Text style={{ fontSize, color: 'black' }}>Animated Header</Animated.Text>
</Animated.View>

여기까지의 전체 코드는 아래처럼 작성할 수 있습니다.

import React from 'react';
import { Animated } from 'react-native';

const App = () => {
  // scrolling: 현재 스크롤의 위치입니다. 초기값은 0입니다.
  const scrolling = React.useRef(new Animated.Value(0)).current;
  // height: 스크롤의 위치에 따른 Header 컴포넌트의 높이값입니다. 스크롤 위치가 0부터 200으로 변함에 따라 높이값은 200부터 50으로 비율에 맞춰 변합니다.
  const height = scrolling.interpolate({
    inputRange: [0, 200],
    outputRange: [200, 50],
    extrapolate: 'clamp',
  });
  // fontSize: 스크롤의 위치에 따른 Text 컴포넌트의 폰트 크기값입니다. 스크롤 위치가 0부터 200으로 변함에 따라 폰트 크기값은 32부터 16으로 비율에 맞춰 변합니다.
  const fontSize = scrolling.interpolate({
    inputRange: [0, 200],
    outputRange: [32, 16],
    extrapolate: 'clamp',
  });
  
  const onScroll = (e) => {
    // position: 스크롤의 y축(세로) 위치입니다.
    const position = e.nativeEvent.contentOffset.y;
        
    scrolling.setValue(position);
  };
  
  return (
    <View>
      <Animated.View style={{ height, alignItems: 'center', justifyContent: 'center', backgroundColor: 'orange' }}>
        <Animated.Text style={{ fontSize, color: 'black' }}>Animated Header</Animated.Text>
      </Animated.View>
      // 10 프레임당 한 번 onScroll 이벤트를 발생시킵니다.
      <Animated.ScrollView scrollEventThrottle={10} onScroll={onScroll}>
        {
          Array(20).fill(0).map((_, index) => {
            return (
              <View key={index} style={{ height: 100, marginVertical: 10, backgroundColor: 'lightgray' }}>
                <Text style={{color: 'black'}}>{`item${index}`}</Text>
              </View>
            );
          })
        }
      </Animated.ScrollView>
    </View>
  );
}

export default App;

마치며

React Native의 Animated API 로 구현할 수 있는 애니메이션은 정말 다양합니다. 서비스에 적절한 애니메이션을 적용하는 것은 서비스를 좀 더 멋지게 만들어줄 수 있는 하나의 방법인 것 같습니다.

예제코드

profile
개발새발자

0개의 댓글