처음 배우는 리액트 네이티브
추가 : 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 등을 활용해 새로운 배열을 할당해야 반영됨.
- 무한 스크롤 지원 : onEndReached와 onEndReachedThreshold를 설정하면 스크롤이 끝에 도달했을 때 추가 데이터를 불러올 수 있음.
- 성능 개선 옵션 : 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를 이용한 예제
- 사용한 API : iTunes 무료 API(https://developer.apple.com/library/archive/documentation/...)
- 목표 : 가수를 검색했을 때 관련된 앨범목록을 받아오는 리스트
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를 참고하였습니다.