본문 바로가기
React 관련/프로젝트

[React-Redux] 실제 프로젝트에 적용하고, thunk로 비동기 통신 구현

by ash9river 2024. 5. 8.

서론

props drilling

기본적으로 local state를 이용하는 useState나 기타 훅은, props를 다른 컴포넌트로 넘겨줄 수 있는데, 이때 props를 필요로 하지 않는 컴포넌트에서 props를 넘겨줘야 하거나 아니면 props를 너무 깊게(depth 관련) 전달하면 추적이 어려워진다.

global state

이에 따라서, 프로젝트에서 local state와 global state(App-Wide State)의 구분이 필요해지고, redux를 이용해야하는 상황이 발생하였다.

global state를 구분하고, 그에 따라서, 상태 관리 라이브러리 중 하나인 redux를 활용하게 되었다.

https://github.com/ash9river/React-Learned/tree/main/section19

 

React-Learned/section19 at main · ash9river/React-Learned

Contribute to ash9river/React-Learned development by creating an account on GitHub.

github.com

 

본론

저장소

리덕스에서는 저장소가 존재하는데, 이 저장소는 관습상 store라고 명명된 폴더를 만들고, 그 폴더를 통해 관리를 한다.

 

이 저장소는 createStore()로 만들 수도 있지만, 리덕스팀에서 이를 deprecated하였으니, 나는 RTK(리덕스툴킷)의 configureStore()를 이용하겠다.

configureStore()

configureStore()는 보통 루트 파일에 작성한다.

폴더구조

다음의 스크린샷처럼 루트 디렉토리에 index라는 이름으로 작성하기 때문에, 여러 가지 리듀서를 combineReducer()로 합친 리듀서를 리듀서로 갖는다.

import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { thunk } from 'redux-thunk';
import exampleReducer from './exampleReducer';
import postsItemReducer from './bulletin/postsItemReducer';
import locationReducer from './map/locationReducer';

const rootReducer = combineReducers({
  // list of Reducers
  exampleReducer,
  postsItemReducer,
  locationReducer,
});

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(thunk),
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

// useSelector 시
// const example = useSelector((state: RootState) => state.exampleReducer.exampleActionFunction);

여기서 RTK에 내장된 createAsyncThunk를 활용해 비동기 통신을 구현하는 것이 아니라, 비동기 통신을 위해 redux-thunk를 사용한다.

 

react-redux를 활용하기 때문에, 좀 더 돌아가는 방식이지만 연습을 위해서 비효율적으로 만들었다.

Reducer

예시

예시로 작성한 리듀서는 다음과 같다.

import { GithubProfile } from 'services/getData';

export const EXAMPLEACTIONTYPE = 'exampleReducer/EXAMPLEACTIONTYPE' as const;

export const exampleActionFunction = (variable: GithubProfile) => ({
  type: EXAMPLEACTIONTYPE,
  payload: variable,
});
export const exampleActionFunctionVersionTwo = (variable: GithubProfile) => ({
  type: EXAMPLEACTIONTYPE,
  payload: variable,
});

type ExampleAction =
  | ReturnType<typeof exampleActionFunction>
  | ReturnType<typeof exampleActionFunctionVersionTwo>;
//  | ReturnType<typeof 액션함수>

type ExampleState = {
  exampleItems: GithubProfile[];
};

const initialState: ExampleState = {
  exampleItems: [],
};

const exampleReducer = (
  state: ExampleState = initialState,
  action: ExampleAction,
) => {
  switch (action.type) {
    case EXAMPLEACTIONTYPE: {
      const existIdx = state.exampleItems.findIndex(
        (item) => item.id === action.payload.id,
      );
      if (existIdx === -1) {
        return {
          ...state,
          exampleItems: [...state.exampleItems, action.payload],
        };
      }
      return state;
    }
    default:
      return state;
  }
};

export default exampleReducer;

먼저, 액션 타입명을 작성한다. 이때, 액션 타입명을 폴더명과 함께 작성하고 as const를 붙여야 한다. as const를 붙여야 액션 타입명으로 인식하고, 폴더명과 함께 작성해야, 액션 타입명의 중복을 쉽게 피할 수 있다.

 

그 다음으로, 액션 함수를 작성한다. 액션 함수의 타입을 지정하고, payload라는 속성에 데이터를 전달한다.

 

