안녕하세요. 음파를 개발하면서 피드를 만들었습니다.
인스타그램이나 네이버 블로그, 페이스북을 떠올리면 쉽게 이해할 수 있어요. 👀
첫번째로 FlatList
를 이용해서 간단하게 피드를 구현하겠습니다.
그 이후로 무한 스크롤을 하기 위한 방법을 알아보고, 위로 스크롤했을 때 Refresh하는 방법, 그리고 마지막으로 피드 상단에 스토리를 넣는 방법에 대해 알아보겠습니다.
ScrollView를 통해 구현할 수도 있지만, FlatList 컴포넌트를 이용하면 어떤 점이 좋을까요?
바로 최적화가 가능하다는 점입니다!
FlatList는 보통 많은 양의 데이터를 출력할 때 이용하는 컴포넌트로서, 현재 화면에 노출되는 즉, 눈에 보이는 아이템들을 렌더링해주기 때문에 속도도 빠릅니다.
그렇기에, 피드를 구현하기에 굉장히 안성맞춤입니다.
더욱 성능을 개선하기 위한 몇가지 방법들이 있는데, 이는 다음에 글로 다시 찾아 오겠습니다 !
많은 데이터를 받아오는 경우, FlatList를 통해 최적화를 하자
피드를 구성하는 컴포넌트들로 이루어지는 화면과 각 컴포넌트들을 먼저 살펴보겠습니다.
피드를 이루는 하나의 글을 담당하는 컴포넌트FeedSection
는 다음과 같습니다.
(이해할 수 있도록 간단하게만 작성하겠습니다.)
FeedSection.js
import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
const FeedSection = ({ title, content }) => {
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.content}>{content}</Text>
</VieW>
)
};
const styles=StyleSheet.create({
container: {
padding: 20
},
title: {
fontSize: 16,
fontWeight: 'bold'
},
content: {
fontSize: 13,
}
});
export default FeedSection;
딱 제목과 내용만 있는 간단한 피드입니다.
위의 컴포넌트로 이루어지는 피드 스크린을 구성하고, 더미 데이터를 이용해 만들어보겠습니다.
FeedScreen.js
import React from 'react';
import { FlatList, StyleSheet } from 'react-native';
import FeedSection from './FeedSection';
const FeedScreen = () => {
const data = [
{ title: '첫번째 제목', content: '첫번째 글' },
{ title: '두번째 제목', content: '두번째 글' },
{ title: '세번째 제목', content: '세번째 글' },
]
return (
<FlatList
data={data}
keyExtractor={(_) => _.title}
style={styles.container}
renderItem={({ item }) => {
const { title, content } = item;
return (
<FeedSection title={title} content={content}/>
)
})}
/>
)
}
const styles=StyleSheet.create({
container: {
flex: 1
}
})
무수히 많은 데이터를 한 번에 가져오는 것은 상당한 시간이 소요됩니다. 서버 쪽에 무리가 되는 일이지요. 그래서 보통 데이터를 가져올 때, 페이징 처리를 하곤 합니다.
인스타그램 피드를 내리다보면 어느 순간 글이 없고 로딩이 돌면서 새로운 글을 받아오는 경험을 해보신 적이 있나요? 🧐 🧐 🧐
이렇게 화면 하단에 위치했을 때, 데이터가 더 존재하지 않아 계속해서 추가적인 데이터를 받아오는 것을 무한 스크롤이라고 합니다.
그럼 여기서 핵심은? 🤷♂️
스크린이 화면 하단에 닿았을 때, 데이터를 받아오는 통신을 하는 것!
FlatList의 props중에는 여러가지가 있습니다. 지금은 무한 스크롤에 이용할 세 가지를 추가할텐데 onEndReached
와 onEndReachedThreshold
그리고 ListFooterComponent
입니다.
✓ onEndReached
이름 그대로 해석하면 이해하기 쉽습니다.
화면의 끝부분 (즉, 맨 아래)에 도달했을 때, 실행시킬 함수를 넣어줍니다.
✓ onEndReachedThreshold
onEndReached를 호출할 시점을 정해주는 것으로, 아래로부터 떨어진 정도에 따라 호출한다고 생각하면 됩니다.
0인 경우, 끝에 도달했을 때 호출하고 값이 1에 가까워질 수록, 끝에 도달하지 않아도 onEndReached를 실행하게 됩니다.
✓ ListFooterComponent
리스트의 Footer, 즉 아래쪽에 들어갈 컴포넌트에 대해서 다룹니다.
무한 스크롤이 동작하려면 화면이 아래에 닿아야 하고 이 때, 데이터를 받아오는 동안 로딩창이 돌아야 한다면? 🔄
바로 이 props을 이용해서 로딩 컴포넌트를 넣어주게 됩니다.
위의 피드 스크린에 props을 추가해보겠습니다.
DataFetch
는 서버로부터 데이터를 20개씩 받아오는 함수라 가정하겠습니다.
import { useState } from 'react';
import { ..., ActivityIndicator } from 'react-native';
const FeedScreen = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
if ( 데이터를 받아올 조건 ) {
setLoading(true);
await DataFetch();
setLoading(false);
}
}
cosnt onEndReached = () => {
if(!loading) {
getData();
}
}
...
<FlatList
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
ListFooterComponent={loading && <ActivityIndicator />}
/>
...
}
onEndReached
를 보면 loading이 false일 때, getData
함수를 호출하고 있습니다.
getData
함수를 보게 되면, 데이터를 받아올 조건이 성립하면 loading을 true로 바꾸고 함수 DataFetch
를 비동기로 호출합니다.
그렇기에 서버로부터 응답을 받기 전까지는 loading이 true인채로 남아 있게 됩니다.
ListFooterComponent
를 보면 loading이 true인 경우 활성화가 되는데,
ActivityIndicator
컴포넌트는 로딩바가 돌아가는 컴포넌트로서 이를 통해 데이터를 받아오는 동안 로딩창이 빙빙 돌게 됩니다.
그렇게 데이터를 받고 나면 loading이 false가 되면서 하단 로딩창이 사라지게 되고, 아래로 더 스크롤할 수 있게 되는 것입니다.
지금은 더미데이터를 이용 했지만 데이터 자체도 state를 이용한다면 바로 업데이트 되는 것이죠
if ( 데이터를 받아올 조건 )
이 부분이 중요한데, 현재 더 불러올 데이터가 없는 경우는 호출할 이유가 없기 때문에 조건문을 잘 설정해야 합니다.
저 같은 경우는, 20개씩 불러오기 때문에 데이터가 20개가 넘어야 하고 + 이전에 불러온 데이터도 20개여야 합니다.
즉, 데이터의 갯수 < 20 && 이전의 불러온 데이터의 갯수 != 20이면 성립하지 않는거죠.
// NotNextFeed = 이전의 데이터를 20개 못 받아오면 true
if(feed.length >= 20 && !notNextFeed)
인스타그램이나 블로그 등을 하다보면 맨 위에서 위로 올려서 새로운 피드들을 받아오는 경험을 한 적이 있나요? 새로운 데이터를 가져오거나 초기화를 다시 해주거나 하는 상황입니다.
무한 스크롤은 아래로 내리는 상황이었다면 지금은 그 반대입니다.
이 때, 필요한 props은 onRefresh
와 refreshing
입니다.
✓ onRefresh
위의 onEndReached를 생각해본다면 쉽게 정답을 찾을 수 있습니다.
바로, refresh하는 경우 (= 위로 스크롤을 하는 경우) 호출되는 함수를 넣어줍니다.
✓ refreshing
true인 경우 onRefreshing이 호출이 됩니다.
이번엔 참 쉽죠??,,
위와 비슷합니다. refresh시 서버와 통신하는 함수를 RefreshDataFetch
라 칭하겠습니다.
import { useState } from 'react';
const FeedScreen = () => {
const [refreshing, setRefreshing] = useState(false);
const getRefreshData = async () => {
setRefreshing(true);
await RefreshDataFetch();
setRefreshing(false);
}
cosnt onRefresh = () => {
if(!refreshing) {
getRefreshData();
}
}
...
<FlatList
onRefresh={onRefresh}
refreshing={refreshing}
/>
...
}
사실 거의 유사합니다. 다만 차이점은 무한 스크롤의 경우 페이징 하는 조건이 있었고, 이번에는 없다는 정도?
refreshing이 true로 바뀌면 RefreshDataFetch
를 비동기 호출 하게 되고, 이것이 완료되면 다시 false로 바꿉니다.
피드에 상단부에 스토리를 넣어주기 위해 관련된 컴포넌트와 어떻게 해야하는지 알아보도록 하겠습니다.
스토리도 FlatList를 이용해서 우선 만들겠습니다.
StorySection.js
import React from 'react'
import { Text, Image, StyleSheet } from 'react-native';
const StorySection = () => {
const storyData = [
{ name: 'peopleA', img: 'a-profile.png' },
{ name: 'peopleB', img: 'b-profile.png' },
{ name: 'peopleC', img: 'c-profile.png' },
]
return (
<FlatList
data={storyData}
keyExtractor={(_) => _._name}
horizontal
renderItem={({ item }) => {
const { name, img } = item
return (
<>
<Image source={{ uri: img }} style={styles.img} />
<Text style={styles.name}>{name}</Text>
</>
)
})}
/>
)
}
const styles=StyleSheet.create({
name: {
fontSize: 14
},
img: {
width: 40,
height: 40,
borderRadius: 40
}
})
혹시 FeedSection 과 다른점을 찾으셨나요? 바로 horizontal
이란 props이 추가되었습니다.
스토리는 옆으로 스크롤을 하기 때문에 horizontal 값을 true로 설정해야 합니다. 즉, 디폴트 값은 false였다는 뜻이겠죠?
FeedSection 을 다루는 FlatList 위에 이 StorySection 을 올리게 되면 어떤 일이 일어날까요?
아래처럼 말이죠.
import StorySection from './StorySection';
const FeedScreen = () => {
return (
<>
<StorySection />
<FlatList
data={data}
keyExtractor={(_) => _.title}
renderItem={({ item }) => {
...
})}
/>
</>
)
}
바로 스토리 영역만큼 height를 줘야하고 나머지 공간을 flex값을 채워 피드가 할당 받게 되는데, 이 때 스토리는 고정이라는 점입니다.
인스타그램을 생각해보면 피드를 내릴 때, 스토리도 함께 내려갑니다.
이것 때문에 한참 고민을 했었는데요. FlatList의 props에 바로 해결 방법이 있습니다.
ListHeaderComponent
props을 이용해봅니다!
✓ ListHeaderComponent
어디서 많이 비슷한 컴포넌트를 보지 않았나요? 바로 ListFooterComponent입니다.
차이점은 상단에 있는지, 하단에 있는지 입니다!
스토리는 상단에 존재해야 하니까 ListHeaderComponent를 이용하면 되겠죠?
바로, FlatList 상단에 스토리 컴포넌트를 넣어주기 때문에 스크롤을 해도 함께 이동하게 됩니다.
const FeedScreen = () => {
return (
<FlatList
data={data}
ListHeaderComponent={<StorySection />}
keyExtractor={(_) => _.title}
renderItem={({ item }) => {
...
})}
/>
)
}
import React, { useState } from 'react';
import { FlatList, StyleSheet, ActivityIndicator } from 'react-native';
import FeedSection from './FeedSection';
import StorySection from './StorySection';
const FeedScreen = () => {
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const data = [
{ title: '첫번째 제목', content: '첫번째 글' },
{ title: '두번째 제목', content: '두번째 글' },
{ title: '세번째 제목', content: '세번째 글' },
]
const getRefreshData = async () => {
setRefreshing(true);
await RefreshDataFetch();
setRefreshing(false);
}
cosnt onRefresh = () => {
if(!refreshing) {
getRefreshData();
}
}
const getData = async () => {
if ( 데이터를 받아올 조건 ) {
setLoading(true);
await DataFetch();
setLoading(false);
}
}
cosnt onEndReached = () => {
if(!loading) {
getData();
}
}
return (
<FlatList
data={data}
keyExtractor={(_) => _.title}
style={styles.container}
ListHeaderComponent={<StorySection />}
onRefresh={onRefresh}
refreshing={refreshing}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
ListFooterComponent={loading && <ActivityIndicator />}
renderItem={({ item }) => {
const { title, content } = item;
return (
<FeedSection title={title} content={content}/>
)
})}
/>
)
}
const styles=StyleSheet.create({
container: {
flex: 1
}
})
피드를 만드는 방법과 스토리를 함께 넣는 법, 추가적으로 무한 스크롤과 Refresh에 대해 정리했습니다:)