server-state-management.md

윤뿔소·2023년 12월 19일
0
post-thumbnail

비동기 및 서버 상태 관리

개발 경험 및 효율성 증대를 위해 클라이언트와 비동기 및 서버 상태 관리를 분리하여 개발하려고 하고, 비동기 및 서버 상태 관리를 맡아주는 라이브러리 설치하려고 함.

서버에서 비동기적으로 가져올 수 있는 데이터들을 관리하기 위해 로직을 따로 구현해도 되지만 라이브러리를 사용해 서버 데이터 감시 및 관리를 편하게 하기 위해 라이브러리를 사용하고자 함.

Tanstack React-Query

React 애플리케이션에서 비동기 로직을 관리하는 데 사용되는 라이브러리. 즉, 서버 상태와 비동기 상태들을 관리해주는 관리자임.

비동기 또는 서버 상태 작업을 용이하게 하기 위해 서버 상태 가져오기 및 비동기 상태 처리, 캐싱과 서버 상태와의 동기화, 업데이트를 도와줌.

React-Query 고른 이유

가장 큰 이유는 클라이언트 전용 상태 관리 라이브러리는 비동기 데이터들을 관리하는데 한계가 명확함. 데이터가 오래됐음에도(Stale) Refetch하지 않는다든지, 데이터 혼합이 발생한다는지 등.

  1. 데이터 관리의 편의성
    리액트 쿼리는 서버데이터를 가져오고 업데이트하는 과정을 추상화하고 간소화해줌. 캐싱, 자동 Refetch, 에러 핸들링 등과 같은 기능을 내장하고 있어 데이터 관리를 간편하게 수행해 개발 경험 증대.
  2. Boilerplate 코드 감소
    Redux와 다르게 Boilerplate 코드가 줄어듦. 불필요한 작업을 안해도 될 뿐만 아니라 소스코드 복잡도를 낮춰 유지/보수 효율성을 늘리고, 사전에 에러를 잘 막을 수도 있음. 차이점은 아래 Redux(saga, toolkit 등)와 React-Query의 차이점을 보면 더 잘 보임.
  3. API 요청 수행을 위한 규격화된 방식 통일 가능
    팀의 구성원이 많아지거나 프로젝트 규모가 커질 수록 API 요청 건에 관련해서 다양한 API 응답 처리법이 늘어날 것임.
  4. 성능 최적화
    캐싱된 데이터들 중복 요청 방지 및 필요 시 알아서 데이터 Refetch 등 개발자가 쉽게 설정할 수 있어 웹 성능 증가.
  5. 서버/비동기 상태 관리의 용이
    리액트 쿼리로 통합해 비동기 로직을 관리할 수 있어 단순화돼 유지/보수 용이. 심지어 웹 어플리케이션의 특성에 따라 클라이언트 상태 라이브러리를 사용하지 않아도 무방.
  6. React-Query with fetch of this project
    하려는 프로젝트에서 서버와의 통신의 횟수가 많다면 사용하는게 더 좋을 수 있음. 비동기적인 기능이 비교적 많은 편에 속한다면 리액트 쿼리로 관리하는 것이 프로젝트 구조가 단순화돼 개발 및 유지/보수에 더욱 효율적.

Redux(saga, toolkit 등)와 React-Query의 차이점

카카오페이 기술 블로그를 참조했다.

Redux saga : Redux saga로 서버 데이터를 관리할 때

// features/todos/todos.slice.ts
// API 상태를 관리하기 위한 Redux State
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { TodoItem } from 'types/todo';

export interface TodoListState {
  data?: TodoItem[];
  isLoading: boolean;
  error?: Error;
}

const initialState: TodoListState = {
  data: undefined,
  isLoading: false,
  error: undefined,
};

export const todoListSlice = createSlice({
  name: 'todoList',
  initialState,
  reducers: {
    requestFetchTodos: (state) => {
      state.isLoading = true;
    },
    successFetchTodos: (state, action: PayloadAction<TodoItem[]>) => {
      state.data = action.payload;
      state.isLoading = false;
      state.error = undefined;
    },
    errorFetchTodos: (state, action: PayloadAction<string>) => {
      state.data = undefined;
      state.isLoading = false;
      state.error = action.payload;
    },
  },
});

