1 of 38

React-Query 가

Redux 를 대체하게 된

이유.mdx

2 of 38

상태 관리

3 of 38

프론트엔드와 상태관리

  • SPA 는 하나의 페이지에서 모든 화면을 보여줘야 하기 때문에 상태 관리가 중요
  • 잦은 비동기 통신에 대한 상태도 관리가 필요

4 of 38

React 의 기본 기능을 통한 상태 관리

  • React 의 기본 기능만으로 상태관리를 하기 위해서는…

useState

useEffect

ContextAPI

useCallback

useReducer

으어어...

개발자

5 of 38

그래서 등장!!! Redux

6 of 38

그래서 편안해 졌나요?

하지만 API 통신이

출동하면 어떨까?

비동기 통신 상태 관리

긴 Boilerplate

코드

모든 기능이

Redux store 에 집중

Redux 에서 비동기 처리를 위한

미들웨어 설정 필요

미들웨어 및 에러 핸들링

직접 구현 필요

미들웨어 및 구현코드

테스트 필요

으어어...

개발자

7 of 38

그래서 등장!!! React-Query

8 of 38

그래서 React-Query 가 뭘 하는데요?

9 of 38

그래서 진짜 편안해 졌나요?

  • 컴포넌트 상태과 서버 상태 관리를 하나의 Store 에서 관리 🡪 분리 가능
  • 긴 Boilerplate 코드 🡪 대폭 감소
  • 미들웨어 구현 및 테스트 🡪 React-Query 의 기능 활용
  • 서로 다른 미들웨어 구현 방식 🡪 React-Query 의 기능과 QueryKey 를 사용하여 통합적 관리

10 of 38

그럼 Redux 를 사용한 코드부터 봅시다!

11 of 38

React 의 영원한 예제 TodoList 코드

12 of 38

컴포넌트 코드

13 of 38

모든 로직이 컴포넌트에 집중

