AWS와 React Query를 활용한 CRUD 만들기

허민(허브)·2022년 6월 2일
47

오늘은 동아리 세미나때 함께 제공하기 위해 준비한 작고 귀여운 예제를 어떻게 만들었는지 공유해드릴려고 합니다.

그 전에 React QUery에 대해서 간단히 설명할려고 합니다!

상태관리 어떻게 하고 계신가요?

여러분들은 상태관리를 어떻게 하고 계신가요? 많은 분들이 상태관리 방법이라고 한다면 상태란 개념이 추상적이기 때문에 상태관리를 “해주는” 라이브러리를 먼저 떠올리는게 대다수 이실 겁니다.

Store에서 비동기 통신 분리하기 (feat. React Query)

해당 블로그 포스팅을 해주신 배민근 개발자님 께서는 상태를 이렇게 말씀해주셨습니다.

주어진 시간에 대해 시스템을 나타내는 것으로 언제든지 변경될 수 있다. 즉 문자열, 배열, 객체 등의 형태로 응용 프로그램에 저장된 데이터.

즉 개발자 입장에선 관리해야하는 데이터들 입니다. 모던 웹 프론트엔드 개발에서는 UI/UX의 중요성과 함께 프로덕트 규모가 많이 커지고 FE에서는 수행하는 역할이 늘어났습니다. 이는 즉 관리하는 상태가 많아졌다는 것을 의미하는데요.

이를 관리하고자 다양한 상태관리 방법론과 라이브러리들이 등장하게 되었습니다.

상태관리의 영역은 과연 어디까지일까?

Redux 등을 활용하면 Store는 전역 상태가 저장되고 관리되는 공간인데 상태관리보단 API 통신 관련코드가 가득 차있는 경험이 분명 있으실 겁니다.

또한 이를 기계적으로 성공, 실패, 로딩 등과 같이 상태를 관리하고 비슷한 구조를 계속 만들어내는것에 피곤함을 느끼셨을 겁니다.

그렇기에 우리는 관리하고 싶은 상태들의 특성에 대해 생각해보았고 데이터의 주체(Ownership)이 어디에 있는가에 따라 관심사를 분리할 수 있었습니다.


배민근 개발자님 께서는 해당 특성들을 모두 일종의 캐시라고 접근하여 상태를 두가지로 나눠보셨습니다.


해당 state의 정의는 배민 세미나에서 정리하였던 내용을 작성하였습니다.

그리고 배민 주문팀에서 선택한 해답은 React-query 도입이였습니다.

React Query Overview

React query는 데이터를 알아서 패치, 캐쉬, 업데이트합니다. 게다가 전역 변수가 오염되지 않는다고 합니다.
또한 선언적, 백그라운드에서 작동되고, 심플하고 훅과 유사하다고 합니다.

The 3 core concepts of React Query

Queries

Queries 는 보통 Get으로 받아올 대부분의 API에 사용합니다. 즉 데이터 Fetching 용이라고 생각하면 됩니다.

A query is a declarative dependency on an asynchronous source of data that is tied to a unique key. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server. If your method modifies data on the server, we recommend using Mutations instead.

이때 Query Key가 필요한데 React Query Key에 따라 query caching을 관리합니다. 이때 실무에선 Array를 많이 사용한다고 합니다.

저는 이런식으로 사용하고 있습니다.

export const queryKeys = {
  items: ['items'] as const,
  createItem: (id: string, name: string, price: number) =>
    ['createItem', id, name, price] as const,
};
...

const { data, status } = useQuery(queryKeys.items, getItemAll);

그리고 이때 다양한 option들이 인터페이스로 제공되게 됩니다. 세미나 스크린샷을 한번 보여드리면 아래와 같습니다.

그리고 일종의 꿀팁은 uqeries의 선언부를 도메인별로 분리해서 빼니 관리가 더 편하다고 하셨습니다.

Mutations

mutation은 Server state의 영향을 줄 때 사용을 합니다. 즉 데이터 생성, 수정, 삭제를 할때 활용을 합니다.

