React-Query + Gemini

단단·2025년 7월 23일
post-thumbnail

프론트엔드 입사 3일차,
React-Query를 사용하여 프로필 설정 부분 생성된 API를 (아직은 미완성이지만 된 부분만)
가져와서 UI에 뿌려주는 과제를 받게 되었다.

해결과정

  1. 실제 백엔드 API와 통신하고 데이터를 화면에 표시하는 부분 찾기
    -폴더 구조 파악도 안 된 상태에서 실제로 통신이 이루어진 부분을 찾기란 쉽지않았다.
    -GeminiCLI를 사용해서 useQuery가 사용된 파일을 검색해달라고 부탁해서 코드를 확인했다.

    -useQuery를 사용하는 파일이 많아서 src>hooks>server>useInquiryAllGetQuery.tsx파일을 예시로 데이터 호출 및 처리 과정을 확인하라고 함
    -먼저 useInquiryAllGetQuery.tsx 커스텀 훅이 어떻게 useQuery를 사용하여 API를 호출하고 있는지 살펴봄
    useInquiryAllGetQuery.tsx

import { InquireType, InquiryAllGetReq } from '@/types/model/inquiry';
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
import queryKey from 'constants/queryKeys';
import { useEffect } from 'react';
import { getInquiryAll } from 'services/inquiry';
import { Pagination } from 'types/pagination';
import { Request } from 'types/request';

const useInquiryAllGetQuery = (request: InquiryAllGetReq) => {
    const queryClient = useQueryClient();
    const {
        path: { companyId },
    } = request;
    const result = useSuspenseQuery({
        queryKey: [queryKey.getInquiryAll, JSON.stringify(request)],

        queryFn: async () => {
            if (!companyId) {
                return { body: [], page: 1, size: 20, total: 0 };
            }
            const response = await getInquiryAll(request);
            return response;
        },
        // throwOnError: true,
        // placeholderData: (previousData) => previousData,
    });

    useEffect(() => {
        if (!result.data) return;

        const { page, size, total } = result.data;
        const nextPage = page + 1;

        if (nextPage * size - size < total) {
            const nextRequest: InquiryAllGetReq = {
                path: { companyId: request.path.companyId },
                query: {
                    ...request.query,
                    pagination: {
                        page: nextPage,
                        size,
                        total,
                    },
                },
                body: {}, // 유지
            };

            queryClient.prefetchQuery({
                queryKey: [queryKey.getVisitHistoryAll, JSON.stringify(nextRequest)],
                queryFn: () => getInquiryAll(nextRequest),
            });
        }
    }, [result.data, queryClient, request]);

    const safeData = result.data
        ? {
              inquiries: result.data.body,
              pagination: {
                  page: result.data.page,
                  size: result.data.size,
                  total: result.data.total,
              },
          }
        : {
              inquiries: [],
              pagination: { page: 1, size: 20, total: 0 },
          };

    return {
        ...result,
        data: safeData,
    };
};

export default useInquiryAllGetQuery;

-그런데 useInquiryAllGetQuery를 사용하는 컴포넌트가 많아서
그 중에서 src>pages>inquiries>pages>indexes>pages>InquiryPage>components>InquityTableBox>components>UserInquiryTableBox>UserInquiryTableBox.tsx파일을 예시로 봄

import BasicTable from 'headful/Table/BasicTable';
import React from 'react';
import EmptyBoundary from '@/components/EmptyBoundary/EmptyBoundary';
import EmptyTable from 'headful/EmptyTable/EmptyTable';
import useInquiryAllGetQuery from '@/hooks/server/useInquiryAllGetQuery';
import { useCompanySelectProvider } from '@/components/CompanySelectProvider/CompanySelectProvider';
**import useCompanyGetQuery from '@/hooks/server/useCompanyGetQuery';**
import { useInquiryAllGetProvider } from '../../../InquiryAllGetProvider/InquiryAllGetProvider';
import InquiryColumns from '../InquiryColumns/InquiryColumns';
import ContentBox from '@/headful/ContentBox/ContentBox';

