
SwiftUI에는 navigationTitle 이라는 것이 있다. 위 그림의 "요약"처럼 처음에는 큼지막하게 보이지만, 사용자가 스크롤 다운하면 보통의 네비게이션 헤더처럼 상단에 작게 표시되는 컴포넌트다. SwiftUI에서는 NavigationView 라는 View 구조체에 임베드된 View 구조체에 .navigationTitle()을 사용해서 정의하면, 위 그림과 같이 표시되게 된다.
이를 React Native에서 Animated 를 이용해 구현해보았다.
여러 검색을 해본 결과, 다음과 같은 방법이 유효할 듯 싶다.
이를 위해서는 일단, 사용자의 스크롤링 위치를 추적하기 위해 scrollY 라는 Animated.View 타입의 변수가 필요하다. 이 변수는 뷰의 리랜더링에 영향을 받으면 안되니, useRef를 이용해 정의해준다.
const scrollY = useRef(new Animated.Value(0)).current
사용자의 스크롤링 위치(offset)를 이 변수에 매핑하기 위해서는 ScrollView 의 onScroll 속성에 작업이 필요하다. onScroll 에는 Animated.event()의 반환값이 매핑되는데, 이를 위해서는 Animated.ScrollView가 필요하다.
<Animated.ScrollView
scrollEventThrottle={16}
onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], { useNativeDriver: true })}>
{largeHeader}
{children}
</Animated.ScrollView>
Animated.event 함수는 첫번째 파라미터에 정의된 값을 자동으로 setValue 함수를 통해 Animated.View 변수에 매핑시켜준다. 위의 경우, contentOffset.y의 값이 scrollY 변수에 매핑되게 된다.
이제 사용자 스크롤링 값에 따라 작은 헤더의 투명도를 변경해야 한다. 사용자의 스크롤링 값은 scrollY에 기록되고 있다. scrollY 값에 따라 특정한 투명도 값을 계산해야 한다. 이렇게 특정 Animated.Value 값의 변화에 따라 정해진대로 값을 계산해주는 함수는 interpolate 함수이다.
const headerOpacity = scrollY.interpolate({
inputRange: [0, headerHeight],
outputRange: [0, 1],
})
inputRange는 scrollY의 값을 의미한다. outputRange는 interpolate 함수의 결과값인 headerOpacity가 갖게될 값을 의미한다. 그래서 위의 코드를 해석하면, scrollY가 0일 때, headerOpacity는 0을 갖고, scrollY가 headerHeight 일 때, headerOpacity는 1을 갖는다. 0 ~ 1 사이에는 자동으로 부드럽게 애니메이션 처리가 된다. (계속 0이다가 갑자기 1로 커지는 것이 아니다)
그래서 이 headerOpacity 값을 작은 헤더의 style에 넣어주면 된다.
<Animated.View
style={{
height: headerHeight,
opacity: headerOpacity,
}}>
{smallHeader}
</Animated.View>
이로서 사용자의 스크롤링 위치에 따라 작은 헤더의 투명도가 0에서 1까지 조절된다. 사용자에게는 작은 헤더가 나타났다 사라졌다 하는 것으로 보일 것이다.

이 과정을 AnimatedHeader 라는 이름의 React 컴포넌트로 만들었는데, 전체 코드는 다음과 같다.
import React, { ReactNode, useRef } from 'react'
import { Animated } from 'react-native'
interface AnimatedHeaderProps {
children: ReactNode
smallHeader: ReactNode
largeHeader: ReactNode
headerHeight: number
}
function AnimatedHeader({
children,
smallHeader,
largeHeader,
headerHeight,
}: AnimatedHeaderProps): JSX.Element {
const scrollY = useRef(new Animated.Value(0)).current
const headerOpacity = scrollY.interpolate({
inputRange: [0, headerHeight],
outputRange: [0, 1],
})
return (
<>
<Animated.View
style={{
height: headerHeight,
opacity: headerOpacity,
}}
>
{smallHeader}
</Animated.View>
<Animated.ScrollView
scrollEventThrottle={16}
onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
useNativeDriver: true,
})}
>
{largeHeader}
{children}
</Animated.ScrollView>
</>
)
}
export default AnimatedHeader