처음 배우는 리액트 네이티브

추가 : ScrollView 대신 FlatList로 무한 스크롤 구현

youbing 2025. 1. 31. 01:52

ScrollView 컴포넌트

  • 스크롤 가능한 영역을 생성할 수 있게 해줌.
  • default : 세로 스크롤, 스크롤바 제공
  • 주의 : 전체 자식 요소를 한 번에 렌더링하기 때문에, 자식 요소가 매우 많으면 성능 저하 발생 (대신, FlatList, SectionList 사용 가능)

FlatList 컴포넌트

  • ScrollView와 유사한 기능을 수행하며, 대량의 데이터를 효율적으로 표시하는 데 적합.
    • (필수) data : 렌더링할 데이터 배열
    • (필수) renderItem : 각 항목을 렌더링하는 함수, 각 아이템을 어떻게 표시할지 정의함.
      • 전달받는 props : item(객체), index(배열 인덱스 번호) 등
    • keyExtractor : 각 항목의 고유한 키를 설정해 성능 최적화
      • item, index를 props로 전달하고, 반환값으로 고유한 키 값을 필요로 함.
    • onEndReached : 리스트 끝에 도달했을 때 실행되는 콜백 (무한 스크롤에 유용)
    • onEndReachedThreshold : onEndReached가 호출되는 임계값(0~1)으로, 1에 가까울수록 더 빨리 호출됨.
      • 1(default) : FlatList 가장 하단 / 0.5 : FlatList의 중간 지점
    • initialNumToRender : 처음에 렌더링할 항목 개수 (성능 최적화)
    • extraData : data 외에도 변경을 감지해야 할 추가 상태 지정 (리렌더링 보장)
  • 렌더링 최적화 : 필요한 데이터만 화면에 표시하고, 보이지 않는 항목은 제거하여 성능 개선
  • 메모이제이션 지원 : keyExtractor를 통해 항목의 고유한 키를 설정하면 불필요한 리렌더링 방지
  • 불변성 요구 : 리스트 데이터가 변경될 경우 setState 등을 활용해 새로운 배열을 할당해야 반영됨.
  • 무한 스크롤 지원 : onEndReachedonEndReachedThreshold를 설정하면 스크롤이 끝에 도달했을 때 추가 데이터를 불러올 수 있음.
  • 성능 개선 옵션 : initialNumToRender, maxToRenderPerBatch, windowSize 등의 속성을 조정하면 렌더링 성능을 더욱 최적화할 수 있음.

간단한 사용 예제

const Mail = () => {
  const data = [
    { id: "1", name: "Yunji" },
    { id: "2", name: "Kitty" },
    { id: "3", name: "Lizzy" },
  ];

  const renderItem = ({ item }) => (
    <View>
      <StyledText>{item.name}</StyledText>
    </View>
  );

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

Paging(페이징)

  • 많은 데이터를 여러 개의 작은 단위(page)로 나누어 표시하는 기법

1. 페이지 번호 기반

  • 사용자가 페이지 번호를 선택하면 해당 페이지의 데이터를 가져오는 방식
  • 단점 : 특정 페이지를 불러올 때마다 새로운 요청이 필요해 UX가 다소 불편할 수 있음.

 

2. 무한 스크롤 기반(infinite Scroll)

  • 사용자가 스크롤을 내리면 자동으로 다음 데이터를 가져오는 방식
  • 단점 : 특정 위치로 이동하기 어렵고, 페이지를 새로고침하면 처음부터 다시 로드해야 하는 경우가 많음.

 

3. 오프셋 기반(offset Pagination)

  • limit(한 번에 가져올 데이터 수)과 offset(건너뛸 데이터 수)을 활용하여 요청
  • 예 : GET /items?limit=10&offset=20  => 21번째 데이터부터 10개씩 가져오기
  • 단점 : 데이터가 많을 경우, offset이 커지면 성능 저하될 수 있음.

 

4. 커서 기반(Cursor Pagination)

  • 특정 기준(예: id 또는 createdAt의 마지막 값)을 커서(cursor)로 사용하여 다음 데이터를 가져옴.
  • 예 : GET /items?limit=10&cursor=20  => id가 20번 이후의 데이터 10개씩 가져오기
  • 단점 : cursor를 관리해야 하며, 이전 페이지로 이동하는 기능이 어려움.

 

결론

페이지네이션 방식

  • 페이지 번호 기반 : 페이지 이동이 필요한 경우
  • 무한 스크롤 기반 : SNS처럼 연속적으로 데이터를 로딩하는 경우

 

API 요청 방식

  • Offset 기반 : 간단한 페이징 구현 가능하지만 성능 저하 가능
  • Cursor 기반 : 대규모 데이터 처리에 최적화되었지만 구현이 복잡

API를 이용한 예제

예시 이미지

npm install axios  # axios 사용을 위해 라이브러리 설치

 

 

파일 구조

app

ㄴ App.js

ㄴ screens

    ㄴ InfiniteFlatList.js : 메인 컴포넌트

    ㄴ SearchAlbum.js : TextInput 컴포넌트와 FlatList로 띄울 요소 1개의 컴포넌트 

ㄴ api

    ㄴ SearchAlbumAxios.js : axios 연동과 관련된 함수 정의

 

// App.js
export default App = () => <InfiniteFlatList />;
// InfiniteFlatList.js
import { useState } from "react";
import { getSearchList } from "../api/SearchAlbumAxios";
import { FlatList } from "react-native";
import { AlbumCard, SearchInput } from "./SearchAlbum";
import styled from "styled-components";

export const InfiniteFlatList = () => {
  const [albums, setAlbums] = useState([]);
  const [page, setPage] = useState(1);
  const [term, setTerm] = useState("");

  const getData = async (text) => {
    if (!term) setTerm(text); // 처음 검색했을 때
    if (term && term !== text) {
      // 재검색했을 때
      setTerm(text);
      setPage(1);
    }

    const result = await getSearchList(text, page);

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

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

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

const Container = styled.View`
  width: 90%;
  margin: 0 auto;
`;
// SearchAlbum.js
import React, { useState } from "react";
import styled from "styled-components";

export const SearchInput = ({ callbackSearch }) => {
  const [text, setText] = useState("");

  return (
    <StyledTextInput
      value={text}
      onChangeText={setText}
      placeholder="검색할 가수명"
      returnKeyType="search"
      onSubmitEditing={() => callbackSearch(text)}
    />
  );
};

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

  return (
    <Wrapper>
      <ArtWork source={{ uri: artworkUrl100 }} />
      <ArtInfo>
        <StyledText>{collectionName}</StyledText>
        <StyledText>{artistName}</StyledText>
      </ArtInfo>
    </Wrapper>
  );
};

const StyledTextInput = styled.TextInput`
  width: 100%;
  height: 50px;
  background-color: #323232;
  color: white;
  border-radius: 20px;
  padding: 0px 20px;
`;

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 ArtInfo = styled.View``;

const StyledText = styled.Text`
  font-size: 13px;
  font-weight: 300;
`;
// SearchAlbumAxios.js
import axios from "axios";

// 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 getSearchList = async (term, page) => {
  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) {
    // console.log(error);
    throw error;
  }
};

 

 

 

본문에서 설명하는 내용은 https://james-sleep.tistory.com/6를 참고하였습니다.