본문 바로가기

React Native

React Native 무한스크롤

React Native에서 <ScrollView>를 이용해 많은 양의 데이터를 표현할때 랜더링 속도가 현저히 느려집니다. 이 문제 해결을 위해 Instagram의 Feed처럼 무한 스크롤을 구현합니다.

 

Example

https://github.com/JamesSleep/InfiniteScroll

 

GitHub - JamesSleep/InfiniteScroll: React Native를 이용한 무한스크롤 구현

React Native를 이용한 무한스크롤 구현. Contribute to JamesSleep/InfiniteScroll development by creating an account on GitHub.

github.com

 

<ScrollView> vs <FlatList>

ScrollView · React Native

 

ScrollView · React Native

Component that wraps platform ScrollView while providing integration with touch locking "responder" system.

reactnative.dev

React Native 공식문서에서는 ScrollView 는 모든 하위 컴포넌트를 한번에 랜더링하기 때문에 퍼포먼스가 떨어진다고 설명하고 있습니다.

FlatList 를 사용하면 메모리와 처리시간을 절약하고 여러항목에 대한 랜더링과 무한 스크롤 등에 편리하다고 합니다.

 

FlatList

Usage

import React from 'react';
import { View, Text, FlatList } from 'react-native'; 

const DATA = [
	{ id: '1', name: 'David' },
	{ id: '2', name: 'Johnson' },
	{ id: '3', name: 'Jason' },
];

const Item = ({ title }) => (
	<View>
		<Text>{title}</Text>
	</View>
);

const App = () => {
	const renderItem = ({ item }) => (
		<Item title={item.title} />
	);

	return (
		<FlatList
			data={DATA}
			renderItem={renderItem}
			keyExtractor={item => item.id}
		/>
	)
}

기본적인 FlatList 사용법 입니다.

FlatList는 Child Components를 감싸는 <ScrollView>와는 다르게 any[ ] 타입의 data, 해당 데이터를 표현할 renderItem, item별로 unique key(type: string) 값을 부여하는 keyExtractor등의 props를 가지고 있습니다.

Pagination or Paging

구현단계 이전에 API 페이지네이션(페이징) 개념에 대해 알아보겠습니다. 전체적인 API Request 단계에서 프론트엔드와 마찬가지로 백엔드에서도 많은 양의 데이터를 DB에서 가져오기 때문에 제한된 호출이 필요합니다.

MySQL로 예를들어 보겠습니다. MySQL에서는 SELECT 쿼리를 사용할때 LIMIT, OFFSET 키워드를 이용하여 페이징처리가 가능합니다.

// 숫자만큼의 행 출력
SELECT * FROM 테이블명 LIMIT 10; 

// 몇번째 행부터 출력할것인지, offset이 0이면 1번째행부터
SELECT * FROM 테이블명 LIMIT 10 OFFSET 0; 
SELECT * FROM 테이블명 LIMIT 10 OFFSET 10;

이 기능을 이용해 백엔드 개발자는 API에 쿼리문을 추가하여 프론트엔드 개발자에게 공유합니다.

https://url?limit=10&offset=10

https://url?limit=10&start=10

API를 이용한 데이터 랜더링

웹, 앱을 개발하는 프론트엔드 개발자의 경우 pagination 혹은 paging이 되어있는 API를 사용하는 것이 아니라면 구현이 힘들어 백엔드 개발자의 도움이 필요합니다. Request Query에 최소한 limit와 같은 제한된 데이터를 요청할 수 있는 옵션이 필요합니다.

이 문서에서는 iTunes 무료 API를 이용하여 가수를 검색했을때 관련된 앨범목록을 받아오는 리스트를 구현해보겠습니다.

https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/index.html#//apple_ref/doc/uid/TP40017632-CH3-SW1

 

iTunes Search API: Overview

 

developer.apple.com

우선 API관련 코드를 작성해보겠습니다. Response 데이터 구조는 위 링크 및 GitHub Example 코드 참고바랍니다.

import axios, { AxiosError } from 'axios';

axios.defaults.headers.common = {
  'Content-Type': 'application/json',
};
axios.defaults.baseURL = 'https://itunes.apple.com';

const params = {
  country: 'KR',
  media: 'music',
  entity: 'album',
  attribute: 'genreIndex',
};

export const getSerachList = async (
  term: string,
  page?: number,
): Promise<iTunesMusic[]> => {
  try {
    const { data } = await axios.get('/search', {
      params: { term, ...params, limit: page ? page * 10 : 10 },
    });

    console.log(JSON.stringify(data.results, null, 2));

    return data.results;
  } catch (error) {
    const { response } = error as unknown as AxiosError;

    if (response) {
      throw { status: response.status, data: response.data };
    }

    throw error;
  }
};

가수명을 입력받을 검색창을 만들어줍니다.

import React, { useState } from 'react';
import { TextInput } from 'react-native';
import styled from 'styled-components/native';
import { AppColor } from '../theme/myTheme';

interface ISearchInput {
  callbackSearch: (term: string) => void;
}

const Input = styled(TextInput)`
  width: 100%;
  height: 50px;
  background-color: #323232;
  border-radius: 20px;
  padding: 0px 20px;
  color: ${AppColor.font};
`;

