Recent Posts
«   2025/09   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
관리 메뉴

개발자 박가나

[241127 TIL] 본캠프 41일차 (TanStack Query 실습) 본문

내일배움캠프

[241127 TIL] 본캠프 41일차 (TanStack Query 실습)

gnchoco97 2024. 11. 27. 22:06

챌린지반에서 TanStack Query 실습을 진행하였다.

 

TanStack Query를 사용하는 이유를 이해하고 사용법에 익숙해지는 것이 목적이었기 때문에 초기에는 App.jsx 파일에 모든 코드가 모여있는 상태였고, 다음과 같은 순서에 따라 코드 리팩토링을 진행하였다. 

  • API 관련 코드를 별도의 파일로 분리
  • Axios Instance 생성
  • Axios Interceptor 생성
  • useQuery 구현
  • useMutation 구현
  • invalidateQueries 사용
  • useQuery를 Custom Hook으로 분리
  • useMutation을 Custom Hook으로 분리
  • 7~8단계에서 생성한 Custom Hook을 하나의 Hook으로 통합
  • 낙관적 업데이트 적용

 

초기 코드

/* App.jsx */

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState("");

  const fetchTodos = async () => {
    const { data } = await axios.get("http://localhost:3000/todos");
    setTodos(data);
  };

  const addTodo = async () => {
    if (!newTodo.trim()) return;
    const { data } = await axios.post("http://localhost:3000/todos", {
      title: newTodo,
      completed: false,
    });
    setTodos((prev) => [...prev, data]);
    setNewTodo("");
  };

  const deleteTodo = async (id) => {
    await axios.delete(`http://localhost:3000/todos/${id}`);
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  };

  const toggleComplete = async (id, completed) => {
    const { data } = await axios.patch(`http://localhost:3000/todos/${id}`, {
      completed: !completed,
    });
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: data.completed } : todo,
      ),
    );
  };

  useEffect(() => {
    fetchTodos();
  }, []);
}

export default TodoList;

 

 

변경 코드

main / App

/* main.jsx */

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

createRoot(document.getElementById('root')).render(
    <StrictMode>
        <QueryClientProvider client={queryClient}>
            <App />
        </QueryClientProvider>
    </StrictMode>
);
/* App.jsx */

import { useState } from 'react';
import { useTodos } from './hooks/useTodos';

function TodoList() {
    const [newTodo, setNewTodo] = useState('');

    const { data: todos, isPending, isError, addMutation, deleteMutation, toggleMutation } = useTodos();

    const addTodo = async () => {
        if (!newTodo.trim()) return;
        addMutation.mutate(newTodo);
        setNewTodo('');
    };

    const deleteTodo = async (id) => {
        deleteMutation.mutate(id);
    };

    const toggleComplete = async (id, completed) => {
        toggleMutation.mutate({ id, completed: !completed });
    };

    if (isPending) return <div>로딩 중...</div>;

    if (isError) return <div>데이터를 불러오는 과정에서 오류 발생</div>;
}

 

 

API

/* src/api/axiosInstance.js */

import axios from 'axios';

const todosAPI = axios.create({
    baseURL: 'http://localhost:3000/todos',
    timeout: 5000
});

todosAPI.interceptors.request.use((config) => {
    console.log(config.baseURL);
    return config;
});

todosAPI.interceptors.response.use(
    (response) => {
        console.log(response.data);
        return response;
    },
    (error) => {
        console.error(error);
        return Promise.reject(error);
    }
);

export default todosAPI;
/* src/api/todos.js */

import todosAPI from './axiosInstance';

export const fetchTodosAPI = async () => {
    const response = await todosAPI.get('/');
    return response.data;
};

export const addTodoAPI = async (title) => {
    const response = await todosAPI.post('/', {
        title,
        completed: false
    });
    return response.data;
};

export const deleteTodoAPI = async (id) => {
    await todosAPI.delete(`/${id}`);
};

export const toggleCompleteAPI = async ({ id, completed }) => {
    const response = await todosAPI.patch(`/${id}`, {
        completed
    });
    return response.data;
};

 

 

Custom Hook

/* src/hooks/useFetchTodos.js */

import { useQuery } from '@tanstack/react-query';
import { fetchTodosAPI } from '../api/todos';

export const useFetchTodos = () => {
    return useQuery({
        queryKey: ['todos'],
        queryFn: fetchTodosAPI
    });
};
/* src/hooks/useAddTodo.js */

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { addTodoAPI } from '../api/todos';

export const useAddTodo = () => {
    const queryClient = useQueryClient();

    const mutation = useMutation({
        mutationFn: addTodoAPI,
        onMutate: async (newTodo) => {
            await queryClient.cancelQueries({
                queryKey: ['todos']
            });

            const prevTodos = queryClient.getQueryData(['todos']);

            queryClient.setQueryData(['todos'], (prev) => [...prev, newTodo]);

            return { prevTodos };
        },
        onError: (error, context) => {
            console.log(error);
            queryClient.setQueryData(['todos'], context.prevTodos);
        },
        onSettled: () => {
            queryClient.invalidateQueries({
                queryKey: ['todos']
            });
        }
    });

    return { addMutation: mutation };
};
/* src/hooks/useDeleteTodo.js */

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { deleteTodoAPI } from '../api/todos';

export const useDeleteTodo = () => {
    const queryClient = useQueryClient();

    const mutation = useMutation({
        mutationFn: deleteTodoAPI,
        onMutate: async (id) => {
            await queryClient.cancelQueries({
                queryKey: ['todos']
            });

            const prevTodos = queryClient.getQueryData(['todos']);

            queryClient.setQueryData(['todos'], (prev) => prev.filter((todo) => todo.id !== id));

            return { prevTodos };
        },
        onError: (error, context) => {
            console.log(error);
            queryClient.setQueryData(['todos'], context.prevTodos);
        },
        onSettled: () => {
            queryClient.invalidateQueries({
                queryKey: ['todos']
            });
        }
    });

    return { deleteMutation: mutation };
};
/* src/hooks/useToggleTodo.js */

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toggleCompleteAPI } from '../api/todos';

export const useToggleTodo = () => {
    const queryClient = useQueryClient();

    const mutation = useMutation({
        mutationFn: toggleCompleteAPI,
        onMutate: async ({ id, completed }) => {
            await queryClient.cancelQueries({
                queryKey: ['todos']
            });

            const prevTodos = queryClient.getQueryData(['todos']);

            queryClient.setQueryData(['todos'], (prev) => prev.map((todo) => (todo.id === id ? { ...todo, completed } : todo)));

            return { prevTodos };
        },
        onError: (error, context) => {
            console.log(error);
            queryClient.setQueryData(['todos'], context.prevTodos);
        },
        onSettled: () => {
            queryClient.invalidateQueries({
                queryKey: ['todos']
            });
        }
    });

    return { toggleMutation: mutation };
};
/* src/hooks/useTodos.js */

import { useFetchTodos } from './useFetchTodos';
import { useAddTodo } from './useAddTodo';
import { useDeleteTodo } from './useDeleteTodo';
import { useToggleTodo } from './useToggleTodo';

export const useTodos = () => {
    const { data, isPending, isError } = useFetchTodos();
    const { addMutation } = useAddTodo();
    const { deleteMutation } = useDeleteTodo();
    const { toggleMutation } = useToggleTodo();

    return { data, isPending, isError, addMutation, deleteMutation, toggleMutation };
};