Part 2 will use React Query to implement the same feature more concisely and efficiently.

The full code is available here.
posts, page, loading, and hasMore.import React, { useState, useEffect, useCallback } from 'react';
import { View, FlatList, Text, ActivityIndicator, StyleSheet, SafeAreaView } from 'react-native';
import axios from 'axios';
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
const PostsScreenWithAxios: React.FC = () => {
const [posts, setPosts] = useState<Post[]>([]);
const [page, setPage] = useState<number>(1); // ํ์ฌ ๋ก๋ 'ํด์ผ ํ ' ํ์ด์ง
const [loading, setLoading] = useState<boolean>(false);
const [hasMore, setHasMore] = useState<boolean>(true);
// ... (fetchPosts, useEffect ์์ , renderItem, renderFooter, handleLoadMore ๊ตฌํ) ...
};
// ... (์คํ์ผ ์ ์) ... const fetchPosts = useCallback(async () => {
// ๋ก๋ฉ ์ค์ด๊ฑฐ๋ ๋ ๊ฐ์ ธ์ฌ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ์คํํ์ง ์์ (์ค๋ณต ๋ฐฉ์ง)
if (loading || !hasMore) {
console.log(`fetchPosts skipped: loading=${loading}, hasMore=${hasMore}`);
return;
}
setLoading(true);
console.log(`Fetching page ${page}...`);
try {
// --- ๋ก๋ฉ ํ์ธ์ฉ 1์ด ์ง์ฐ ---
await new Promise(resolve => setTimeout(resolve, 1000));
// --------------------------
const response = await axios.get<Post[]>(
`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=5`
);
if (response.data.length > 0) {
setPosts((prevPosts) => [...prevPosts, ...response.data]); // ๊ธฐ์กด ๊ฒ์๊ธ์ ์ ๊ฒ์๊ธ ์ถ๊ฐ
setPage((prevPage) => prevPage + 1); // ๋ค์ ํ์ด์ง ๋ฒํธ๋ก ์
๋ฐ์ดํธ
} else {
setHasMore(false);
console.log('No more posts to load.');
}
} catch (error) {
console.error("๋ฐ์ดํฐ ๋ก๋ฉ ์ค ์ค๋ฅ ๋ฐ์:", error);
} finally {
setLoading(false);
}
}, [page, loading, hasMore]); // page, loading, hasMore๊ฐ ๋ณ๊ฒฝ๋ ๋ ํจ์ ์ฌ์์ฑuseEffect an empty dependency array, [], so it runs only once on mount. useEffect(() => {
// ์ปดํฌ๋ํธ ๋ง์ดํธ ์ ์ฒซ ํ์ด์ง ๋ฐ์ดํฐ ๋ก๋ฉ ์์ (page๊ฐ 1์ผ ๋)
console.log('useEffect for initial fetch triggered.');
fetchPosts();
}, []);fetchPosts to onEndReached to load more data when scrolling reaches the end of the list. // ์์ดํ
๋ ๋๋ง ํจ์
const renderItem = ({ item }: { item: Post }) => (
<View style={styles.itemContainer}>
<Text style={styles.itemTitle}>{item.id}. {item.title}</Text>
<Text style={styles.itemBody}>{item.body}</Text>
</View>
);
// ํธํฐ ๋ก๋ฉ ์ธ๋์ผ์ดํฐ
const renderFooter = () => {
return loading ? <ActivityIndicator size="large" color="#007AFF" style={styles.loader} /> : null;
};
return (
<SafeAreaView style={styles.container}>
<FlatList
data={posts}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
onEndReached={fetchPosts} // ์คํฌ๋กค ๊ธฐ๋ฐ ๋ก๋ฉ ํธ๋ฆฌ๊ฑฐ
onEndReachedThreshold={1} // ์คํฌ๋กค์ด ๋ฆฌ์คํธ ๋์ ๋๋ฌํ์ ๋ ํธ์ถ๋๋ ์๊ณ๊ฐ (0~1 ์ฌ์ด) - 1์ด๋ฉด ๋ฆฌ์คํธ ๋์ ๋๋ฌํ์ ๋ ํธ์ถ๋จ
ListFooterComponent={renderFooter}
contentContainerStyle={styles.listContainer}
/>
</SafeAreaView>
);
// --- ์คํ์ผ ์ ์ ---
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fff' },
listContainer: { paddingHorizontal: 15, paddingTop: 10, paddingBottom: 30 },
itemContainer: { backgroundColor: '#f9f9f9', padding: 15, marginBottom: 12, borderRadius: 8, borderWidth: 1, borderColor: '#eee', shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2 },
itemTitle: { fontSize: 17, fontWeight: '600', marginBottom: 6, color: '#333' },
itemBody: { fontSize: 14, color: '#666', lineHeight: 20 },
loader: { marginVertical: 25, alignItems: 'center' },
});
export default PostsScreenWithAxios;// PostsScreenWithAxios.tsx
import React, { useState, useEffect, useCallback } from 'react';
import { View, FlatList, Text, ActivityIndicator, StyleSheet, SafeAreaView } from 'react-native';
import axios from 'axios';
// ๊ฒ์๊ธ ๋ฐ์ดํฐ ํ์
์ ์
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
const PostsScreenWithAxios: React.FC = () => {
const [posts, setPosts] = useState<Post[]>([]); // ๋ถ๋ฌ์จ ๊ฒ์๊ธ ๋ฐ์ดํฐ ๋ฐฐ์ด
const [page, setPage] = useState<number>(1); // ํ์ฌ ๋ก๋ 'ํด์ผ ํ ' ํ์ด์ง ๋ฒํธ
const [loading, setLoading] = useState<boolean>(false); // ๋ฐ์ดํฐ ๋ก๋ฉ ์ค ์ฌ๋ถ
const [hasMore, setHasMore] = useState<boolean>(true); // ๋ ๋ถ๋ฌ์ฌ ๋ฐ์ดํฐ๊ฐ ์๋์ง ์ฌ๋ถ
// ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ํจ์ (useCallback ์ฌ์ฉ)
const fetchPosts = useCallback(async () => {
// ๋ก๋ฉ ์ค์ด๊ฑฐ๋ ๋ ๊ฐ์ ธ์ฌ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ์คํํ์ง ์์
if (loading || !hasMore) {
console.log(`fetchPosts skipped: loading=${loading}, hasMore=${hasMore}`);
return;
}
setLoading(true); // ๋ก๋ฉ ์์
console.log(`Fetching page ${page}...`);
try {
// --- ๋ก๋ฉ ์ํ ํ์ธ์ ์ํ ์ธ์์ ์ง์ฐ ์ถ๊ฐ (1์ด) ---
await new Promise(resolve => setTimeout(resolve, 1000));
// -------------------------------------------------
const response = await axios.get<Post[]>(
`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`
);
if (response.data.length > 0) {
setPosts((prevPosts) => [...prevPosts, ...response.data]);
// ๋ค์ ํ์ด์ง ๋ฒํธ๋ก ์
๋ฐ์ดํธ
setPage((prevPage) => prevPage + 1);
} else {
setHasMore(false); // ๋ ์ด์ ๋ฐ์ดํฐ ์์
console.log('No more posts to load.');
}
} catch (error) {
console.error("๋ฐ์ดํฐ ๋ก๋ฉ ์ค ์ค๋ฅ ๋ฐ์:", error);
// ์๋ฌ ์ฒ๋ฆฌ ๋ก์ง
} finally {
setLoading(false); // ๋ก๋ฉ ์ข
๋ฃ
}
// useCallback์ ์์กด์ฑ ๋ฐฐ์ด: ํจ์ ๋ด์์ ์ง์ ์ฌ์ฉํ๋ ์ํ/ํ๋กญ์ค
}, [page, loading, hasMore]);
// *** ํต์ฌ ์์ : ์ด๊ธฐ ๋ฐ์ดํฐ ๋ก๋ฉ ***
useEffect(() => {
// ์ปดํฌ๋ํธ ๋ง์ดํธ ์ ์ฒซ ํ์ด์ง ๋ฐ์ดํฐ ๋ก๋ฉ ์์ (page๊ฐ 1์ผ ๋)
console.log('useEffect for initial fetch triggered.');
fetchPosts();
// ์์กด์ฑ ๋ฐฐ์ด์ ๋น ๋ฐฐ์ด([])๋ก ์ค์ ํ์ฌ ๋ง์ดํธ ์ ์ค์ง ํ ๋ฒ๋ง ์คํ
}, []); // ๋น ๋ฐฐ์ด๋ก ๋ณ๊ฒฝ!
// ์์ดํ
๋ ๋๋ง ํจ์
const renderItem = ({ item }: { item: Post }) => (
<View style={styles.itemContainer}>
<Text style={styles.itemTitle}>{item.id}. {item.title}</Text>
<Text style={styles.itemBody}>{item.body}</Text>
</View>
);
// ํธํฐ ๋ก๋ฉ ์ธ๋์ผ์ดํฐ
const renderFooter = () => {
return loading ? <ActivityIndicator size="large" color="#007AFF" style={styles.loader} /> : null;
};
// ์คํฌ๋กค์ด ๋ฆฌ์คํธ ๋์ ๋๋ฌํ๋ฉด ํธ์ถ๋ ํจ์
const handleLoadMore = () => {
console.log(`onEndReached triggered. Current state: loading=${loading}, hasMore=${hasMore}, page=${page}`);
// ๋ก๋ฉ ์ค์ด ์๋๊ณ , ๋ ๊ฐ์ ธ์ฌ ํ์ด์ง๊ฐ ์์ ๋๋ง fetchPosts ํธ์ถ
if (!loading && hasMore) {
console.log('handleLoadMore is calling fetchPosts...');
fetchPosts();
} else {
console.log('handleLoadMore determined not to call fetchPosts.');
}
};
return (
<SafeAreaView style={styles.container}>
<FlatList
data={posts}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
onEndReached={handleLoadMore} // ์คํฌ๋กค ๊ธฐ๋ฐ ๋ก๋ฉ ํธ๋ฆฌ๊ฑฐ
onEndReachedThreshold={0.5} // ์ธ์ ํธ๋ฆฌ๊ฑฐํ ์ง ๊ฒฐ์ (์ค์)
ListFooterComponent={renderFooter}
contentContainerStyle={styles.listContainer}
/>
</SafeAreaView>
);
};
// --- ์คํ์ผ ์ ์ ---
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fff' },
listContainer: { paddingHorizontal: 15, paddingTop: 10, paddingBottom: 30 },
itemContainer: { backgroundColor: '#f9f9f9', padding: 15, marginBottom: 12, borderRadius: 8, borderWidth: 1, borderColor: '#eee', shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2 },
itemTitle: { fontSize: 17, fontWeight: '600', marginBottom: 6, color: '#333' },
itemBody: { fontSize: 14, color: '#666', lineHeight: 20 },
loader: { marginVertical: 25, alignItems: 'center' },
});
export default PostsScreenWithAxios;FlatList: https://reactnative.dev/docs/flatlistโuseEffect official documentation: https://react.dev/reference/react/useEffectโuseCallback official documentation: https://react.dev/reference/react/useCallbackโ