[React-Native] 유튜브 API 를 활용하여 무한 스크롤 구현하기 (feat. A to Z )
개요 ⏰
이번 포스트는 유튜브 API 를 활용하여
계속해서 렌더링되는 데이터들을 무한스크롤을 통해 React-Native
환경에서 구현 해 볼 예정이다.
init 🌅
먼저 /
루트 경로에서 아래 명령을 실행하여 React-Native
프로젝트를 생성 해 준다.
npx react-native init [프로젝트명]
생성된 프로젝트는 yarn
혹은 npm
명령을 사용하여
안드로이드 스튜디오 애뮬레이터를 통해 실행할 수 있다.
yarn run android
or
npm run android
초기 실행 시 앱을 애뮬레이터에 설치하는 과정이 존재하므로,
시간이 다소 소요될 수 있다.
App.tsx 🚢
생성된 프로젝트를 실행하여 보면 아래와 같은 화면을 볼 수 있는데,
이는 최 상위 부모 컴포넌트로 App.tsx
를 불러와 화면에 띄워주는 방식인데,
별도의 컴포넌트를 생성하여 import 해 주는 방식으로 렌더링 하면 된다.
Components 🚀
/
루트 경로에 /src
디렉토리를 생성하여 필요한 컴포넌트를 제작하려고한다.
TypeListItem
컴포넌트를 제작하기에 앞서,
클리이언트에서 사용할 데이터들의 타입을 먼저 선언해주려한다.
// Type 정의
export type TypeListItem = {
title: string // 제목 텍스트
thumbnail: string // URL
publishedAt: string // ex) "2022-06-15T12:31:57Z"
viewCount: number // 조회수
channelTitle: string // 채널명
}
ListItemView
함수형 컴포넌트(FC)
를 제작하여 item
의 정보를 화면에 출력하는 컴포넌트이다.
import React from "react";
import { TypeListItem } from "./TypeListItem";
import { Image, Text, View } from "react-native";
export const ListItemView: React.FC<{ item: TypeListItem }> = (props) => {
return (
<View>
<Image style= source= />
<View
style=
>
<Text style=>{props.item.title}</Text>
<Text style=>
{/* 특수문자 ' · ' */}
{props.item.channelTitle} · 조회수 {props.item.viewCount} /{" · "}
{props.item.publishedAt}
</Text>
</View>
</View>
);
};
인라인형태로 스타일을 지정한게 마크다운 문법에 적용이 되지 않는다.. 😭
props
를 매개변수로 받고,
그 안에 정의된 item
은 TypeListItem
의 타입을 따른다.
높이가 200 으로 고정된 URI
는,
props.item.thumbnail
로 부터 이미지 URL 을 가져와 화면에 표시한다.
ListView (더미)
해당 컴포넌트는,
더미데이터를 가진 배열을 FlatList
를 활용하여
각 항목을 ListItemView
를 통해 렌더링하는 컴포넌트이다.
import React, {useState} from 'react';
import {TypeListItem} from './TypeListItem';
import {FlatList} from 'react-native';
import {ListItemView} from './ListItemView';
export const ListView: React.FC = () => {
const [list] = useState<TypeListItem[]>([
{
title: 'TITLE_01',
thumbnail:
'https://docs.expo.dev/static/images/tutorial/background-image.png',
publishedAt: '2023-08-22',
viewCount: 100,
channelTitle: 'CHANNEL TITLE 01',
},
{
title: 'TITLE_02',
thumbnail:
'https://docs.expo.dev/static/images/tutorial/background-image.png',
publishedAt: '2023-08-22',
viewCount: 200,
channelTitle: 'CHANNEL TITLE 02',
},
{
title: 'TITLE_03',
thumbnail:
'https://docs.expo.dev/static/images/tutorial/background-image.png',
publishedAt: '2023-08-22',
viewCount: 300,
channelTitle: 'CHANNEL TITLE 03',
},
]);
return (
<FlatList
data={list}
renderItem={({item}) => <ListItemView item={item} />}
/>
);
};
useState
훅을 사용하여 list
의 상태를 초기화 한다.
이 상태의 초기 값에는 TypeListItem[]
타입의 배열로, 목록에 표시할 데이터를 담고 있다.
각 객체는 title
, thumbnail
, publishedAt
, viewCount
, channelTitle
속성을 가진다.
FlatList
컴포넌트를 사용해서 정의한 데이터를 렌더링한다.
data
에는 FlatList
가 렌더링 할 데이터(list
) 를 지정한다.
renderItem
에는 각 아이템을 어떻게 렌더링 할 것인지를 지정하며,
여기서는 ListItemView
컴포넌트를 사용한다.
App.tsx
불필요한 코드를 제거한 상태로 SafeAreaView
영역 내부에
더미 데이터가 담긴 ListView
컴포넌트를 불러온다.
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
*/
import React from "react";
import { SafeAreaView, StatusBar, useColorScheme } from "react-native";
import { Colors } from "react-native/Libraries/NewAppScreen";
import { ListView } from "./src/ListView";
function App(): JSX.Element {
const isDarkMode = useColorScheme() === "dark";
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};
return (
<SafeAreaView style={backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? "light-content" : "dark-content"}
backgroundColor={backgroundStyle.backgroundColor}
/>
<ListView />
</SafeAreaView>
);
}
export default App;
useData
Youtube API 를 사용하여,
컴포넌트에 더미가 아닌 실제 데이터를 불러오기 위한 컴포넌트이다.
ListView
컴포넌트에서 불러오는 더미데이터를 모두 지우고,
const { data, loadData } = useData();
해당 코드를 입력하여 실 데이터를 불러온다.
data
와loadData
는 커스텀 훅을 사용하여 가져옴 ❗️
loadData
는 useEffect
를 사용하여 호출하고, 의존성 배열에도 추가.
- ListView.tsx
import React, { useEffect } from "react";
import { TypeListItem } from "./TypeListItem";
import { FlatList } from "react-native";
import { ListItemView } from "./ListItemView";
import { useData } from "./useData";
export const ListView: React.FC = () => {
const { data, loadData } = useData();
useEffect(() => {
loadData();
}, [loadData]);
return (
<FlatList
data={data}
renderItem={({ item }) => <ListItemView item={item} />}
/>
);
};
- useData.ts
axios
라이브러리를 사용하여
Youtube API 를 요청하고, 데이터를 화면에 보여주는 로직을 구현하려한다.
npm install axios or yarn add axios
https://console.cloud.google.com/apis/api/youtube.googleapis.com/metrics?hl=ko&project=test-467a4
위 경로로 접근하여 Youtube Data API v3
를 사용할 수 있는 API 키 🔑 를 발급받는다.
import { useCallback, useState } from 'react'
import { TypeListItem } from './TypeListItem'
import axios from 'axios'
// 동일한 config 값 들을 가지고있는 axiosInstance
const axiosInstance = axios.create({
baseURL: 'https://www.googleapis.com/youtube/v3/', // 주로 서버 URL == host
})
const API_KEY = 'api_key'
export const useData = () => {
const [ data, setData ] = useState<TypeListItem[]>([])
const loadData = useCallback(async() => {
try {
// baseURL 로 get 요청 => '/video' 엔드포인트로 요청, 파라미터로는 호출할 때 사용되는 config(매개변수) 값
const videoResults = await axiosInstance.get<{
kind: 'youtube#videoListResponse';
etag: string;
nextPageToken: string;
prevPageToken: string;
pageInfo: {
totalResults: number;
resultsPerPage: number;
};
items: {
kind: 'youtube#video';
etag: string;
id: string;
snippet: {
publishedAt: string;
channelId: string;
title: string;
description: string;
thumbnails: {
[key: string]: {
url: string;
width: number;
height: number;
};
};
channelTitle: string;
tags: string[];
categoryId: string;
};
contentDetails: {
duration: string;
dimension: string;
definition: string;
caption: string;
licensedContent: boolean;
regionRestriction: {
allowed: [string];
blocked: [string];
};
contentRating: {
mpaaRating: string;
tvpgRating: string;
bbfcRating: string;
chvrsRating: string;
eirinRating: string;
cbfcRating: string;
fmocRating: string;
icaaRating: string;
acbRating: string;
oflcRating: string;
fskRating: string;
kmrbRating: string;
djctqRating: string;
russiaRating: string;
rtcRating: string;
ytRating: string;
};
};
statistics: {
viewCount: number;
likeCount: number;
dislikeCount: number;
favoriteCount: number;
commentCount: number;
}
}[]
}>('/videos', {
params: {
key: API_KEY,
part: 'snippet, contentDetails, statistics',
chart: 'mostPopular',
// 국가코드
regionCode: 'KR',
},
})
const videoData = videoResults.data
setData(videoData.items.map(( item ) => ({
title: item.snippet.title,
thumbnail: item.snippet.thumbnails.high.url,
publishedAt: item.snippet.publishedAt,
viewCount: item.statistics.viewCount,
channelTitle: item.snippet.channelTitle,
})))
} catch(ex) {
console.log(ex)
}
}, [])
return {
data,
loadData,
}
}
해당 API 를 사용하기 위해 key
값을 매개변수로 넘겨주고,
필수 파라미터로 정의되어있는 part
를 같이 넘겨준다.
필수로 넘겨주는 매개변수에는 여러 속성이 존재하는데,
snippet
, contentDetails
, statistics
을 지정해준다.
요청을 하게되면 데이터가 EN
으로 들어오기 때문에
한국 데이터를 불러오기 위해 국가코드(regionCode
) 를 지정해주었다.
ListView (최종)
Custom Hook
을 통해 데이터를 불러오고,
FlatList
를 사용하여 데이터를 렌더링하는 컴포넌트이다.
사용자가 스크롤을 하여 출력된 ListItem
목록의 끝에 도달하면 loadMoreData
함수가 호출된다.
import React, { useEffect } from "react";
import { TypeListItem } from "./TypeListItem";
import { FlatList } from "react-native";
import { ListItemView } from "./ListItemView";
import { useData } from "./useData";
export const ListView: React.FC = () => {
const { data, loadData, loadMoreData } = useData();
useEffect(() => {
loadData();
}, [loadData]);
return (
<FlatList
data={data}
renderItem={({ item }) => <ListItemView item={item} />}
onEndReached={loadMoreData}
// 마지막에 가까운 지점
onEndReachedThreshold={0.1}
/>
);
};
useData
라는 custom hook
을 호출하여 각 인수들을 가져온다.
data
는 목록에 표시할 데이터가 정의되어있고,
loadData
는 데이터를 초기로 로드하는 함수이다.
loadMoreData
는 이후 컴포넌트에서 정의할 함수인데,
스크롤이 끝난 경우 새로운 데이터를 불러오는 로직이 담겨있는 함수이다.
useEffect
훅을 사용하여,
컴포넌트가 마운트 될 때 loadData
함수를 호출하여 초기 데이터를 로드한다.
FlatList
에는 데이터를 어떤 형태로 가져올 것인지 파라미터를 지정해주는데,
data
는 표시할 데이터,
renderItem
은 각 항목을 어떻게 렌더링 할 것인지,
onEndReached
는 목록의 끝에 도달했을 때 호출될 함수,
onEndReachedThreshold
목록 끝에 얼마나 가까워야 onEndReached
함수가 호출될지 정하는 임계값 ? 이다.
FIX 🔨
다음과 같이 Youtube API 를 사용하여 실 데이터를 화면에 출력을 했지만,
스크롤이 끝난 경우,
계속되는 스크롤을 통해 새로운 데이터를 reload
하여 불러오는것을 목표로 했었다 💦
하지만 위 이미지를 보면,
스크롤이 끝난 경우 새로운 데이터를 불러오기는 하지만,
이전 데이터를 지우고 새로운 데이터가 불러와 지는 형태로 구현이 되어있다.
Concat
concat
은 문자열이나 배열을 연결할 때 사용하는 함수이다.
즉, useData
컴포넌트 내부에 정의되어있는
실 데이터를 불러오는 setData
State 에 concat
함수를 사용하여
이전 데이터가 담긴 배열 뒤에 새로운 데이터 배열을 이어서 가져올 수 있도록 구현했다.
- Before
- After