React Native에서 스크롤 시 헤더의 일부가 자연스럽게 숨는 UI 구현

모바일 앱을 만들다 보면, 스크롤 시 헤더의 일부(예를 들어 안내 배너나 서브 타이틀 등)가 자연스럽게 위로 사라졌다가, 다시 스크롤을 최상단으로 올렸을 때 복귀하는 UI를 구현하고 싶은 경우가 있다. Animated API로 구현해본 내용 정리해본다.
김보람's avatar
Jul 14, 2025
React Native에서 스크롤 시 헤더의 일부가 자연스럽게 숨는 UI 구현

🏗️ 전체 구조

이 패턴은 상위 컴포넌트에서 스크롤 이벤트를 감지하여 헤더의 일부분(숨기고 싶은 영역)의 위치를 Animated.Value로 제어한다.

헤더 영역의 실제 높이는 onLayout을 이용해 동적으로 측정하며, 스크롤이 내려가면 해당 영역이 자연스럽게 위로 사라지고, 스크롤이 다시 최상단에 오면 복귀하도록 한다.


💻 AnimatedHeaderLayout 예시 코드

import {
  Animated,
  NativeScrollEvent,
  NativeSyntheticEvent,
  View,
} from 'react-native';
import { useRef, useState } from 'react';

export const AnimatedHeaderLayout: React.FC<{
  children: React.ReactNode;
  headerTitle: string;
  subTitle?: string;
  paddingBottom?: number;
}> = ({ children, headerTitle, paddingBottom = 123, subTitle }) => {
  const scrollY = useRef(new Animated.Value(0)).current;
  const prevScroll = useRef(0);
  const headerTextBoxVisible = useRef(true);
  const headerTextBoxY = useRef(new Animated.Value(0)).current;
  const [headerTextBoxHeight, setHeaderTextBoxHeight] = useState<number>(0);

  // 스크롤에 따라 헤더 일부분을 슬라이드로 숨기고 복귀시키는 로직
  const handleScroll = Animated.event(
    [{ nativeEvent: { contentOffset: { y: scrollY } } }],
    {
      useNativeDriver: true,
      listener: (event: NativeSyntheticEvent<NativeScrollEvent>) => {
        const current = event.nativeEvent.contentOffset.y;
        if (
          current > prevScroll.current &&
          current > 0 &&
          headerTextBoxVisible.current
        ) {
          // 아래로 스크롤: 텍스트 박스 숨김
          Animated.timing(headerTextBoxY, {
            toValue: -headerTextBoxHeight,
            duration: 200,
            useNativeDriver: true,
          }).start();
          headerTextBoxVisible.current = false;
        } else if (
          current <= 0 &&
          !headerTextBoxVisible.current
        ) {
          // 스크롤 맨 위: 텍스트 박스 복귀
          Animated.timing(headerTextBoxY, {
            toValue: 0,
            duration: 200,
            useNativeDriver: true,
          }).start();
          headerTextBoxVisible.current = true;
        }
        prevScroll.current = current;
      },
    }
  );

  return (
    <View style={{ flex: 1 }}>
      <HeaderComponent
        title={headerTitle}
        subTitle={subTitle}
        headerTextBoxY={headerTextBoxY}
        setHeaderTextBoxHeight={setHeaderTextBoxHeight}
      />
      <Animated.ScrollView
        onScroll={handleScroll}
        scrollEventThrottle={16}
        contentContainerStyle={{
          flexGrow: 1,
          paddingBottom: paddingBottom,
          paddingTop: headerTextBoxHeight,
        }}
      >
        {children}
      </Animated.ScrollView>
    </View>
  );
};

🧑‍💻 헤더 컴포넌트에서 숨길 영역 제어하기

헤더 컴포넌트에서는 숨기고자 하는 영역(텍스트 박스 등)에 Animated.View와 onLayout을 사용해서 실제 높이를 상위로 올려주고, props로 받은 Animated.Value를 이용해 위치를 제어한다.

import { Animated, LayoutChangeEvent, Text, View } from 'react-native';
import { Dispatch, SetStateAction } from 'react';

interface HeaderComponentProps {
  title: string;
  subTitle?: string;
  headerTextBoxY: Animated.Value;
  setHeaderTextBoxHeight: Dispatch<SetStateAction<number>>;
}

export const HeaderComponent = ({
  title,
  subTitle,
  headerTextBoxY,
  setHeaderTextBoxHeight,
}: HeaderComponentProps) => {
  // 숨겨질 박스의 높이를 onLayout으로 측정한다
  const onLayoutTextBox = (e: LayoutChangeEvent) => {
    setHeaderTextBoxHeight(e.nativeEvent.layout.height);
  };

  return (
    <View>
      {/* 상단 고정 영역(예: 네비, 기본 타이틀) */}
      <Animated.View
        onLayout={onLayoutTextBox}
        style={{
          transform: [{ translateY: headerTextBoxY }],
        }}
      >
        <Text style={{ fontSize: 20, fontWeight: 'bold' }}>{title}</Text>
        {subTitle && (
          <Text style={{ color: '#888', marginTop: 4 }}>{subTitle}</Text>
        )}
      </Animated.View>
    </View>
  );
};

💡 이 패턴의 핵심 포인트

  • 숨기고 싶은 영역(텍스트 박스 등)의 높이를 onLayout으로 동적으로 측정한다.

  • Animated.Value로 해당 영역의 위치를 translateY로 제어한다.

  • 스크롤을 내릴 때, 일정 위치를 넘으면 해당 영역이 자연스럽게 위로 사라진다.

  • 스크롤이 최상단에 오면 다시 자연스럽게 복귀한다.

  • 상위 컴포넌트에서는 스크롤 컨텐츠의 paddingTop을 headerTextBoxHeight로 지정해야 컨텐츠가 헤더 아래에 정확히 붙는다.


⚙️ 확장과 응용

  • 헤더의 전체 높이도 Animated.Value로 제어하면, “헤더가 넓어졌다 좁아졌다” 효과도 추가할 수 있다.

  • Animated.Value의 interpolate를 활용하면 투명도, 색상 등 다양한 효과를 줄 수 있다.


Share article

RN 삽질 일지