Redux-Toolkit 이란?
리덕스 개발팀에서 만든 공식 라이브러리.
기존의 리덕스의 불편함을 개선하여 모듈 작성 시 액션 타입, 액션 생성 함수, 리듀서를 한 번에 작성할 수 있고,
상태를 업데이트할 때 불변성에 대해 신경쓰지 않아도 된다는 장점이 있다.
Redux-Toolkit으로 비동기처리 하는 법?
그것은 바로, Redux Thunk를 사용하는 것이다.
createAsyncThunk와 createSlice를 사용하여 Redux-Toolkit만으로 비동기 처리를 쉽게 할 수 있으며,
Redux-Saga에서만 사용할 수 있던 기능(이미 호출한 API 취소하기 등)까지 사용할 수 있다.
PokeAPI로 Redux-Toolkit Thunk로 비동기 처리 실습하기
1. 새 프로젝트 생성
npx react-native init PokeAPIExample
React Native 프로젝트를 생성한다.
이름은 'PokeAPIExample'로 하겠다. (다른 이름으로 바꿔도 상관 없다.)
VSCode로 방금 생성한 프로젝트를 열고, 터미널에 다음 패키지들을 설치한다.
npm install @reduxjs/toolkit react-redux axios
2. Redux 설정
src 폴더 하단에 store 폴더를 생성하고,
그 안에 애플리케이션의 상태를 관리할 store.ts 파일을 생성한다.
그리고 그 안에 다음과 같이 코드를 작성한다.
// src/store/store.ts
// ①
import {configureStore} from '@reduxjs/toolkit';
// ②
const store = configureStore({
reducer: {},
});
// ③
export type RootState = ReturnType<typeof store.getState>;
// ④
export type AppDispatch = typeof store.dispatch;
// ⑤
export default store;
① : configureStore를 사용하여 Redux 스토어를 설정한다.
② : 스토어는 여러 개의 리듀서로 나눌 수 있으며, 현재 우리 프로젝트에서는 pokemon의 리듀서만 사용할 계획이다.
③ : store.getState를 호출해서 얻은 상태 타입으로 RootState의 타입을 정의한다. ReturnType<typeof store.getState>는 store.getState의 반환 타입을 추론하여 얻는다.
④ : AppDispatch의 타입을 정의한다. store.dispatch의 타입이다.
⑤ : 생성한 스토어를 default로 export 한다. 이 스토어는 애플리케이션에서 상태관리를 위해 사용된다.
다음으로, src 폴더 하단에 slices 폴더를 생성하여
그 안에 우리가 구현할 핵심 기능인 PokeAPI의 비동기 액션을 정의할 pokemonSlice.ts 파일을 생성한다.
Documentation - PokéAPI
If you were using v1 of this API, please switch to v2 (this page). Read more… Quick tip: Use your browser's "find on page" feature to search for specific resource types (Ctrl+F or Cmd+F). Information This is a consumption-only API — only the HTTP GET m
pokeapi.co
PokeAPI의 문서를 살펴보면,
특정 포켓몬 데이터를 가져오기 위해 https://pokeapi.co/api/v2/pokemon/{id or name}/ 엔드포인트를 사용하고,
해당 API 호출에 대한 응답에서 'sprites' 객체를 찾으면 포켓몬의 이미지 URL을 가져올 수 있다.
또한, 상세정보 응답에서 'species' 객체의 'url'을 사용해 종 정보를 가져올 수 있으며,
종 정보 응답에서 'names' 배열을 확인해보면 'language.name'이 'ko'인 항목을 찾아 한글 이름을 추출할 수 있다.
이 정보들을 기반으로 pokemonSlice.ts 파일을 코딩해볼것이다.
// src/slices/pokemonSlice.ts
import {createSlice, createAsyncThunk} from '@reduxjs/toolkit';
import axios from 'axios';
// Pokemon 데이터의 타입을 정의한다. name은 포켓몬의 이름, image는 포켓몬의 이미지 URL이다.
interface Pokemon {
name: string;
image: string;
}
// 포켓몬 상태의 타입을 정의한다. data는 포켓몬 배열, loading은 로딩 상태, error는 에러 메시지, page는 현재 페이지 번호이다.
interface PokemonState {
data: Pokemon[];
loading: boolean;
error: string | null;
page: number;
}
// PokemonState의 초기 상태를 정의한다.
const initialState: PokemonState = {
data: [],
loading: false,
error: null,
page: 1,
};
// createAsyncThunk를 사용해 fetchPokemon이라는 비동기 thunk를 생성한다.
// 이 thunk는 페이지 번호를 받아서 해당 페이지의 포켓몬 데이터를 가져온다.
export const fetchPokemon = createAsyncThunk(
'pokemon/fetchPokemon',
async (page: number) => {
const offset = (page - 1) * 12; // 페이지에 따라 offset을 계산합니다.
const response = await axios.get(
`https://pokeapi.co/api/v2/pokemon?limit=12&offset=${offset}`,
);
const pokemonData = await Promise.all(
response.data.results.map(
async (pokemon: {name: string; url: string}) => {
const pokemonDetail = await axios.get(pokemon.url);
const speciesDetail = await axios.get(pokemonDetail.data.species.url);
const koreanNameEntry = speciesDetail.data.names.find(
(name: {language: {name: string}}) => name.language.name === 'ko',
);
const koreanName = koreanNameEntry
? koreanNameEntry.name
: pokemon.name;
return {
name: koreanName,
image: pokemonDetail.data.sprites.front_default,
};
},
),
);
return pokemonData;
},
);
// createSlice를 사용해 pokemon 슬라이스를 생성한다.
// 슬라이스는 상태와 관련된 리듀서 및 액션을 포함한다.
const pokemonSlice = createSlice({
name: 'pokemon', // 슬라이스의 이름
initialState, // 슬라이스의 초기 상태
reducers: {
// 페이지를 설정하는 리듀서
setPage: (state, action) => {
state.page = action.payload;
},
},
// 비동기 작업 및 외부 액션을 처리하는 extraReducers
extraReducers: builder => {
builder
// fetchPokemon이 pending 상태일 때, 로딩 상태를 true로 설정한다.
.addCase(fetchPokemon.pending, state => {
state.loading = true;
state.error = null;
})
// fetchPokemon이 fulfilled 상태일 때, 로딩 상태를 false로 설정하고, 데이터를 업데이트한다.
.addCase(fetchPokemon.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
// fetchPokemon이 rejected 상태일 때, 로딩 상태를 false로 설정하고, 에러 메시지를 업데이트한다.
.addCase(fetchPokemon.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Failed to fetch Pokémon';
});
},
});
// setPage 액션을 export한다. 이 액션은 페이지를 설정하는 데 사용된다.
export const {setPage} = pokemonSlice.actions;
// 슬라이스의 리듀서를 default로 export한다. 이 리듀서는 configureStore에서 사용된다.
export default pokemonSlice.reducer;
3. 커스텀 훅 작성(useAppDispatch.ts)
다음으로는, 타입이 지정된 dispatch 함수를 사용할 수 있도록 커스텀 훅을 만들 것이다.
// src/hooks/useAppDispatch.ts
import {useDispatch} from 'react-redux';
import type {AppDispatch} from '../store/store';
// useDispatch 훅을 사용해서 AppDispatch 타입을 명시한 커스텀 훅을 생성합니다.
// 이 훅은 앱에서 dispatch를 사용할 때 타입 안전성을 제공합니다.
const useAppDispatch = () => useDispatch<AppDispatch>();
// 생성한 커스텀 훅을 default로 export합니다. 이 훅을 사용하면 타입이 지정된 dispatch 함수를 사용할 수 있습니다.
export default useAppDispatch;
4. PokemonList 컴포넌트(App.tsx) 개발
마지막으로, Redux 상태를 가져와서 데이터를 fetch 시키고,
포켓몬 리스트를 화면에 표시하여 포켓몬 도감을 만들어 보도록 하겠다.
// App.tsx
import React, {useEffect} from 'react';
import {
SafeAreaView,
StyleSheet,
Text,
FlatList,
ActivityIndicator,
View,
Image,
Button,
} from 'react-native';
import {useSelector} from 'react-redux';
import {RootState} from './src/store/store';
import {fetchPokemon, setPage} from './src/slices/pokemonSlice';
import {Provider} from 'react-redux';
import store from './src/store/store';
import useAppDispatch from './src/hooks/useAppDispatch';
const PokemonList = () => {
// useAppDispatch 훅을 사용해 dispatch 함수를 가져온다.
const dispatch = useAppDispatch();
// useSelector 훅을 사용해 Redux 상태에서 pokemon 상태를 가져온다.
const {data, loading, error, page} = useSelector(
(state: RootState) => state.pokemon,
);
// 컴포넌트가 처음 렌더링되거나 페이지 번호가 변경될 때 fetchPokemon 액션을 dispatch한다.
useEffect(() => {
dispatch(fetchPokemon(page));
}, [dispatch, page]);
//페이지를 증가시키는 핸들러 함수
const handleNextPage = () => {
dispatch(setPage(page + 1));
};
//페이지를 감소시키는 핸들러 함수
const handlePrevPage = () => {
if (page > 1) {
dispatch(setPage(page - 1));
}
};
return (
<SafeAreaView style={styles.container}>
{loading ? (
// 로딩 중일 때 ActivityIndicator를 표시한다.
<ActivityIndicator size="large" color="#0000ff" />
) : error ? (
// 에러가 발생한 경우 에러 메세지를 표시한다.
<Text style={styles.error}>{error}</Text>
) : (
<View style={styles.body}>
<Text style={styles.title}>포켓몬도감</Text>
<FlatList
data={data}
keyExtractor={item => item.name} // 각 아이템의 고유한 key를 설정한다.
renderItem={({item}) => (
<View style={styles.itemContainer}>
<Image source={{uri: item.image}} style={styles.image} />
<Text style={styles.item}>{item.name}</Text>
</View>
)}
numColumns={3} // 아이템을 3열로 표시한다.
contentContainerStyle={styles.list}
scrollEnabled={false}
/>
<View style={styles.pagination}>
<Button
title="Previous"
onPress={handlePrevPage}
disabled={page === 1} // 현재 페이지가 1이면 Previous 버튼을 비활성화한다.
/>
<Text style={styles.pageNumber}>Page {page}</Text>
<Button title="Next" onPress={handleNextPage} />
</View>
</View>
)}
</SafeAreaView>
);
};
const App = () => (
// Provider 컴포넌트를 사용해 애플리케이션에 Redux 스토어를 제공한다.
<Provider store={store}>
<PokemonList />
</Provider>
);
// StyleSheet를 사용해 스타일을 정의한다.
const styles = StyleSheet.create({
body: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 30,
marginVertical: 20,
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
list: {
justifyContent: 'center',
alignItems: 'center',
},
itemContainer: {
alignItems: 'center',
padding: 10,
width: 120,
borderWidth: 1,
borderColor: '#000000',
margin: 5,
borderRadius: 10,
},
item: {
padding: 10,
fontSize: 15,
height: 44,
},
image: {
width: 50,
height: 50,
},
error: {
color: 'red',
fontSize: 18,
},
pagination: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginVertical: 30,
},
pageNumber: {
fontSize: 18,
marginHorizontal: 10,
},
});
// App 컴포넌트를 default로 export한다.
export default App;
이렇게 하면 PokeAPI를 활용한 포켓몬도감 앱이 완성된다..!
마지막으로,
npm run android 또는 npm run ios를 통해 에뮬레이터 또는 시뮬레이터로 앱이 잘 만들어졌는지 확인해보자.
만약에 블로그 포스팅대로 잘 따라왔다면 다음과 같은 결과물이 나올 것이다.
이상으로 블로그 포스팅을 마치도록 하겠다.
끝!
'개발 > React Native' 카테고리의 다른 글
[React Native, RN, Typescript] 팀프로젝트 앱 개발 담당파트 시연영상 (0) | 2024.08.01 |
---|---|
[RN 개발일지] 좋아요 기능 구현하기 (버튼 클릭 시 이미지 및 좋아요 수 변경) (0) | 2024.05.15 |
[에러] error Failed to install the app. Command failed with exit code 1: ./gradlew app:installDebug -PreactNativeDevServerPort=8081 이슈 (0) | 2024.05.07 |
[에러] pod install --repo-update m1 이슈 (0) | 2024.05.07 |
[React Native] Text에서 특정 글자만 색상(스타일) 변경하기. (0) | 2023.07.06 |