세 번째는 타입 지정이다. ReturnType<typeof 액션함수>를 통해서 리듀서의 전체 액션 타입을 하나로 관리한다.

 

마지막은 리듀서이다. action.type로 분기하고, 로직에 따른 작동을 수행한다.

실제 활용

예시로 깃허브 api를 통해서 깃허브 프로필을 불러올 것이다. 일단 api불러오는 로직은 다음과 같다.

API 로직

이하의 링크에서 사용하는 방식을 따왔다.


https://ash9river.tistory.com/47

 

React의 Typescript와 함께 data fetching(axios로 API 호출)

Typescript typescript를 사용함에 따라 기존의 코드에서 사용하던 axios가 어려워져버렸다... 기존의 자바스크립트를 이용한 axios는 간단하고 데이터를 쉽게 받아올 수 있었으나, Dynamically Typed Language라

ash9river.tistory.com

import { AxiosRequestConfig, isAxiosError } from 'axios';
import { Dispatch } from 'redux';
import { exampleActionFunction } from '../store/exampleReducer';
import { apiRequester, setRequestDefaultHeader } from './api-requester';

const bracket = {};

export const getData = async <T>(
  url: string,
  config?: AxiosRequestConfig,
): Promise<T> => {
  try {
    const modifiedConfig = setRequestDefaultHeader(config || bracket);
    const response = await apiRequester.get<T>(url, modifiedConfig);
    return response.data;
  } catch (error) {
    if (isAxiosError(error)) throw new Error(error.message);
    else throw error;
  }
};

export async function getUserData(username: string) {
  const response = await getData<GithubProfile>(
    `https://api.github.com/users/${username}`,
  );

  return response;
}

export interface UserProps {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

export interface GithubProfile {
  login: string;
  id: number;
  node_id: string;
  avatar_url: string;
  gravatar_id: string;
  url: string;
  html_url: string;
  followers_url: string;
  following_url: string;
  gists_url: string;
  starred_url: string;
  subscriptions_url: string;
  organizations_url: string;
  repos_url: string;
  events_url: string;
  received_events_url: string;
  type: string;
  site_admin: boolean;
  name: string;
  company: string;
  blog: string;
  location: null;
  email: null;
  hireable: null;
  bio: string;
  public_repos: number;
  public_gists: number;
  followers: number;
  following: number;
  created_at: Date;
  updated_at: Date;
}

리듀서

주석을 달아뒀으니까, 참고해서 해보자.

 

redux-thunk를 활용한 미들웨어인데, 이 함수를 dispatch하여서 사용할려면, useDispatch를 이용하고, 그 때, AppDispatch의 타입을 이용하면 된다.

import { Dispatch } from 'redux';
import { getUserData } from 'services/getData';
// 액션 함수의 타입 지정, 보통 '폴더명/액션타입명'
export const GETPOSTITEM = 'bulletin/GETPOSTITEM' as const;

// 액션함수
export const getPostItem = (payload: any) => ({
  type: GETPOSTITEM,
  payload,
});

// 액션 타입 지정
type PostsItemAction = ReturnType<typeof getPostItem>;

// 게시글 state 타입 지정
type PostItemState = {
  postItem: {
    title: string;
    content: string;
  };
};

// 게시글 처음 state
const initialState: PostItemState = {
  postItem: {
    title: '',
    content: '',
  },
};

// redux-thunk를 활용한 미들웨어
export const getPostItemWithThunk = (): ((
  dispatch: Dispatch,
) => Promise<void>) => {
  return async (dispatch: Dispatch) => {
    try {
      const userData = await getUserData('ash9river');

      const myData: PostItemState = {
        postItem: {
          title: userData.company,
          content: userData.html_url || '',
        },
      };

      dispatch({
        type: GETPOSTITEM,
        payload: myData,
      });
    } catch (error) {
      console.error('Error fetching user data:', error);
    }
  };
};

const postsItemReducer = (
  state: PostItemState = initialState,
  action: PostsItemAction,
) => {
  const { type, payload } = action;
  switch (type) {
    case GETPOSTITEM: {
      return {
        postItem: payload.postItem,
      };
    }
    default:
      return state;
  }
};

export default postsItemReducer;

 

결론

redux 저장소를 만들고, redux-thunk를 활용해서 비동기 통신의 결과물을 성공적으로 저장소에 저장할 수 있게 되었다.

 

미들웨어의 작동 방식은 생략하겠다.