export const SearchInput = ({ callbackSearch }: ISearchInput) => {
  const [text, setText] = useState<string>('');

  return (
    <Input
      value={text}
      onChangeText={setText}
      placeholder="가수명을 입력해주세요..."
      placeholderTextColor="#848484"
      returnKeyType="search"
      onSubmitEditing={() => callbackSearch(text)}
    />
  );
};

FlatList로 랜더링할 컴포넌트를 작성합니다. 앨범사진, 앨범이름, 가수명 세가지의 정보만을 사용하겠습니다.

import React from 'react';
import { Image, Text, View } from 'react-native';
import styled from 'styled-components/native';
import { WIDTH } from '../constant/size';
import { iTunesMusic } from '../helper/api';
import { AppColor } from '../theme/myTheme';

interface IAlbumCard {
  album: iTunesMusic;
}

const Wrapper = styled(View)`
  width: 100%;
  height: 80px;
  flex-direction: row;
  align-items: center;
  margin: 10px 0px;
`;

const ArtWork = styled(Image)`
  width: 75px;
  height: 75px;
  border-radius: 5px;
  margin-right: 10px;
`;

const AlbumInfo = styled(View)``;

const AlbumTitle = styled(Text)`
  color: ${AppColor.font};
  font-size: ${WIDTH * 0.04}px;
  font-weight: 600;
  margin-bottom: 10px;
`;

const AlbumArtist = styled(Text)`
  color: ${AppColor.font};
  font-size: 13px;
  font-weight: 300;
`;

export const AlbumCard = ({ album }: IAlbumCard) => {
  const { artworkUrl100, artistName, collectionName } = album;

  const trimText = (text: string, limit: number) =>
    text.length > limit ? `${text.slice(0, limit)}...` : text;

  return (
    <Wrapper>
      <ArtWork source={{ uri: artworkUrl100 }} />
      <AlbumInfo>
        <AlbumTitle>{trimText(collectionName, 26)}</AlbumTitle>
        <AlbumArtist>{trimText(artistName, 26)}</AlbumArtist>
      </AlbumInfo>
    </Wrapper>
  );
};

FlatList를 구현해줍니다. 우선 검색 후 10개의 데이터만을 받아보겠습니다.

import React, { useState } from 'react';
import { FlatList } from 'react-native';
import { AlbumCard } from '../../components/albumCard';
import { SearchInput } from '../../components/searchInput';
import { getSerachList, iTunesMusic } from '../../helper/api';
import { Container } from '../../theme/styles';

export const InfiniteFlatList = () => {
  const [albums, setAlbums] = useState<iTunesMusic[]>([]);
	const [page, setPage] = useState<number>(1);

  const getData = async (text: string) => {
    const result = await getSerachList(text, page);

    if (result.length === albums.length) return;

    setAlbums(result);
  };

  return (
    <Container>
      <SearchInput callbackSearch={getData} />
      <FlatList
        style={{ width: '100%' }}
        data={albums}
        renderItem={({ item, index }) => <AlbumCard key={index} album={item} />}
        keyExtractor={(item, index) => index.toString()}
      />
    </Container>
  );
};

데이터를 잘 받아오는게 확인되었다면 이제 페이징 기능을 추가하겠습니다.

<FlatList
  style={{ width: '100%' }}
  data={albums}
  renderItem={({ item, index }) => <AlbumCard key={index} album={item} />}
  keyExtractor={(item, index) => index.toString()}
  onEndReached={() => getData(term)}
  onEndReachedThreshold={1}
/>

onEndReached는 스크롤이 랜더링된 콘텐츠 내의 아래쪽 가장자리에 닿을때 발생하는 이벤트입니다.

onEndReachedThreshold는 onEndReached 콜백을 트리거하는 시점을 정합니다. 콘텐츠 맨 가장자리를 기준으로 0~1 사이의 값을 입력합니다. 0에 가까울수록 스크롤을 완전히 내렸을때 트리거가 발생합니다.

검색 후 Response된 10개의 데이터 중 첫번째 값은 “First Winter - Single” 마지막 값은 “Crash Landing on You”입니다.

왼쪽이미지는 스크롤이 최상단에 있고 오른쪽은 스크롤이 가장 아래로 내려간 상태입니다.

왼쪽사진을 기준으로 화면 가장아래가 { 1 }, 오른쪽사진의 화면 가장아래를 { 0 } 으로 생각하시면됩니다.

기기의 크기, 랜더링된 컴포넌트의 총 길이에 관계없이 스크롤이 시작되는 시점을 { 1 }, 스크롤이 끝나는 시점을 { 0 } 이라고 생각하시고 onEndReachedThreshold값을 지정해주시면 됩니다.

 

이제 트리거를 지정했으니 데이터를 추가로 불러오는 코드를 작성해보겠습니다.

const getData = async (text: string) => {
    const result = await getSerachList(text, page);

    if (result.length === albums.length) return;

    setAlbums(result);
    setPage(page + 1);
 };

itunes API에 페이징관련 쿼리값은 없지만 limit를 이용해 스크롤시 추가로 데이터를 불러오도록 작성했습니다.

const { data } = await axios.get('/search', {
    params: { term, ...params, limit: page ? page * 10 : 10 },
});

FlatList가 완성되었습니다.