type UserInquiryTableBoxProps = {};

const UserInquiryTableBox = ({}: UserInquiryTableBoxProps) => {
    const { pagination: p, selectedSite, dateRange } = useInquiryAllGetProvider();
    const { company, site } = useCompanySelectProvider();

    const {
        data: { inquiries },
    } = useInquiryAllGetQuery({
        path: { companyId: company?.uuid as string },
        query: { pagination: p, siteUrl: site as string, dateRange },
        body: {},
    });

    const {
        data: { company: companyData },
    } = useCompanyGetQuery({ path: { companyId: company?.uuid as string }, query: {}, body: {} });

    const companyColumns = companyData.columns?.split(',') ?? [];

    const inquiryColumns = InquiryColumns({ inquiries });
    const desiredColumnKeys = ['seq', 'createdDate', 'createdTime', 'clientPhone', 'device', 'ip', ...companyColumns];
    const filteredColumns = inquiryColumns.filter((col) => desiredColumnKeys.includes(col.key));

    return (
        <ContentBox title={'문의내역'}>
            <EmptyBoundary data={inquiries} fallback={<EmptyTable text="문의 내역이 없습니다." />}>
                <BasicTable>
                    <BasicTable.Header>
                        {filteredColumns.map(({ key, label }) => (
                            <BasicTable.Cell asHeader align="left" key={key}>
                                {label}
                            </BasicTable.Cell>
                        ))}
                    </BasicTable.Header>
                    <BasicTable.Body>
                        {inquiries?.map((item: any, rowIndex: number) => (
                            <BasicTable.Row key={item.uuid}>
                                {filteredColumns.map((col) =>
                                    // render 함수에서 생성된 JSX에 key를 부여합니다.
                                    React.cloneElement(col.render(item, rowIndex), { key: col.key + col.label })
                                )}
                            </BasicTable.Row>
                        ))}
                    </BasicTable.Body>
                </BasicTable>
            </EmptyBoundary>
        </ContentBox>
    );
};

export default UserInquiryTableBox;

-이파일에서 useInquiryAllGetQuery 훅을 호출하여 얻은 데이터를 테이블 형태로 화면에 표시한다.
-즉, 훅을 호출하여 문의 데이터를 가져오고, BasicTable 컴포넌트를 사용해서 화면에 테이블 형태로 데이터를 뿌려줌

데이터 호출 :
src/hooks/server/useInquiryAllGetQuery.tsx 파일에서 useSuspenseQuery(react-query의 useQuery와 유사)를 사용하여 getinquiryAll API 함수를 호출하고 데이터를 가져옴.

데이터 표시 :
src/pages/inquiries/pages/indexes/pages/InquiryPage/components/InquiryTableBox/components/UserInquiryTableBox/UserInquiryTableBox.tsx 컴포넌트에서 useInquiryAllGetQuery 훅을 사용해서 데이터를 받아온 후 BasicTable 컴포넌트를 이용해 해당 데이터를 화면에 렌더링 함.

그럼 이를 참고해서 데이터를 표시해야되는 UserInfo.tsx(프로필설정) 의 Query.ts파일도 만들어보자

  1. 먼저 사용자 정보를 가져오는 API함수가 어디있는지 확인해야 한다.
    -services 디렉터리에서 관련 함수를 찾아봄
    -getUserAll은 모든 유저를 가져오는 함수로 파악됨. 현재 로그인된 특정 유저의 정보를 가져오는 함수가 필요하다. user.ts파일을 직접 읽어서 확인하자
    -getUserAll 함수만으로는 현재 로그인한 사용자의 정보를 가져올 수 없다. auth.ts파일에 현재 정보를 가져오는 함수가 있는지 확인하자

getContext : 현재 로그인한 사용자의 정보를 가져오는 함수임

  1. useUserContextGetQuery.ts 파일 생성하기

