프로젝트/날씨 칵테일

[프로젝트] 날씨 칵테일 | 2. 리덕스 툴킷으로 비동기 통신 하는 법 (axios, createAsyncThunk)

paintover23 2023. 10. 14. 12:14
728x90

리액트 리덕스 툴킷에서 비동기 통신하는 법은 두 가지가 있다: 첫번째는 일반적인 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;
  }
}
728x90
반응형