NEXT.JS/Cock! 칵테일 프로젝트

[NEXT] 5. Redux-Toolkit으로 비동기 api 불러오기, 상태관리하기

예글 2024. 3. 8. 18:50

🚀 Redux-Toolkit 내부 thunk로 비동기 처리

  • Redux-Toolkit에는 내부적으로 thunk를 내장하고 있어서, 다른 미들웨어를 사용하지 않고도 비동기 처리를 할 수 있음
  • createAsyncThunk와 createSlice를 사용하여 Redux Toolkit만으로 비동기 처리를 쉽게 할 수 있으며, redux-saga에서만 사용할 수 있었던 기능까지 사용할 수 있음

https://redux-toolkit.js.org/usage/usage-with-typescript

 

Usage With TypeScript | Redux Toolkit

 

redux-toolkit.js.org

난 타입스크립트로 하고 있기 때문에 이 공식문서를 참고하면서 만들었다!

 

1️⃣ thunk 만들기 - createAsyncThunk

인자: 액션 타입 문자열, 프로미스를 반환하는 비동기 함수, 추가 옵션 

입력받은 액션 타입 문자열을 기반으로 프로미스 라이프사이클 액션 타입을 생성하고, thunk action creator를 반환

thunk action creator: 프로미스 콜백을 실행하고 프로미스를 기반으로 라이프사이클 액션을 디스패치

import {
  createSlice,
  createAsyncThunk,
  ActionReducerMapBuilder,
  AsyncThunk,
} from "@reduxjs/toolkit";

interface CocktailsState {
  loading: boolean;
  cocktails: any[];
  error: any;
  cocktail: any[];
}

// createAsyncThunk를 사용해 비동기로 칵테일 데이터 가져오기
export const fetchCocktails = createAsyncThunk(
  "cocktails/fetchCocktails",
  async () => {
    const res = await fetch(
      "https://www.thecocktaildb.com/api/json/v1/1/search.php?s="
    );
    return await res.json();
  }
);
  • createAsyncThunk는 세 가지 thunk action creator 반환
    • fetchCocktails.pending
    • fetchCocktails.fulfilled
    • fetchCocktails.rejected
  • 이 액션들이 디스패치 되면, thunk는 아래 과정들을 실행
    • pending 액션 디스패치
    • payloadCreator 콜백 호출하고 프로미스가 반환되기를 기다림
    • 프로미스가 반환되면, 프로미스의 상태에 따라 다음 행동을 실행
      • 프로미스가 이행된 상태라면, action.payload를 fulfilled 액션에 담아 디스패치
      • 프로미스가 거부된 상태라면, rejected 액션을 디스패치하되 rejectedValue(value) 함수의 반환값에 따라 액션에 어떤 값이 넘어올지 결정됨
        • rejectedValue가 값을 반환하면, action.payload를 reject 액션에 담음
        • rejectedValue가 없거나 값을 반환하지 않았다면, action.error 값처럼 오류의 직렬화된 버전을 reject 액션에 담음
    • 디스패치된 액션이 어떤 액션인지에 상관없이, 항상 최종적으로 디스패치된 액션을 담고 있는 이행된 프로미스를 반환

 

2️⃣ slice 만들기

  • thunk를 만들었다면, slice 만들기
import {
  createSlice,
  createAsyncThunk,
  ActionReducerMapBuilder,
  AsyncThunk,
} from "@reduxjs/toolkit";

interface CocktailsState {
  loading: boolean;
  cocktails: any[];
  error: any;
  cocktail: any[];
}

// createAsyncThunk를 사용해 비동기로 칵테일 데이터 가져오기
export const fetchCocktails = createAsyncThunk(
  ...
);

const cocktailSlice = createSlice({
  // 리듀서 이름
  name: "cocktails",
  // 초기 상태
  initialState: {
    loading: false,
    cocktails: [],
    error: null,
    cocktail: [],
  } as CocktailsState,
  reducers: {},
  // 추가 리듀서
  extraReducers: (builder: ActionReducerMapBuilder<CocktailsState>) => {
    builder
      // 데이터 가져오는 중
      .addCase(fetchCocktails.pending, (state) => {
        state.loading = true;
      })
      // 데이터 가져오기 성공
      .addCase(fetchCocktails.fulfilled, (state, action) => {
        state.loading = false;
        state.cocktails = action.payload.drinks;
      })

      .addCase(fetchCocktails.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});

export default cocktailSlice.reducer;

 

3️⃣ store에 저장

import { configureStore } from "@reduxjs/toolkit";
import cocktailSlice from "./features/cocktailSlice";
import { useDispatch } from "react-redux";

// 스토어 생성
const store = configureStore({
  reducer: {
    app: cocktailSlice,
  },
});

// useSelector 사용 시 타입으로 사용하기 위함
export type RootState = ReturnType<typeof store.getState>;

// useDispatch를 좀 더 명확하게 사용하기 위함
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch: () => AppDispatch = useDispatch;

export default store;
  • store에 저장하지 않는 다면 애써 만든 리듀서를 사용할 수 없게 됨

4️⃣ 컴포넌트에 적용

- app/page.tsx

"use client";

export default function Home() {
 
  const [modifiedCocktails, setModifiedCocktails] = useState([]);

  const { loading, cocktails, error } = useSelector((state: any) => ({
    ...state.app,
  }));

  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchCocktails());
  }, []);

  useEffect(() => {
    if (cocktails) {
      const newCocktails = cocktails.map((item: any) => {
        const { idDrink, strAlcoholic, strDrinkThumb, strGlass, strDrink } =
          item;

        return {
          id: idDrink,
          name: strDrink,
          img: strDrinkThumb,
          info: strAlcoholic,
          glass: strGlass,
        };
      });
      setModifiedCocktails(newCocktails);
    } else {
      setModifiedCocktails([]);
    }
  }, [cocktails]);

  if (loading) {
    return <Loading />;
  }

  if (error) {
    return <p>{error.message}</p>;
  }
  
  return (
    ...
      
  );
}

 