import { useQuery } from '@tanstack/react-query';
import { getContext } from '@/services/auth';
import queryKey from '@/constants/queryKeys';

const useUserContextGetQuery = () => {
    const result = useQuery({
        queryKey: [queryKey.getContext],
        queryFn: getContext,
    });

    return result;
};

export default useUserContextGetQuery;
  1. UserInfo.tsx파일 수정
    -useUserContextGetQuery 훅을 사용하고, 가져온 데이터를 상태에 반영하기
import React, { useEffect, useState } from 'react';
import styles from './UserInfo.module.scss';
import InputA from '@/ui-kit/src/components/contents/Input/A/InputA';
import ColorChip from '@/headful/ColorChip/ColorChip';
import BasicModal from '@/headful/BasicModal/BasicModal';
import ButtonB from '@/ui-kit/src/components/contents/Button/B/ButtonB';
import UserPasswordChangeForm from '../UserPasswordChangeForm/UserPasswordChangeForm';
import useUserContextGetQuery from '@/hooks/server/users/useUserContextGetQuery';

const UserInfo = () => {
    const { data: userData } = useUserContextGetQuery();
    const [userInfo, setUserInfo] = useState({
        name: '',
        role: '',
        account: '',
        phone: '',
    });

    useEffect(() => {
        if (userData) {
            setUserInfo({
                name: userData.body.name,
                role: userData.body.role,
                account: '', // API에 없음
                phone: '', // API에 없음
            });
        }
    }, [userData]);

    const handleSave = () => {
        alert(JSON.stringify(userInfo));
    };

    return (
        <div className={styles.UserInfo}>
            <div className={styles.FormItem}>
                <div className={styles.Label}>
                    <span>이름</span>
                </div>
                <div className={styles.Input}>
                    <UserInfoInput
                        value={userInfo.name}
                        onChange={(e) => setUserInfo({ ...userInfo, name: e.target.value })}
                    />
                </div>
            </div>
            <div className={styles.FormItem}>
                <div className={styles.Label}>
                    <span>직급</span>
                </div>
                <div className={styles.Input}>
                    <UserInfoInput
                        value={userInfo.role}
                        onChange={(e) => setUserInfo({ ...userInfo, role: e.target.value })}
                    />
                </div>
            </div>
            <div className={styles.FormItem}>
                <div className={styles.Label}>
                    <span>권한</span>
                </div>
                <div className={styles.Input}>
                    <ColorChip bgColor="#D8F5DA" color="#1D5C21" text="관리자" />
                </div>
            </div>
            <div className={styles.FormItem}>
                <div className={styles.Label}>
                    <span>계정</span>
                </div>
                <div className={styles.Input}>
                    <UserInfoInput
                        value={userInfo.account}
                        onChange={(e) => setUserInfo({ ...userInfo, account: e.target.value })}
                    />
                </div>
            </div>
            <div className={styles.FormItem}>
                <div className={styles.Label}>
                    <span>비밀번호</span>
                </div>
                <div className={styles.Input}>
                    <UserInfoInput type="password" placeholder="비밀번호" />
                </div>
            </div>
            <BasicModal>
                <BasicModal.Trigger>
                    <ButtonB
                        bgColor="#F8F8F8"
                        color="#333333"
                        width="124px"
                        height="45px"
                        style={{ border: '1px solid #E2E2E2', marginBottom: '8px' }}
                    >
                        비밀번호 변경
                    </ButtonB>
                </BasicModal.Trigger>
                <BasicModal.Backdrop />
                <BasicModal.Content title="비밀번호 변경" width="550px" height="460px">
                    <UserPasswordChangeForm />
                </BasicModal.Content>
            </BasicModal>
            <div className={styles.FormItem}>
                <div className={styles.Label}>
                    <span>휴대폰 번호</span>
                </div>
                <div className={styles.Input} style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
                    <UserInfoInput
                        width={84}
                        maxLength={3}
                        value={userInfo.phone.slice(0, 3)}
                        onChange={(e) =>
                            setUserInfo({
                                ...userInfo,
                                phone: e.target.value + userInfo.phone.slice(3),
                            })
                        }
                    />
                    <span>-</span>
                    <UserInfoInput
                        width={84}
                        maxLength={4}
                        value={userInfo.phone.slice(3, 7)}
                        onChange={(e) =>
                            setUserInfo({
                                ...userInfo,
                                phone:
                                    userInfo.phone.slice(0, 3) +
                                    e.target.value +
                                    userInfo.phone.slice(7),
                            })
                        }
                    />
                    <span>-</span>
                    <UserInfoInput
                        width={84}
                        maxLength={4}
                        value={userInfo.phone.slice(7)}
                        onChange={(e) =>
                            setUserInfo({
                                ...userInfo,
                                phone: userInfo.phone.slice(0, 7) + e.target.value,
                            })
                        }
                    />
                </div>
            </div>
            <ButtonB
                bgColor="#007AFF"
                color="#FFFFFF"
                width="124px"
                height="45px"
                onClick={handleSave}
            >
                저장
            </ButtonB>
        </div>
    );
};