export default function Todo() {�  const fetchTodo = async () => {

    try {

      dispatch(requestFetch());

      const todoList = await getTodo();

      dispatch(successFetch(todoList));

    } catch (err) {

      dispatch(errorFetch(err));

    }

  };

�  const handleSubmit = async (e) => {

    try {

      dispatch(requestPost());

      await addTodo(inputRef.current.value);

      dispatch(successPost());

      fetchTodo();

    } catch (err) {

      dispatch(errorPost(err));

    }};

API 통신 파트 부분이

컴포넌트에 전부 포함되어 있는 모습

14 of 38

비동기 상태 관리를 위한 긴 코드들

export default function Todo() {�  const dispatch = useDispatch();

  const data = useSelector((state) => state.fetchTodo.data);

  const fetchIsLoading = useSelector((state) => state.fetchTodo.isLoading);

  const fetchError = useSelector((state) => state.fetchTodo.error);

�  const postIsLoading = useSelector((state) => state.postTodo.isLoading);

  const postError = useSelector((state) => state.postTodo.error);

  if (fetchIsLoading || postIsLoading) return <h1>로딩 중</h1>;

�  if (fetchError || postError) return <h1>에러 발생</h1>;

�  if (data === undefined) return <h1>리스트 없음</h1>;

다양한 변수 선언 및

가독성 저해,

통일된 스타일 유지의 어려움

15 of 38

Store 코드

16 of 38

너무 긴 Boilerplate 코드

import { createStore } from "redux";

// 초기 상태

const initialState = {

  fetchTodo: {

    data: [], isLoading: false, error: undefined,

  },

  postTodo: {

    isLoading: false, error: undefined,

  },

};

// 액션 타입 정의

const REQUEST_FETCH = "REQUEST_FETCH";

const SUCCESS_FETCH = "SUCCESS_FETCH";

const ERROR_FETCH = "ERROR_FETCH";

const REQUEST_POST = "REQUEST_POST";

const SUCCESS_POST = "SUCCESS_POST";

const ERROR_POST = "ERROR_POST";

간단한 상태 하나 추가에도

액션타입, 액션함수,

리듀서 설정 필요

17 of 38

너무 긴 Boilerplate 코드

// 액션 생성자

export const requestFetch = () => ({ type: REQUEST_FETCH });

export const successFetch = (data) => ({

  type: SUCCESS_FETCH,

  payload: data,

});

export const errorFetch = (error) => ({

  type: ERROR_FETCH,

  payload: error,

});

export const requestPost = () => ({ type: REQUEST_POST });

export const successPost = () => ({ type: SUCCESS_POST });

export const errorPost = (error) => ({

  type: ERROR_POST,

  payload: error,

});

18 of 38

// 리듀서

const todoReducer = (state = initialState, action) => {

  switch (action.type) {

    case REQUEST_FETCH:

      return {

        ...state,

        fetchTodo: {

          data: undefined,

          isLoading: true,

          error: undefined,

        },

      };

    case SUCCESS_FETCH:

      return {

        ...state,

        fetchTodo: {

          data: action.payload,

          isLoading: false,

          error: undefined,

        },

      };

    case ERROR_FETCH:

      return {

        ...state,

        fetchTodo: {

          data: undefined,

          isLoading: false,

          error: action.payload,

        },

      };

    case REQUEST_POST:

      return {

        ...state,

        postTodo: {

          isLoading: true,

          error: undefined,

        },

      };

    case SUCCESS_POST:

      return {

        ...state,

        postTodo: {

          isLoading: false,

          error: undefined,

        },

      };

    case ERROR_POST:

      return {

        ...state,

        postTodo: {

          isLoading: false,

          error: action.payload,

        },

      };

    default:

      return state;

  }

};

const store = createStore(todoReducer);�export default store;

Reducer 가 가지는

코드 가독성의 문제

리팩토링 또는 로직 수정의

어려움 등등

19 of 38

이렇게 된 이상...

이렇게 된 이상

통신 파트라도

분리한다

20 of 38

Redux-thunk

도입

21 of 38

Redux 는 사실...

비동기

통신이

뭐드라...

22 of 38

Redux 는 사실...

  • Redux 는 비동기 통신 상태 관리를 위한 라이브러리가 아닙니다
  • 따라서 비동기 통신 상태에 함수 자체를 dispatch 에 넣어서 전달이 불가능 합니다. 원래는 객체만을 전달 가능
  • 하지만 프론트에서 잦은 비동기 통신 상태 관리를 Redux 를 통해 하다보니 비동기 통신을 위한 라이브러리 Redux-thunk, Redux-saga 같은 미들 웨어가 추가 되었습니다!

23 of 38

그래서 등장!!! Redux

24 of 38

Redux-thunk 적용하기

  • Redux 의 applyMiddleware 와 redux-thunk 를 사용하여 dispatch 가 함수 자체를 받을 수 있도록 세팅!

import { createStore, applyMiddleware } from "redux";

import { thunk } from "redux-thunk";

// 기존 코드

// 스토어 생성

const store = createStore(todoReducer, applyMiddleware(thunk));

25 of 38

API 통신 로직을 분리해 봅시다!

  • Store 에 API 통신 파트 부분을 구현!

export const fetchTodo = () => {

  return async (dispatch) => {

    dispatch(requestFetch());

    try {

      const data = await getTodo();

      dispatch(successFetch(data));

    } catch (error) {

      dispatch(errorFetch(error));

    }

  };

};

export const postTodo = (content) => {

  return async (dispatch) => {

    dispatch(requestPost());

    try {

      await addTodo(content);

      dispatch(successPost());

      dispatch(fetchTodo());

    } catch (error) {

      dispatch(errorPost(error));

    }

  };

};

26 of 38

이제 공통된 API 호출 파트를 불러서 사용!

  • 각 컴포넌트에서는 Store 에 공통으로 선언된 API를 불러서 사용

export default function Todo() {�  const dispatch = useDispatch();

  const data = useSelector((state) => state.fetchTodo.data);

  const fetchIsLoading = useSelector((state) => state.fetchTodo.isLoading);

  const fetchError = useSelector((state) => state.fetchTodo.error);�  const postIsLoading = useSelector((state) => state.postTodo.isLoading);

  const postError = useSelector((state) => state.postTodo.error);

�  useEffect(() => {

    dispatch(fetchTodo());

  }, [dispatch]);

�  const handleSubmit = (e) => {

    dispatch(postTodo(inputRef.current.value));

  };

27 of 38

컴포넌트 코드가 많이 줄었습니다!

const fetchTodo = async () => {

    try {

      dispatch(requestFetch());

      const todoList = await getTodo();

      dispatch(successFetch(todoList));

    } catch (err) {

      dispatch(errorFetch(err));

    }

  };

�  const handleSubmit = async (e) => {

    e.preventDefault();

    try {

      dispatch(requestPost());

      await addTodo(inputRef.current.value);

      dispatch(successPost());

      fetchTodo();

    } catch (err) {

      dispatch(errorPost(err));

    }

  };

�  useEffect(() => {

    fetchTodo();

  }, [dispatch]);

useEffect(() => {

    dispatch(fetchTodo());

  }, [dispatch]);

�  const handleSubmit = (e) => {

    dispatch(postTodo(inputRef.current.value));

  };

컴포넌트 담당자는

통신 로직과 상관 없이

개발이 가능!

28 of 38

그럼에도...

  • 아직 Store 코드가 너무 길고, 많은 기능이 집중 되어 있습니다
  • 서버 사이드의 상태와 컴포넌트의 상태를 같은 Store 가 관리 합니다
  • 각기 다른 개발자가 API 통신 파트를 개발하게 되면, 공통된 스타일의 유지가 어렵습니다

29 of 38

진짜 등장!!! React-Query

30 of 38

React-Query 적용하기!

import Todo from "./Todo";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {

  return (

    <QueryClientProvider client={queryClient}>

      <Todo />

    </QueryClientProvider>

  );

}

export default App;

31 of 38

React-Query 적용하기! 데이터 받아오기

// React Query

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

export default function Todo() {

  const { data, isLoading, isError } = useQuery({

    queryKey: ["todo"],

    queryFn: () => getTodo(),

  });

  if (isLoading) return <h1>로딩 중</h1>;

�  if (isError) return <h1>에러 발생</h1>;

�  if (data === undefined) return <h1>리스트 없음</h1>;

32 of 38

React-Query 적용하기! 데이터 보내기

export default function TodoQuery() {�  const postTodoMutation = useMutation({

    mutationFn: (todo) => {

      addTodo(todo);

    },

    onSuccess: () => {

      queryClient.invalidateQueries({ queryKey: ["todo"] });

    },

  });��const handleSubmit = (e) => {

    e.preventDefault();�    postTodoMutation.mutate(inputRef.current.value, {

      onSuccess: () => alert("Todo 등록 성공"),

      onError: () => alert("Todo 등록 실패"),

    });�    inputRef.current.value = "";

    queryClient.invalidateQueries();

  };

33 of 38

그래서 우리가 얻은 것은!?

34 of 38

짧아진 코드!

import { createStore, applyMiddleware } from "redux";

import { thunk } from "redux-thunk";

import { getTodo, addTodo } from "./api"; // API 호출을 하는 함수들

// 기존 코드

const initialState = {

  fetchTodo: {

    data: [],

    isLoading: false,

    error: undefined,

  },

  postTodo: {

    isLoading: false,

    error: undefined,

  },

};

// 액션 타입 정의

const REQUEST_FETCH = "REQUEST_FETCH";

const SUCCESS_FETCH = "SUCCESS_FETCH";

const ERROR_FETCH = "ERROR_FETCH";

const REQUEST_POST = "REQUEST_POST";

const SUCCESS_POST = "SUCCESS_POST";

const ERROR_POST = "ERROR_POST";

// 액션 생성자

export const requestFetch = () => ({ type: REQUEST_FETCH });

export const successFetch = (data) => ({

  type: SUCCESS_FETCH,

  payload: data,

});

export const errorFetch = (error) => ({

  type: ERROR_FETCH,

  payload: error,

});

export const requestPost = () => ({ type: REQUEST_POST });

export const successPost = () => ({ type: SUCCESS_POST });

export const errorPost = (error) => ({

  type: ERROR_POST,

  payload: error,

});

export const fetchTodo = () => {

  return async (dispatch) => {

    dispatch(requestFetch());

    try {

      const data = await getTodo();

      dispatch(successFetch(data));

    } catch (error) {

      dispatch(errorFetch(error));

    }

  };

};

export const postTodo = (content) => {

  return async (dispatch) => {

    dispatch(requestPost());

    try {

      await addTodo(content);

      dispatch(successPost());

      dispatch(fetchTodo());

    } catch (error) {

      dispatch(errorPost(error));

    }

  };

};

// 리듀서

const todoReducer = (state = initialState, action) => {

  switch (action.type) {

    case REQUEST_FETCH:

      return {

        ...state,

        fetchTodo: {

          data: undefined,

          isLoading: true,

          error: undefined,

        },

      };

    case SUCCESS_FETCH:

      return {

        ...state,

        fetchTodo: {

          data: action.payload,

          isLoading: false,

          error: undefined,

        },

      };

    case ERROR_FETCH:

      return {

        ...state,

        fetchTodo: {

          data: undefined,

          isLoading: false,

          error: action.payload,

        },

      };

    case REQUEST_POST:

      return {

        ...state,

        postTodo: {

          isLoading: true,

          error: undefined,

        },

      };

    case SUCCESS_POST:

      return {

        ...state,

        postTodo: {

          isLoading: false,

          error: undefined,

        },

      };

    case ERROR_POST:

      return {

        ...state,

        postTodo: {

          isLoading: false,

          error: action.payload,

        },

      };

    default:

      return state;

  }

};

// 스토어 생성

const store = createStore(todoReducer, applyMiddleware(thunk));

export default store;

export default function TodoQuery() {

  const inputRef = useRef();

  const queryClient = useQueryClient();

�  const { data, isLoading, isError } = useQuery({

    queryKey: ["todo"],

    queryFn: () => getTodo(),

  });

�  const postTodoMutation = useMutation({

    mutationFn: (todo) => {

      addTodo(todo);

    },

    onSuccess: () => {

      queryClient.invalidateQueries({ queryKey: ["todo"] });

    },

  });

�  const handleSubmit = (e) => {

    e.preventDefault();

�    postTodoMutation.mutate(inputRef.current.value, {

      onSuccess: () => alert("Todo 등록 성공"),

      onError: () => alert(" 등록 실패"),

    });

�    inputRef.current.value = "";

    queryClient.invalidateQueries();

  };

35 of 38

짧아진 코드!

  • 상태 관리를 위해 redux 의 액션 타입 정의, 액션 함수 생성, 리듀서 추가 등의 귀찮을 일을 안해도 됩니다!
  • 컴포넌트에서 간단하게 API 호출만 하게 되므로 해당 컴포넌트가 어떤 일을 하는지 쉽게 알 수 있습니다

36 of 38

쉬운 서버 상태 관리가 가능

  • 서버 상태 관리를 React-query 에서 제공하는 공통 된 상태로 관리가 가능
  • 서버 상태 관리를 위한 불필요한 코드 대폭 축소 가능
  • 서버 상태에 따른 리렌더링 및 에러 핸들링을 쉽게 처리 가능
  • 서로 다른 API 호출 로직으로 인한 협업 시의 효율 감소 방지

37 of 38

38 of 38

아직도

React-query,

Zustand 안쓰니?