리액트 리덕스 툴킷에서 비동기 통신하는 법은 두 가지가 있다: 첫번째는 일반적인 axios(또는 fetch)를 활용하는 것이고, 두번째는 createAsyncThunk를 활용하는 것이다. 본 글에서는 두 가지를 모두 이용하여 그 차이점을 설명하고자 한다.
비동기 통신하기 (1) axios 활용하기
// GetWeather.tsx
// 중략...
useEffect(() => {
const getCurrentWeather = async () => {
try {
const position = await getLocation();
const { latitude, longitude } = position.coords;
const weatherData = await fetchWeatherData(latitude, longitude);
dispatch(setWeatherInfo(weatherData)); // setWeatherInfo(액션명), weatherData(페이로드)
} catch (error) {
console.error('Error:', error);
}
};
getCurrentWeather();
}, [dispatch]);
위의 컴포넌트에서 fetchWeatherData 라는 함수를 통해 API 통신한 결과 값을 weatherData 변수에 담는 것을 확인할 수 있다. 그리고 setWeatherInfo라는 액션을 통해 weatherData를 페이로드로 전달하여 store에 등록될 수 있도록 한다.
// WeatherAPI > index.ts
const fetchWeatherData = async (latitude: number, longitude: number) => {
try {
const response = await axios.get<WeatherData>(
`https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${API_KEY}&units=metric&lang=kr`
);
const cityName = await axios.get(
`http://api.openweathermap.org/geo/1.0/reverse?lat=${latitude}&lon=${longitude}&appid=${API_KEY}`
);
const newCityName = `${cityName.data[0].name} ${response.data.name}`;
const newResponse = { ...response.data, name: newCityName };
return newResponse;
} catch (err) {
throw new Error('openweathermap API 통신 에러');
}
};
fetchWeatherData 함수를 살펴보면 axios를 통해 API 통신을 하는 것을 알 수 있다.
즉, return newResponse 에서 newResponse에 해당하는 데이터가 전술한 weatherData 에 담기게 된다.
비동기 통신하기 (2) createAsyncThunk 활용하기
// GetCocktail.tsx
function GetCocktail({ weatherName }: GetCocktailProps) {
const { cocktailInfo, status } = useAppSelector((state) => state.cocktail);
const dispatch = useAppDispatch();
// action dispatch 하기
useEffect(() => {
if (weatherName) {
dispatch(fetchCocktail(weatherName)); // fetchCocktail은 비동기 작업
}
}, [dispatch, weatherName]); // dispatch, weatherName 갱신에 따라 재 렌더링
// 중략..
// features > cocktailSlice.ts (방법1)
export const fetchCocktail = createAsyncThunk<Cocktail, string>(
'cocktail/fetchCocktail',
async (weatherName) => {
try {
const glassType: any = WGobject[weatherName];
const response = await fetch(
`https://www.thecocktaildb.com/api/json/v1/1/filter.php?g=${glassType}`
);
// console.log('response:', response.headers.get('content-type'));
if (!response.ok) {
throw new Error(
`칵테일 데이터를 가져오는 데 실패했습니다: ${response.statusText}`
);
}
// 중략..
const initialState: CocktailState = {
cocktailInfo: null,
status: 'idle',
};
const cocktailSlice = createSlice({
name: 'cocktail',
initialState,
reducers: {}, // 동기 작업
extraReducers: (builder) => {
// 비동기 작업
builder
.addCase(fetchCocktail.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchCocktail.fulfilled, (state, action) => {
state.status = 'succeeded';
state.cocktailInfo = action.payload;
})
.addCase(fetchCocktail.rejected, (state) => {
state.status = 'failed';
});
},
});
먼저 createAsyncThunk는 매개변수 2개를 받는다. 첫번째 인자로는 Action Type을 나타내는 문자열을 받는다.('cocktail/fetchCocktail' 이 부분) 이는 상태에 따른 Action Types를 만든다(각각 pending, fulfilled, rejected). createSlice 부분의 extraReducers 내부에 pending, fulfilled, rejected에 따른 status 상태가 변화하는 것을 파악할 수 있다. fulfilled 됐을 때 액션의 payload 값이 비로소 status에 저장된다. 두번째 매개변수는 비동기 결과를 포함한 Promise를 반환하는 콜백함수(async)이다.
(참고) createAsyncThunk 내 API 주소 모듈 분리
또는 createAsyncThunk 내부의 API 주소를 따로 모듈로 분리하여 가독성을 높이는 방법을 택할 수도 있다.
// cocktailSlice.ts (방법2)
export const fetchCocktail = createAsyncThunk<Cocktail, string>(
'cocktail/fetchCocktail',
async (weatherName) => {
try {
const glassType: any = WGobject[weatherName];
const cocktailArr = await getGlassTypeAPI(glassType); // getGlassTypeAPI 함수 호출, return: cocktailArr
const randomIndex = Math.floor(Math.random() * cocktailArr.length);
return cocktailArr[randomIndex];
} catch (error) {
console.error(
'서버와 통신에 실패하였습니다. 나중에 다시 시도해주세요.',
error
);
throw error;
}
}
);
// 중략..
// CocktailAPI > index.ts
export async function getGlassTypeAPI(glassType: string) {
try {
const response = await fetch(
`https://www.thecocktaildb.com/api/json/v1/1/filter.php?g=${glassType}`
);
if (!response.ok) {
throw new Error(
`칵테일 데이터를 가져오는 데 실패했습니다: ${response.statusText}`
);
}
if (response.headers.get('content-type')?.includes('application/json')) {
// const contentType = response.headers.get('content-type');
// console.log('contentType', contentType); // application.json
const data = await response.json();
const cocktailArr = data.drinks;
return cocktailArr;
} else {
console.error(
'서버로부터 예상치 못한 응답을 받았습니다. 나중에 다시 시도해 주세요.'
);
}
} catch (error) {
console.error(
'서버와 통신에 실패하였습니다. 나중에 다시 시도해주세요.',
error
);
throw error;
}
}