export const { requestFetchTodos, successFetchTodos, errorFetchTodos } =
  todoListSlice.actions;

export default todoListSlice.reducer;
// features/todos/todos.saga.ts
import { PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';
import { call, put, takeEvery } from 'redux-saga/effects';
import { TodoItem } from '../../types/todo';
import {
  errorFetchTodos,
  errorPostTodos,
  requestFetchTodos,
  requestPostTodos,
  successFetchTodos,
  successPostTodos,
} from './todos.slice';

async function getTodoList() {
  const { data } = await axios.get<TodoItem[]>('./todos');
  return data;
}

function* requestFetchTodoTask() {
  try {
    const data: TodoItem[] = yield call(getTodoList);
    yield put(successFetchTodos(data));
  } catch (e) {
    yield put(errorFetchTodos(e.message));
  }
}

async function postTodoList(contents: string) {
  await axios.post('/todos', { contents });
}

function* requestPostTodoTask(action: PayloadAction<string>) {
  try {
    yield call(postTodoList, action.payload);
    yield put(successPostTodos());
  } catch (e) {
    yield put(errorPostTodos(e.message));
  }
}

function* successPostTodoTask() {
  // 서버에 새로운 Todo 추가 요청 성공 시
  // 서버에서 Todo 목록을 다시 받아오기 위해 Action Dispatch
  yield put(requestFetchTodos());
}

function* todoListSaga() {
  yield takeEvery(requestFetchTodos.type, requestFetchTodoTask);
  yield takeEvery(requestPostTodos.type, requestPostTodoTask);
  yield takeEvery(successPostTodos.type, successPostTodoTask);
}

export default todoListSaga;

React-Query : React-Query로 서버 데이터를 관리할 때

// quires/QTdeeoorssuuy.ts
// API 상태를 불러오기 위한 React Query Custom Hook
import axios from 'axios';
import { useQuery } from 'react-query';
import { TodoItem } from 'types/todo';

// useQuery에서 사용할 UniqueKey를 상수로 선언하고 export로 외부에 노출합니다.
// 상수로 UniqueKey를 관리할 경우 다른 Component (or Custom Hook)에서 쉽게 참조가 가능합니다.
export const QUERY_KEY = '/todos';

// useQuery에서 사용할 `서버의 상태를 불러오는데 사용할 Promise를 반환하는 함수`
const fetcher = () => axios.get<TodoItem[]>('/todos').then(({ data }) => data);

const useTodosQuery = () => {
  return useQuery({ queryKey: QUERY_KEY, queryFn: fetcher });
};

export default useTodosQuery;

빠르게 보는 사용 예시

// 코드를 받는 최상위 파일 :  _app.tsx, layout.tsx 등
import {
  useQuery,
  useMutation,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query';
import { getTodos, postTodo } from '../my-api';

/// QueryClient 만들기
// 1. 그냥 선언
const queryClient = new QueryClient();
// 2. useState 선언 + 기본 설정
const [client] = useState(
  new QueryClient({
    defaultOptions: {
      queries: {
        refetchOnWindowFocus: false, // 윈도우가 다시 포커스되었을때 데이터를 refetch
        refetchOnMount: false, // 데이터가 stale 상태이면 컴포넌트가 마운트될 때 refetch
        retry: 1, // API 요청 실패시 재시도 하는 옵션 (설정값 만큼 재시도)
      },
    },
  }),
);

function App() {
  return (
    // 우리 App 루트에 Provider 설정(필수 설정)
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  );
}

function Todos() {
  // queryClient 접근
  const queryClient = useQueryClient();

  // useQuery
  const query = useQuery({ queryKey: ['todos'], queryFn: getTodos });

  // Mutations
  const mutation = useMutation({
    mutationFn: postTodo,
    onSuccess: () => {
      // Invalidate 및 refetch
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <div>
      <ul>{query.data?.map((todo) => <li key={todo.id}>{todo.title}</li>)}</ul>

      <button
        onClick={() => {
          mutation.mutate({
            id: Date.now(),
            title: 'Do Laundry',
          });
        }}
      >
        Add Todo
      </button>
    </div>
  );
}

render(<App />, document.getElementById('root'));
profile
코뿔소처럼 저돌적으로

0개의 댓글