[React native] FlatList 자동 스크롤 슬라이더 / 캐러셀 구현하기
Code/React-Native

[React native] FlatList 자동 스크롤 슬라이더 / 캐러셀 구현하기

반응형

오늘의 결과물

 

회사 앱이 런칭되고 나서 react native에서 캐러셀을 활용한 디자인을 구현해야 하는 상황이 자주 발생했다.

참으로 다양한 캐러셀과 슬라이더를 만들었는데 이번에 메인 페이지 개편 작업을 하며 5가지 종류의 새로운 캐러셀을 추가로 작업했다.

가만히 생각해보니 리액트 네이티브를 만진 지 얼마 되지 않았을 때.. 잡힐 듯 잡히지 않는 듯한 FlatList로 캐러셀과 슬라이더를 구현하느라 상당히 진땀을 뺐던 기억이 났다.

캐러셀 자체를 만드는 것은 어렵지 않았으나 추가로 들어가는 로직이라던가 슬라이더의 전체 스타일을 잡는 과정에서 마음대로 되지 않았던 문제가 있었다. 그 당시에는 pm과 기획의 부재로 구두상으로 계속 디자이너님께 물어보며 재량껏 구현을 해야 했던 상황이라 더 고되었던 것으로 기억하는 것 같기도 하다.

예로 들자면, FlatList의 스크롤 양 끝에 여백을 주고 싶은데 style 프롭스에 여백을 설정하면 마지막 카드가 잘린다던가 카드 사이 여백이 안 잡혀서 여기저기 flex를 써봤다던가, 현재 스크롤의 오프셋을 어떻게 감지하고 어떻게 관리를 해서 어떤 식으로 보여줄 것인지 정도의 문제가 있었다. 

그래서 오늘은 react native에서 캐러셀이나 슬라이더를 구현해야 하는 상황이 왔을 때 FlatList의 기본 동작과 구조를 파악하면서

 그 어떤 디자인이 와도 이 내용을 바탕으로 무한 활용하여 막힘없이 구현할 수 있도록 3가지 정도의 핵심 키워드를 가지고 구현과정을 서술하고자 한다.

 

구현 내용

-  기본 캐러셀 / 슬라이더 구현 (slider / carousel)

-  페이지네이션 / 오프셋 설정 (scroll offset / pagination )

-  자동 스크롤 애니메이션 (auto scroll slider / carousel animation)

 

 

1. 기본 캐러셀 / 슬라이더 구현 (slider / carousel)

우선 디자인의 구조를 먼저 파악해보겠습니다.

 

캐러셀의 스크롤 양 끝에는 padding 값이 24가 들어가고, 카드의 width는 디스플레이 width에서 24 * 2를 뺀 값이고, 각 카드 사이에 들어갈 margin의 값은 12로 디자인되어있습니다.

const windowWidth = Dimensions.get('window').width;
const margin = 12;

const cardSize = {width: windowWidth - 24 * 2, height: 400};

해당 값을 상수로 설정하여 구현해보겠습니다.

 

export default function Carousel() {
  const data = useMemo(
    () => [
      {
        mainImageUrl:
          '...',
      },
      ...,
    ],
    [],
  );

  return (
    <View style={styles.container}>
      <Text style={{fontSize: 20, textAlign: 'center'}}>딤문의 개발일기</Text>
      <FlatList
        data={data}
        horizontal
        contentContainerStyle={{paddingHorizontal: 24}}
        renderItem={({item}) => (
          <TouchableOpacity style={{marginRight: margin}}>
            <ImageBackground
              style={cardSize}
              source={{uri: item.mainImageUrl}}
            />
          </TouchableOpacity>
        )}
        keyExtractor={(_, index) => String(index)}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    width: windowWidth,
    backgroundColor: '#fff',
    paddingTop: 100,
  },
});

우선 mainImageUrl 키를 갖고 있는 객체를 요소로하는 배열을 FlatList의 data의 프롭스로 전달합니다.

가로 형태로 컨텐츠의 정렬이 필요하기 때문에 horizontal props의 값을 truthy하게 설정,

contentContainerStyle 프롭스에 캐러셀 내부에 설정하길 희망했던 padding 값을 부여합니다.

- style 프롭스에 값을 설정할 경우 FlatList 자체의 스타일의 설정으로 감지, contentContainerStyle 을 통해 설정을 해야 스크롤 내부의 스타일을 설정할 수 있음