📔 useSelector

  • 스토어의 상태값을 반환해주는 역할
  • 리덕스 스토어의 상태값이 바뀐 경우 바뀐 스토어의 상태값을 다시 가져와 컴포넌트 렌더링 시킴
  • state => state.모듈명 형식으로 상태값을 반환 -> state.app

 

📔 useDispatch

  • useDispatch 객체를 dispatch로 재선언한 후, dispatch 변수를 활용하여 액션 호출 
  • 실행할 액션함수명을 적은 후, 해당 액션함수의 파라미터에 변경할 상태값을 추가하고 dispatch로 감싸면 해당 액션을 호출하는 dispatch 함수 완성.
  • 파라미터에는 문자열뿐만 아니라 true/false, 정수, json 타입 등 다양한 타입의 내용을 넣을 수 있음

 

- 크롬 Redux DevTools에서 상태값 확인

  • 처음에는 초깃값 형태로 들어오는 것을 확인

  • 렌더링 되면 칵테일 api에 담겨있는 정보들이 들어옴

5️⃣ 로딩페이지 만들기

  • 위에서 useSelector로 loading, cocktails, error를 받아왔었다
  • 나의 경우 칵테일 아이콘을 다운받아 로딩이 될 때 이 화면이 보이게끔 설정하였다

6️⃣ 실제 데이터 붙이기

- app/recipe/page.tsx

"use client";

const RecipeListPage = () => {
  const [modifiedCocktails, setModifiedCocktails] = useState([]);

  const { loading, cocktails, error } = useSelector((state: any) => ({
    ...state.app,
  }));

  const dispatch = useDispatch<AppDispatch>();

  useEffect(() => {
    dispatch(fetchCocktails());
  }, []);

  useEffect(() => {
    if (cocktails) {
      const newCocktails = cocktails.map((item: any) => {
        const { idDrink, strAlcoholic, strDrinkThumb, strGlass, strDrink } =
          item;

        return {
          id: idDrink,
          name: strDrink,
          img: strDrinkThumb,
          info: strAlcoholic,
          glass: strGlass,
        };
      });
      setModifiedCocktails(newCocktails);
    } else {
      setModifiedCocktails([]);
    }
  }, [cocktails]);

  if (loading) {
    return <Loading />;
  }

  if (error) {
    return <p>{error.message}</p>;
  }

  return (
    <div className={styles.root}>
      <div className={styles.top}>
        <span>총 {modifiedCocktails.length}개</span>
        <div>
          <select>
            <option>오름차순</option>
            <option>내림차순</option>
            <option>등록순</option>
          </select>
        </div>
      </div>
      <div className={styles.bottom}>
        {modifiedCocktails.map((item) => {
          return <CardItem item={item} />;
        })}
      </div>
    </div>
  );
};

export default RecipeListPage;
  • 우선, 나는 전체 칵테일 레시피를 recipe 페이지에서 보여줄 것이기 때문에 메인페이지에 썼던 코드를 recipe 파일로 옮겼다
  • 그리고 cardItem에 item을 props로 넘겨주었다

- components/Items/CardItem/index.tsx

"use client";

interface CocktailProps {
  item: {
    id: string;
    glass: string;
    img: string;
    info: string;
    name: string;
  };
}

const CardItem = ({ item }: CocktailProps) => {
  const router = useRouter();

  return (
    <div
      className={styles.box}
      onClick={() => router.push(`recipe/${item.id}`)}
    >
      <div className={styles.top}>
        <img src={item.img} alt={item.name} className={styles.thumbnail} />
      </div>
      <div className={styles.bottom}>
        <p className={styles.name}>{item.name}</p>
        <p className={styles.explain}>
          {item.info}
          {item.glass}
        </p>
      </div>
    </div>
  );
};

export default CardItem;
  • 받아온 Item 타입도 정해주고
  • 아이템에 대한 정보를 넣어주었다.

🔥 [오류 해결] 'AsyncThunkAction<any, void, AsyncThunkConfig>' 형식의 인수는 'UnknownAction' 형식의 매개 변수에 할당될 수 없습니다. 

  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchCocktails());
  }, []);
  • 자꾸 이부분에서 오류가 났는데 생각보다 쉽게 해결하였다.. 다행!!
const dispatch = useDispatch<AppDispatch>();

  useEffect(() => {
    dispatch(fetchCocktails());
  }, []);
  • 이렇게 useDispatch옆에 <AppDispatch>만 붙여주면 해결~~ 역시 스택오버플로우 짱,,

https://stackoverflow.com/questions/70143816/argument-of-type-asyncthunkactionany-void-is-not-assignable-to-paramete

 

Argument of type 'AsyncThunkAction<any, void, {}>' is not assignable to parameter of type 'AnyAction'

store.ts export const store = configureStore({ reducer: { auth: authReducer }, middleware: [], }); export type AppDispatch = typeof store.dispatch; export type RootState = Retu...

stackoverflow.com