ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Portfolly] 포트폴리오 CRUD를 react-query로 마이그레이션
    Studying/Proj 과정 2023. 12. 25. 20:23

    들어가면서...

    기존 Portfolly 프로젝트에 react-query 라이브러리를 도입해 모든 비동기 데이터를 쿼리에 저장하고 업데이트 하는 형태로 마이그레이션 하겠습니다.

     

     

     

    Read - 포트폴리오 불러오기

    useQuery 훅으로 포트폴리오 목록 데이터를 불러오는 건 아래 게시물에서 다뤘습니다.

     

     

    [Portfolly] react-query로 불필요한 서버 요청 감소하기

    문제 상황 기존 Portfolly 홈페이지는 5개의 포트폴리오 카테고리 탭을 바꿀 때마다 서버에 새로운 데이터를 요청합니다. 데이터에 큰 변동이 없음에도 서버에 잦은 요청을 하는 건 꽤 낭비라고 생

    all-done.tistory.com

     

    포트폴리오 상세 보기 페이지에서 호출하는 포트폴리오 1개 detail 데이터를 받아오는 건, 포트폴리오 목록을 가져오는 것보다 단순하면 더 단순했지 별 차이점이 없어서 따로 기록하진 않겠습니다.

     

     

     

    Create, Update - 포트폴리오 생성/수정 하기

    저는 포트폴리오 작성/수정 API 요청을 usePortfolioPostQuery 훅에서 일괄 처리했습니다.

     

    먼저 필요한 매개변수 및 상수를 먼저 보겠습니다.

     

    '포트폴리오 수정'일 경우 수정할 포트폴리오 id를 매개변수로 받습니다.

    '포트폴리오 등록'일 경우 id는 서버에서 DB에 저장할 때 새롭게 생성하므로 매개변수로 받지 않습니다.

    export const usePortfolioPostQuery = (id?: string) => {
    	const queryClient = useQueryClient();
    	const portfolioDetailQueryKey = ['portfolios', 'detail', id];
        
    	const postPortfolio = (body: any) => fetch(`/portfolios`, 'POST', body);
    	const updatePortfolio = (body: any) => fetch(`/portfolios?id=${id}`, 'PATCH', body);
    	
        ...
    };

     

    queryClient: 포트폴리오 데이터를 수정한 뒤 queryClient 객체의 setQueryData로 쿼리를 변경하기 위해 생성합니다.

    portfolioDetailQueryKey: setQueryData로 변경할 때, 특정 쿼리를 지정하는 데 사용하는 queryKey 입니다.

    postPortfolio: POST 요청을 보내고 Promise 객체를 반환 받는 비동기 함수입니다.

    updatePortfolio: PATCH 요청을 보내고 Promise 객체를 반환 받는 비동기 함수입니다.

     

    데이터 요청 후 페이지 이동 및 전역 상태 변경이 있으므로 아래 두 개 훅도 호출합니다.

    const navigate = useNavigate();
    const dispatch = useDispatch();

     

     

    대망의 useMutation() 입니다.

    return useMutation({
        mutationFn: id? updatePortfolio : postPortfolio,
        onSuccess: (response) => {
            if(id) {
                queryClient.setQueryData(portfolioDetailQueryKey, response);
            }
            else queryClient.setQueryData(['portfolios', 'detail', response.id], response);
            // queryClient.invalidateQueries({portfolioDetailQueryKey: ['portfolios'], refetchType: 'all' });
            navigate(`/portfolios/${response.id}`);
            dispatch(setToast({id: 0, type:'success', message: '포트폴리오가 등록되었습니다.'}));
        },
        onError: () => {
            dispatch(setToast({id: 0, type:'success', message: '포트폴리오 등록에 실패했습니다.'}));
        },
    });

    mutationFn을 보면 매개변수로 id를 받았을 때, 즉 '포트폴리오 수정'인 경우 updatePortfolio 함수를 등록합니다.

    id가 존재하지 않으면 '포트폴리오 작성'이므로 postPortfolio 함수를 등록합니다.

     

    onSuccess는 비동기 요청이 정상적으로 수행됐을 때 실행되는 콜백 함수입니다.

     

    if(id)라면, 즉 '포트폴리오 수정'이라면 해당 포트폴리오 데이터가 담긴 portfolioDetailQueryKey를 가진 쿼리에 setQueryData로 값을 업데이트 해줍니다.

     

    이전 데이터에 대해 불변성을 지키면서 값을 업데이트 할 경우, 자동으로 화면을 리렌더링 해줍니다. 즉슨 수정한 포트폴리오 정보를 다시 GET 요청 하지 않고도 쿼리 값만 업데이트 해 화면을 다시 그려주는 겁니다! 저는 react-query에서 이 부분이 특히 마음에 듭니다.

     

    반대로 '포트폴리오 등록'의 경우 서버에서 생성한 idqueryKey를 생성해 업데이트 합니다.

     

    처음에는 주석과 같이 포트폴리오 목록 쿼리도 업데이트 하려고 했습니다. 그런데 저렇게 refetch해서 GET 요청을 다시 하지 말고, 똑같이 setQueryData로 업데이트 하는 방식을 사용하려고 잠시 주석 처리 했습니다.

     

    그리고 마지막으로 작성/수정한 포트폴리오 페이지로 이동한 뒤, 포트폴리오가 정상 등록되었다는 전역 Toast 컴포넌트를 띄워줍니다.

     

    에러 발생 시 onError 메서드가 실행되고, 등록에 실패했다는 전역 Toast 컴포넌트가 나타납니다.

     

     

    수정된 정보만 서버에 PATCH 하기

    그리고 react-query와 상관은 없지만... API에 update를 위해 PATCH 요청을 할 때,

    수정 된 값만 업데이트 하는 PATCH 요청의 특성을 제대로 살려 서버에 데이터를 전송할 때도 수정된 값만 전송하도록 했습니다.

     

    react-hook-form의 dirtyFields를 통해 '더럽혀진 필드' = '변경된 form 요소' 배열을 획들할 수 있습니다.

     

    저는 이를 활용해서 다음과 같이 변경된 값들만 객체로 묶어 requestBody로 전송했습니다.

    const {
        register,
        reset,
        handleSubmit,
        getValues,
        setValue,
        formState: {
            dirtyFields, // <- 여기!
            errors,
            isSubmitting
        }
    } = useForm<PortfolioFormValues>({
        mode: 'onSubmit',
        defaultValues: defaultValues,
    });
        
    const onSubmit = (form: PortfolioFormValues) => {
        const isEditMode = portfolio ? true: false;
    
        if(isEditMode) {
        	// 변경된 key만 객체에 담는다
            const copyForm: {[key: string]: any} = {...form};
            const changedKeys = Object.keys(dirtyFields);
            const changedValues: {[key: string]: any} = {};
    
            changedKeys.map((key) => {
                changedValues[key] = copyForm[key];
            });
            changedValues.section = form.section;
    
            return portfolioMutation.mutate(changedValues);
        }
    
        return portfolioMutation.mutate(form);
    };

     

     

     

    Delete - 포트폴리오 삭제하기

    다음은 포트폴리오 삭제 요청을 하는 usePortfolioDeleteQuery 훅 입니다.

     

    삭제 할 포트폴리오 id를 매개변수로 받은 뒤 요청 url에 담아 보냅니다.

    export const usePortfolioDeleteQuery = (id: string) => {
    	const navigate = useNavigate();
    	const dispatch = useDispatch();
    	const currentSection = useSelector(section);
    
    	const queryClient = useQueryClient();
    	const deletePortfolio = () => fetch(`/portfolios?id=${id}`, 'DELETE');
    
    	return useMutation({
    		mutationFn: deletePortfolio,
    		onSuccess: () => {
    			queryClient.removeQueries({queryKey: ['portfolios', 'detail', id]})
    			dispatch(setToast({id: 0, type:'success', message: '포트폴리오를 삭제했습니다.'}));
    			navigate(`/main/${toUrlParameter(currentSection)}`);
    		},
    		onError: () => {
    			dispatch(setToast({id: 0, type:'error', message: '삭제를 실패했습니다.'}));
    		},
    	});
    };

     

    성공하면 queryClient.removeQueries를 통해 해당 포트폴리오 쿼리를 삭제합니다.

     

    이때 위에서 본 setQueryData와 차이점은, 첫 번째 매개변수로 QueryKey가 아닌, QueryFilter를 받는다는 점 입니다.

     

    QueryFilter 객체는 queryKeytype 속성을 가진 객체로, 해당 필터 조건을 만족시키는 쿼리를 복수개 찾을 수 있습니다.

     

    만약 queryKey['portfolios', 'detail'] 까지만 적었다면 두 문자열을 가진 쿼리 배열을 반환 받습니다. 1번, 2번 포트폴리오를 조회해서 쿼리에 데이터가 있다면 2개가 반환되는 것입니다.

     

    저의 경우 삭제할 포트폴리오 1개만 찾으면 되므로 queryKey를 구체적으로 명시했습니다.

     

    queryFilter에 대한 자세한 내용은 아래 공식 문서를 참조할 수 있습니다.

     

    Filters | TanStack Query Docs

     

    tanstack.com

     

     

     

    마이그레이션 후기

    react-query 마이그레이션을 정말 꼭!! 해보고 싶었는데 이렇게 구현되어 보람차고 즐거운 경험이었습니다.

     

    특히 포트폴리오를 작성, 수정한 뒤 새로워진 데이터를 한 번 더 GET 요청 하지 않고, 쿼리 값만 바꾼다는 게 정말 좋았습니다.

     

    그냥 useState 상태값으로 관리할 경우 서버의 responseData를 setState로 업데이트시킬 수 있지만, react-query는 코드를 더럽히지 않으면서 queryKey로 특정 쿼리를 골라 집어 변경시킨다는 게 매력적이었습니다.

     

    또한 onSuccess, onError를 컴포넌트 내에서 처리하지 않고, 각각의 Query 훅 안에서 해결했더니 코드가 훨씬 깔끔하고 수정 용이성도 높았습니다. :D

Designed by Tistory.