renderItem props에는 디자인되어있는 카드의 사이즈와 함께 카드 사이마다 필요한 margin값을 부여한 컴포넌트를 리턴할 수 있도록 합니다.

 

- 구현 결과

기본 카드 형태의 캐러셀 완성

기본 카드 형태의 캐러셀 / 슬라이더가 완성되었습니다.

여기까지 진행한 경우 카드가 나열된 긴 스크롤일 뿐, pagination / snap시 카드 위치 자동 조정등은 작동하지 않는 상태입니다.

반응형

2. 페이지네이션 / 오프셋 설정 (scroll offset / pagination )

이제 스크롤을 했을 때 페이지네이션이 가능하도록 오프셋 설정을 해봅시다.

const windowWidth = Dimensions.get('window').width;
const margin = 12;

const cardSize = {width: windowWidth - 24 * 2, height: 400};
const offset = cardSize.width + margin;

export default function Carousel() {
   const data = useMemo(
    () => [
      {
        mainImageUrl:
          '...',
      },
      ...,
    ],
    [],
  );
  
  const snapToOffsets = useMemo(() => Array.from(Array(data.length)).map((_, index) => index * offset),
  [data],
  );
  
  return (
    <View style={styles.container}>
      <Text style={{fontSize: 20, textAlign: 'center'}}>딤문의 개발일기</Text>
      <FlatList
        ...
        data={data}
        snapToOffsets={snapToOffsets}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    width: windowWidth,
    backgroundColor: '#fff',
    paddingTop: 100,
  },
});

FlatList의 snapToOffsets 프롭스는 유효한 오프셋 값들을 배열의 형태로 받습니다.

우선 images의 각 요소를 카드 한 개라고 생각하고, 모든 카드들에 대해 오프셋의 범위를 설정하기를 원하는 상태입니다.

그리고 스크롤을 했을 때 카드가 한 개씩 이동했으면 하기때문에 이를 바탕으로 images의 길이만큼 배열을 만들고, index * offset으로 값을 넣어줍니다.

그렇게 되면, 이제 스크롤이 끝났을 때 카드가 알아서 제 위치를 찾아가게 됩니다.

 

3. 자동 스크롤 애니메이션 (auto scroll slider / carousel animation)

이제 자동 스크롤을 구현할 차례입니다.

- useInterval.ts

import {useEffect, useRef} from 'react';
type Callback = {(): void};

function useInterval(callback: Callback, delay: number | null) {
  const savedCallback = useRef<Callback>(() => {});

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

export default useInterval;

 

- Carousel.tsx

...
import useInterval from './useInterval';

... 
export default function Carousel() {
  const [currentIndex, setCurrentIndex] = useState(0);
  const flatListRef = useRef<FlatList>(null);

  ...,


  useEffect(() => {
    if (currentIndex !== snapToOffsets.length) {
      flatListRef.current?.scrollToOffset({
        animated: true,
        offset: snapToOffsets[currentIndex],
      });
    }
  }, [currentIndex, snapToOffsets]);

  useInterval(() => {
    setCurrentIndex(prev => (prev === snapToOffsets.length - 1 ? 0 : prev + 1));
  }, 2400);

  return (
    <View style={styles.container}>
      <Text style={{fontSize: 20, textAlign: 'center'}}>딤문의 개발일기</Text>
      <FlatList
      	...
        ref={flatListRef}
      />
    </View>
  );
}

const styles = ...

currentIndex 라는 state를 통해 현재 카드의 인덱스를 저장하고, flatListRef를 통해 currentIndex state가 변경될 때마다 자동으로 스크롤을 할 수 있도록 처리했습니다.

그리고 useInterval 훅스를 통해 2400ms마다 스크롤이 변경될 수 있도록 setCurrentIndex를 호출합니다. 

예제의 경우 가장 마지막 카드에 도착한 경우 다시 첫번째 카드로 이동할 수 있도록 구현했습니다.

 

- 구현 완료!

(슬라이드 dot 부분은 따로 예제에 넣지 않았습니다. currentIndex를 통해 구현 가능)

 

이렇게 기본적인 오프셋 오토 스크롤(offset auto scroll animation)이 가능한 캐러셀(carousel) 및 슬라이더(slider) 구현이 가능합니다.

 

 

반응형