React Native Animated: 스크롤 시 헤더 타이틀 페이드 인/아웃 구현
NOTE
이 포스터는 React Native의 Animated API를 사용하여 내비게이션 헤더 타이틀을 동적으로 제어하는 방법을 다룹니다. 스크롤 위치에 따라 헤더 타이틀이 페이드 인/아웃되도록 하는 커스텀 훅을 만들고, 이를 스크롤 가능한 컴포넌트(ScrollView) 에 적용하는 전 과정을 공유합니다.
Bitfolio앱을 개발할 때 구현했던 UI의 일부입니다.
설명의 이해를 돕기 위해, 실제 product의 핵심 일부만을 발췌해 예제로 재구성하였습니다. [해당 예제](https://snack.expo.dev/@Heesu Choiio/configuring-the-header-bar-%7C-react-navigation)를 기준으로 설명을 진행하니 참고 바랍니다.
전제 조건
- @react-navigation/native >= 5.x.x
- @react-navigation/stack >= 5.x.x
화면 구성
이번 포스터의 핵심은 스크롤 위치에 따라 헤더 타이틀이 페이드 인/아웃되도록 돕는 커스텀 훅을 만드는 것이므로, 화면 구성은 최대한 간결하게 진행하겠습니다.
App.tsx 파일에서 @react-navigation/stack
의 createStackNavigator
를 이용해 스택 내비게이터를 만들 거예요. 그리고 ScrollView 컴포넌트로 감싼 HomeScreen이라는 화면도 하나 만들어 두겠습니다.
import { View, Text, ScrollView } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
function HomeScreen() {
return (
<ScrollView>
<View style={{ flex: 1, height: 1000 }}>
<Text style={{ fontSize: 20, fontWeight: 'bold', marginLeft: 16, paddingTop: 30 }}>
Home
</Text>
</View>
</ScrollView>
);
}
const Stack = createStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="HomeScreen">
<Stack.Screen name="HomeScreen" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
커스텀 훅 구현
스크롤 위치에 따라 헤더 타이틀에 페이드 인/아웃 애니메이션을 적용할 수 있도록 돕는 커스텀 훅인 useAnimatedHeaderTitle
을 구현해보겠습니다.
이 훅은 다음 두 가지 인자를 받습니다.
- title: 새롭게 설정될 헤더 타이틀 값
- triggerPoint: Y축 기준으로 페이드 인이 시작될 스크롤 위치
useLayoutEffect(() => {
if (title !== undefined) {
navigation.setOptions({ title })
}
}, [navigation, title])
title 인자가 명시된 경우, 위 코드처럼 해당 값을 네비게이션의 헤더 타이틀로 설정합니다. 반면, title이 명시되지 않은 경우에는 초기 화면 구성에서 name을 HomeScreen
으로 정의하였으니, HomeScreen
이 기본 헤더 타이틀로 설정됩니다.
헤더 타이틀 값을 설정할 때 useEffect가 아닌 useLayoutEffect를 사용하는 이유가 궁금할 수 있을 것 같은데요.
그 이유를 간단히 설명하자면, useEffect
는 컴포넌트가 화면에 렌더링되고 화면에 실제로 그려진 후(paint 이후) 실행되기 때문에, 기본적으로 설정된 "HomeScreen"이라는 타이틀이 잠깐 보였다가 이후에 인자로 받은 title로 변경되는 깜빡임 현상이 발생할 수 있습니다.
반면 useLayoutEffect
는 렌더링 직후, 화면에 그려지기 전에 실행되기 때문에, 처음부터 원하는 title이 헤더에 적용되며 이러한 깜빡임 없이 자연스럽게 처리됩니다.
이제 핵심 기능인 페이드 인/아웃 애니메이션을 구현해보겠습니다.
우선, Y축 스크롤 위치(contentOffset
)를 추적하기 위해 Animated.Value
를 생성합니다. 이 값은 scrollY라는 상수에 useRef를 이용해 초기값 0으로 설정합니다
const scrollY = useRef(new Animated.Value(0)).current
다음으로, scrollY
가 triggerPoint
에 도달하면 타이틀이 서서히 나타나도록 opacity 애니메이션을 설정하고, 이를 headerStyleInterpolator
를 통해 헤더 스타일에 반영합니다.
useEffect(() => {
navigation.setOptions({
headerStyleInterpolator: () => {
const opacity = scrollY.interpolate({
inputRange: [triggerPoint, triggerPoint + 20],
outputRange: [0, 1],
})
return {
titleStyle: { opacity },
}
},
})
}, [navigation, scrollY])
이제 앞서 설명한 내용을 바탕으로 전체 코드를 정리해보면 다음과 같습니다!
import { useLayoutEffect, useEffect, useRef } from 'react';
import { Animated } from 'react-native';
import { useNavigation } from '@react-navigation/native';
type HeaderTitleProps = {
title?: string | React.ReactNode;
triggerPoint: number;
}
const useAnimatedHeaderTitle = ({ title, triggerPoint }: HeaderTitleProps) => {
const scrollY = useRef(new Animated.Value(0)).current;
const navigation = useNavigation();
useLayoutEffect(() => {
if(title) {
navigation.setOptions({ title })
}
}, [navigation, title])
useEffect(() => {
navigation.setOptions({
headerStyleInterpolator: () => {
const opacity = scrollY.interpolate({
inputRange: [triggerPoint, triggerPoint + 20],
outputRange: [0, 1]
});
return {
titleStyle: { opacity }
}
}
})
}, [navigation, scrollY])
return { scrollY };
}
export default useAnimatedHeaderTitle;
적용해보기
이제 useAnimatedHeaderTitle
훅을 실제로 어떻게 사용하는지 간단히 소개해 보겠습니다.
먼저, 이전에 작성했던 App.tsx 파일에서 HomeScreen 컴포넌트를 수정해보겠습니다. 방금 만든 훅을 불러와서, 헤더 타이틀로 사용할 title과 페이드 인이 시작될 기준점인 triggerPoint
를 인자로 넘겨주고, 반환되는 scrollY
값을 받아옵니다.
그다음 ScrollView의 onScroll
이벤트에 이 scrollY를 연결해주기만 하면 됩니다. 또한 scrollEventThrottle
값을 16으로 설정해주어 스크롤 이벤트가 적절한 빈도로 발생하도록 설정합니다.
import { View, Text, ScrollView } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
const HomeScreen = () => {
+ const { scrollY } = useAnimatedHeaderTitle({ title: 'Home', triggerPoint: 30 });
+ const handleScroll = Animated.event(
+ [ { nativeEvent: { contentOffset: { y: scrollY } } } ],
+ { useNativeDriver: false }
+ )
return (
<ScrollView
+ onScroll={ handleScroll }
+ scrollEventThrottle={16}
>
<View style={{ flex: 1, height: 1000 }}>
<Text style={{ fontSize: 20, fontWeight: 'bold', marginLeft: 16, paddingTop: 30 }}>
Home
</Text>
</View>
</ScrollView>
);
}
이제 스크롤을 내려보면, 지정한 triggerPoint를 넘어설 때 헤더 타이틀이 자연스럽게 페이드 인되는 모습을 확인할 수 있습니다.
마무리
이번 글에서는 스크롤 위치에 따라 헤더 타이틀이 부드럽게 나타나는 효과를 커스텀 훅으로 분리해보았습니다.
로직을 훅으로 분리해두면, 다른 화면에서도 동일한 애니메이션을 쉽게 재사용할 수 있고, 컴포넌트 구조도 훨씬 깔끔하게 유지할 수 있습니다.
예제에서는 ScrollView
를 기준으로 설명했지만, FlatList
등 다른 스크롤 가능한 컴포넌트에도 동일한 방식으로 적용할 수 있습니다.
간단하지만 사용자 경험에 확실한 차이를 줄 수 있는 인터랙션이니, 프로젝트에 도입해 더 매끄러운 UX를 만들어보세요!