그리고 해당 mutate가 발생하고 성공할 경우 Query Invalidation 을 통해 refatching 을 할 수 있습니다.

Query Invalidation

이러면 해당 Key를 가진 query는 stale 취급되고, 현재 rendering 되고 있는 query들은 모두 background에서 refetch 됩니다.

그런데 Caching이랑 Synchronization은요?


이 컨셉의 이해가 중요합니다.
배민 주문팀으로 이해를 한다면 나에게 보이는 것은 주문 전, 사장님은 주문 후입니다. 해당 statle data엣 대해선 API를 다시 찔러줘야 합니다.
600초 내에서는 캐시가 유효하겠지만 610초에 대해서는 API가 낡았다고 생각합니다. loader가 돈다던지 하면서 API를 다시 찔러볼겁니다. 이렇게 하면 stale data에 대해선 낡은 데이터를 먼저 보여주고 background에서는 re-fatch를 합니다. 이에 대해선 latency가 숨켜진다는 장점이 확실합니다.

그리고 이를 메모리 케시에도 적용한것이 바로 react-query, swr, ect.... 등등 입니다.

Query 상태흐름은 아래와 같습니다.

QueryCLient의 내부적 동작원리


그렇다면 Query Client는 어떻게 전역에 흩뿌려져있는 server State를 관리할 수 있을까요? 정답은 QueryClientProvider 에 있는데요. 는 UseContext로 구현이 되어 있어 전역에서 QueryClient와 함께 구현이 되어져 있습니다.

실전 예제 같이 따라해보기

먼저 서버를 구현해야하는데 저는 AWS를 통해서 구현을 하였습니다.
자습서 : Lambda 및 DynamoDB를 사용한 CRUD API 구축

해당 내용을 보고 먼저 따라하시면 큰 문제가 없으실 겁니다. 그리고 중요한 건은 저희는 React를 통해 개발하기 때문에 도메인 위치가 달라 CORS 에러가 발생합니다. 그래서 API Gateway에서 네트워크 설정을 함께 해주셔야 합니다.

먼저 API를 미리 다 만들어두셨단 가정하에 프론트 코드를 한번 살펴보겠습니다 .
Frontend 예제 코드

Create Client

먼저 index.tsx에 Provider와 client를 선언해 줍니다.

import {
  useQuery,
  useMutation,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
} from 'react-query';

...


// TODO: Create a client
const queryClient = new QueryClient();

...
// TODO: Provide the client to your App 
	<QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} position="bottom-right" />
    </QueryClientProvider>

이때 ReactQUeryDevtools를 함께 선언한 것이 보이는데 이는 data의 흐름을 관찰 할 수 있도록 제공하는 debug tool입니다.

API 통신 코드 작성하기

src/api아래 itemService.ts를 만들어 아래와 같이 작성해줍니다.

import axios from 'axios';

export type Item = {
  id: string;
  name: string;
  price: number;
};
export type Items = {
  Count: number;
  Items: Item[];
  ScannedCount: number;
};

export const apiClient = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
  headers: {
    'Content-type': 'application/json',
  },
});

export const getItemAll = async () => {
  const { data } = await apiClient.get<Items>('/items');
  return data;
};

export const getItemById = async (id: any) => {
  const response = await apiClient.get<Item>(`/items/$${id}`);
  return response.data;
};

export const createItem = async ({ id, name, price }: Item) => {
  const response = await apiClient.put('/items', {
    price,
    name,
    id,
  });
  return response.data;
};

export const deleteById = async (id: string) => {
  const response = await apiClient.delete(`/items/${id}`);
  return response.data;
};

queryKeys 생성하기

src/query/queryKey.ts아래 아래와 같이 작성 해줍니다.

export const queryKeys = {
  items: ['items'] as const,
  createItem: (id: string, name: string, price: number) =>
    ['createItem', id, name, price] as const,
};

이렇게 작성해주는 이유는 queryKeys를 통해 받은 파라미터를 api함수의 파라메터로 함께 넘겨줄 수 있기 때문에 배열로 작성합니다.

그리고 한 파일에서 모든 key를 관리하면 key가 오염되는 일을 방지할 수 있습니다.