export default UserInfo;

const UserInfoInput = ({ ...props }: React.ComponentProps<typeof InputA>) => (
    <InputA s padding={{ y: 15, x: 20 }} height={45} width={props.width || '100%'} {...props} />
);

-UserInfo.tsx 파일에 useUserContextGetQuery훅을 사용하여 사용자 정보를 가져오고, useEffect로 상태를 업데이트하며, 저장 버튼 클릭 시 alert 창으로 현재 상태를 보여주는 기능을 추가함.
-> 이제 프로필 설정 페이지에서 실제 사용자 데이터를 불러와 수정하고, 저장 버튼읠 눌러 변경된 데이터를 확인할 수 있다!

But 콘솔에러 발생

내용 > 컴파일 오류 발생 관련,, 어쩌구 저쩌구
이유 > rank, email, phone 속성이 없다는 메시지
User 타입 정의를 확인해보자

User타입에는 uid, name, role, companyUuid만 있고, UserAllGetItem rank가 아닌 position과 email, phone이 포함되어 있어서 그런 것
getContext API가 User타입을 반환하므로, Userinfo.tsx에서 User타입의 속성을 사용하도록 수정해야 한다.
또한, getContext API 응답에는 position, email, phone 정보가 포함되어 있지 않기 때문에 이런 오류가 발생한 것.

getContext 함수를 다시 확인해서 어떤 정보를 반환하는지 파악하자
-> 확인결과 : getContext는 User타입을 반환하므로 name과 role만 사용할 수 있음
UserInfo.tsx를 다시 수정해서 User타입에 맞게 name과 role만 사용하고 나머지 정보는 빈값으로 두기로 함!

또한 rank대신 role을 사용해보자

잼미니 응답 내용 이렇게 나옴

But 콘솔에러 발생

내용 > 이번에는 key prop을 지정하지 않아서 발생하는 경고 warring!!!!
이유 > React는 key prop을 사용하여 리스트의 항목들을 효율적으로 업데이트하고 식별한다.

메시지에서 DynamicMenu.tsx:15라고 명시되어 있으므로, 해당 파일의 15번째 줄 근처에서 리스트를 렌더링하는 부분 (예:map함수)을 찾아 각 리스트 항목에 고유한 key prop을 추가해야 함

DynamicMenu.tsx파일확인 => compaines.map부분에 key prop이 누락되어 있음
=> company.uuid를 key로 사용하도록 수정함

  • menuTree.map부분에 key prop도 누락되어 잇어서 menu.id 또는 menu.name과 같이 고유한 값을 key로 사용하도록 수정함
    하지만 여기에서는 menu.id가 없으므로 menu.name을 사용함.
profile
단단한 개발자

0개의 댓글