useQuery를 통해 데이터 가져오기

  const { data, status } = useQuery(queryKeys.items, getItemAll);

  return (
    <div>
      <h1>메뉴 등록하기</h1>
      <Form />
      <div>나의 메뉴판 정보</div>
      {status === 'loading' ? (
        <span>로딩 중 입니다...</span>
      ) : (
        <List listData={data} />
      )}
    </div>

useQuery를 통해 아래와 같이 App.tsx에서 데이터를 fatching합니다.

useMutation을 통해 데이터 생성, 수정, 삭제하기

생성

src/components/Form.tsx에서 아래와 같이 useMutation을 선언합니다.

const addItem = useMutation((item: Item) => createItem(item), {
    onSuccess: (data) => {
      console.log(data);
      queryClient.invalidateQueries(queryKeys.items);
    },
    onError: (error) => {
      console.log(error);
    },
  });

...

 const onSubmit: SubmitHandler<IFormInput> = (data) => {
    const Item = {
      id: uuidv4(),
    };
    const newItem = Object.assign(Item, data);
    addItem.mutate(newItem);
  };

이후 이벤트 핸들러나 함수 내에서 mutate를 일으키면 됩니다. 이때 해당 API가 호출 성공할 경우 invalidateQueries를 통해 stale date가 모두 refatch됩니다.

저같은 경우는 zero-config를 사용했기 때문에 fatch와 동시에 stale 데이터로 취급됩니다.

삭제

src/components/List.tsx 내에 아래와 같이 선언해줍니다.

const deleteItem = useMutation((id: string) => deleteById(id), {
    onSuccess: (data) => {
      console.log(data);
      queryClient.invalidateQueries(queryKeys.items);
    },
    onError: (error) => {
      console.log(error);
    },
  });

...

 const handleDelete = (id: string) => {
    deleteItem.mutate(id);
  };

그리고 삭제하기 버튼이 눌렸을 때 해당 이벤트 핸들러가 작동하도록 해줍니다.

수정도 매우 유사하게 동작됩니다.
다만 코드 내 UI관련 state로 인해 조금 길어지나 내용은 동일합니다.

수정

const editItem = useMutation((item: Item) => createItem(item), {
    onSuccess: (data) => {
      console.log(data);
      setEditable(!editable);
      queryClient.invalidateQueries(queryKeys.items);
    },
    onError: (error) => {
      console.log(error);
    },
  });

...

 const handleEdit = (id: string, name: string, price: number) => {
    setEditable(true);
    setClickedId(id);
    setMenuInputVal(name);
    setPriceInputVal(price);
    const editObj = {
      id: clickedId,
      name: menuInputVal,
      price: priceInputVal,
    };
    editItem.mutate(editObj);
  };

이렇게 모든 코드를 작성해보았습니다.

Frontend 예제 코드

해당 내용은 모두 github에 작성되어 있으며 vercel을 통해 배포되어 있으니 간편하게 확인해주시고 부족하지만 제 코드가 도움이 되셨길 바랍니다.!

DEMO 바로 보러가기

profile
Adventure, Challenge, Consistency

9개의 댓글

comment-user-thumbnail
2022년 6월 2일

확실히 react-query가 최근에 많이 쓰이고 있는것 같아요. 잘 배우고 잘 적용해보신 것 같네요~ 좋은 글 감사합니다. :)

1개의 답글
comment-user-thumbnail
2022년 6월 7일

아직 프로젝트 경험이 많지 않아서, react-query와 같은 서버상태 관리도구나 recoil을 사용해보지 못했는데 얼른 공부해서 사용해봐야겠네요 😀
글 잘 읽었습니다~

1개의 답글
comment-user-thumbnail
2022년 6월 8일

export const getItemById = async (id: any) => {
const response = await apiClient.get(/items/$${id});
return response.data;
};

오타발견요!

1개의 답글
comment-user-thumbnail
2022년 6월 15일

좋은 글 감사합니다
이전 글에 댓글 남긴 것이 있는데 확인 부탁드려도 괜찮을까요??

1개의 답글