Skip to content

Commit

Permalink
[FE] 리뷰 상세페이지 query 훅 분리 및 HTTP 요청 테스트 진행 (#216)
Browse files Browse the repository at this point in the history
* refactor: DetailedPage/index.tsx 리팩토링

- early return를 사용해 코드의 가독성을 높임

* feat: useGetDetailedReview 훅 생성 및 DetailedReviewPageContents에 적용

* feat: useSearchParamAndQuery 훅 생성 및 DetailedPageContent에 적용

* refactor: 리뷰 상세페이지에서 id라고 사용했던 key값, params의 key를 reviewId로 변경

- DetailedReview의 router param을 id에서 reviewId로 변경

* ci: dependencies에서 jest 삭제 및 ts-jest 설치

* ci: jest에서 절대 경로 사용할 수 있도록  jest.config.js 추가

* chore: eslint적용 제외 파일에 jest.config.js, tsconfig.json 추가

* ci: jest의 testEnvioronment를 jsdom으로 설정

* refactor: useGetDetailedReview에서 query 결과를 모두 반환하는 방식으로 변경

* fix: jest에서 msw ver2를 목서버로 사용 시 생기는 오류 수정

1. msw/node 를 읽지 못함
- jest.config.js의 testEnvironment 빈문자열

2. ReferentError: TextEnCoder is not defined
- 해결 : jest.polyfills.js 추가 및 undici 설치

3. ReferenceError: ReadableStream is not defined
- 해결 : undici 다운 그레이드
undici": "^6.19.5", -> "^5.0.0"

* ci : jest에서 env 파일 읽을 수 있도록 dotenv 설치 및 jest에 적용

* fix: mock 핸들러인 getDetailedReview 에서 중복된 쿼리 매개 변수 사용 수정

- 오류 상황:  jest에서 msw 사용 시, get의 url에 파라미터 사용 시 중복된 쿼리 매개 변수 오류가 남
- 오류 메세지 ::Found a redundant usage of query parameters in the request handler
- 해결:  리뷰 상세보기 페이지의 reviews까지의 url 상수를 만들고, get에서는 이 상수를  활용한 정규표현식으로 리뷰 상세보기 페이지로 오는 모든 요청을 가로챌 수 있도록  함

* refactor: getWrongDetailedReview 목서버 핸들러 및 관련 상수 삭제

- getDetailedReview에서 request를 분석해 http오류 여부를 결정함

* feat: queryClientWrapper 생성

- queryClientWrapper : msw를 사용한 jest 테스트에 queryWrapper로 사용

* test:리뷰 상세 페이지 api 요청 성공에 대한 테스트 추가

* fix: groupAccessCodeAtom의 기본값 원래대로 복구

* chore:queryClientWrapper 네이밍 표기법을  파스칼 케이스로 변경

* fix: 머지 충돌 방지를 위해 yarn.lock 삭제

* fix:  머지 시 yarn.lock 충돌 해결
  • Loading branch information
BadaHertz52 authored Aug 6, 2024
1 parent 4039156 commit 36ebd7a
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 59 deletions.
4 changes: 3 additions & 1 deletion frontend/src/apis/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export const DETAILED_REVIEW_API_PARAMS = {
},
};

export const DETAILED_REVIEW_API_URL = `${process.env.API_BASE_URL}/${DETAILED_REVIEW_API_PARAMS.resource}`;

export const REVIEW_WRITING_API_PARAMS = {
queryString: {
reviewRequestCode: 'reviewRequestCode',
Expand All @@ -14,7 +16,7 @@ export const REVIEW_WRITING_API_PARAMS = {
const endPoint = {
postingReview: `${process.env.API_BASE_URL}/reviews`,
gettingDetailedReview: (reviewId: number, memberId: number) =>
`${process.env.API_BASE_URL}/${DETAILED_REVIEW_API_PARAMS.resource}/${reviewId}?${DETAILED_REVIEW_API_PARAMS.queryString.memberId}=${memberId}`,
`${DETAILED_REVIEW_API_URL}/${reviewId}?${DETAILED_REVIEW_API_PARAMS.queryString.memberId}=${memberId}`,
gettingDataToWriteReview: (reviewRequestCode: string) =>
`${process.env.API_BASE_URL}/reviews/write?${REVIEW_WRITING_API_PARAMS.queryString.reviewRequestCode}=${reviewRequestCode}`,
gettingReviewList: `${process.env.API_BASE_URL}/reviews`,
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { default as useSidebar } from './useSidebar';
export { default as useModalClose } from './useModalClose';
export { default as useGroupAccessCode } from './useGroupAccessCode';
export { default as useGetDetailedReview } from './review/useGetDetailedReview';
export { default as useSearchParamAndQuery } from './useSearchParamAndQuery';
33 changes: 33 additions & 0 deletions frontend/src/hooks/review/useGetDetailedReview/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useSuspenseQuery } from '@tanstack/react-query';

import { getDetailedReviewApi } from '@/apis/review';
import { REVIEW_QUERY_KEYS } from '@/constants';
import { DetailReviewData } from '@/types';

interface UseGetDetailedReviewProps {
reviewId: number;
memberId: number;
groupAccessCode: string;
}

interface FetchDetailedReviewParams {
reviewId: number;
memberId: number;
groupAccessCode: string;
}

const useGetDetailedReview = ({ reviewId, memberId, groupAccessCode }: UseGetDetailedReviewProps) => {
const fetchDetailedReview = async ({ reviewId, memberId, groupAccessCode }: FetchDetailedReviewParams) => {
const result = await getDetailedReviewApi({ reviewId, memberId, groupAccessCode });
return result;
};

const result = useSuspenseQuery<DetailReviewData>({
queryKey: [REVIEW_QUERY_KEYS.detailedReview, reviewId, memberId],
queryFn: () => fetchDetailedReview({ reviewId, memberId, groupAccessCode }),
});

return result;
};

export default useGetDetailedReview;
26 changes: 26 additions & 0 deletions frontend/src/hooks/review/useGetDetailedReview/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { renderHook, waitFor } from '@testing-library/react';

import { DETAILED_PAGE_MOCK_API_SETTING_VALUES } from '@/mocks/mockData/detailedReviewMockData';
import QueryClientWrapper from '@/queryTestSetup/QueryClientWrapper';

import useGetDetailedReview from '.';
// 아래의 테스트는 로그인이 유효하다는 가정하에서 진행

describe('리뷰 상세페이지 데이터 요청 테스트', () => {
const GROUND_ACCESS_CODE = '1234';

it('유효힌 id,memberId 사용해야 라뷰 상세 페이지 데이터를 불러온다.', async () => {
const { reviewId, memberId } = DETAILED_PAGE_MOCK_API_SETTING_VALUES;

const { result } = renderHook(
() => useGetDetailedReview({ reviewId, memberId, groupAccessCode: GROUND_ACCESS_CODE }),
{ wrapper: QueryClientWrapper },
);

await waitFor(() => {
expect(result.current.status).toBe('success');
});

expect(result.current.data).toBeDefined();
});
});
22 changes: 22 additions & 0 deletions frontend/src/hooks/useSearchParamAndQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useLocation, useParams } from 'react-router';

interface UseSearchParamAndQueryProps {
paramKey: string;
queryStringKey?: string;
}
/**
* url에서 원하는 param, queryString의 값을 가져온다.
* @param paramKey: 가져오고 싶은 param의 key
* @param queryStringKey: 가져오고 싶은 queryString의 key (옵셔널)
*/
const useSearchParamAndQuery = ({ paramKey, queryStringKey }: UseSearchParamAndQueryProps) => {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);

return {
param: useParams()[`${paramKey}`],
queryString: queryStringKey ? queryParams.get(queryStringKey) : null,
};
};

export default useSearchParamAndQuery;
2 changes: 1 addition & 1 deletion frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const router = createBrowserRouter([
element: <ReviewPreviewListPage />,
},
{
path: 'user/detailed-review/:id',
path: 'user/detailed-review/:reviewId',
element: <DetailedReviewPage />,
},
{
Expand Down
37 changes: 15 additions & 22 deletions frontend/src/mocks/handlers/review.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { http, HttpResponse } from 'msw';

import endPoint from '@/apis/endpoints';
import endPoint, { DETAILED_REVIEW_API_PARAMS, DETAILED_REVIEW_API_URL } from '@/apis/endpoints';

import {
DETAILED_REVIEW_MOCK_DATA,
DETAILED_PAGE_MOCK_API_SETTING_VALUES,
DETAILED_PAGE_ERROR_API_VALUES,
} from '../mockData/detailedReviewMockData';
import { DETAILED_REVIEW_MOCK_DATA, DETAILED_PAGE_MOCK_API_SETTING_VALUES } from '../mockData/detailedReviewMockData';
import { REVIEW_PREVIEW_LIST } from '../mockData/reviewPreviewList';
import { REVIEW_WRITING_DATA } from '../mockData/reviewWritingData';

Expand All @@ -18,23 +14,20 @@ export const PAGE = {
};

const getDetailedReview = () =>
http.get(
endPoint.gettingDetailedReview(
DETAILED_PAGE_MOCK_API_SETTING_VALUES.reviewId,
DETAILED_PAGE_MOCK_API_SETTING_VALUES.memberId,
),
async () => {
http.get(new RegExp(`^${DETAILED_REVIEW_API_URL}/\\d+$`), async ({ request }) => {
//요청 url에서 reviewId, memberId 추출
const url = new URL(request.url);
const urlReviewId = url.pathname.replace(`/${DETAILED_REVIEW_API_PARAMS.resource}/`, '');
const urlMemberId = url.searchParams.get(DETAILED_REVIEW_API_PARAMS.queryString.memberId);

const { reviewId, memberId } = DETAILED_PAGE_MOCK_API_SETTING_VALUES;
// 유효한 reviewId, memberId일 경우에만 데이터 반환
if (Number(urlReviewId) == reviewId && Number(urlMemberId) === memberId) {
return HttpResponse.json(DETAILED_REVIEW_MOCK_DATA);
},
);
}

const getWrongDetailReview = () =>
http.get(
endPoint.gettingDetailedReview(DETAILED_PAGE_ERROR_API_VALUES.reviewId, DETAILED_PAGE_ERROR_API_VALUES.memberId),
async () => {
return HttpResponse.json({ error: '잘못된 상세리뷰 요청' }, { status: 404 });
},
);
return HttpResponse.json({ error: '잘못된 상세리뷰 요청' }, { status: 404 });
});

const getDataToWriteReview = () =>
http.get(endPoint.gettingDataToWriteReview(10), async ({ request }) => {
Expand Down Expand Up @@ -68,6 +61,6 @@ const getReviewPreviewList = () => {
});
};

const reviewHandler = [getDetailedReview(), getWrongDetailReview(), getReviewPreviewList(), getDataToWriteReview()];
const reviewHandler = [getDetailedReview(), getReviewPreviewList(), getDataToWriteReview()];

export default reviewHandler;
5 changes: 0 additions & 5 deletions frontend/src/mocks/mockData/detailedReviewMockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ export const DETAILED_PAGE_MOCK_API_SETTING_VALUES = {
memberId: 2,
};

export const DETAILED_PAGE_ERROR_API_VALUES = {
reviewId: 0,
memberId: 0,
};

const ANSWER =
'림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. \n 바람은 여전히 그를 감싸며, 그의 마음 속 깊은 곳에 있는 꿈과 희망을 불러일으켰습니다.\n 림순은 미소 지으며 앞으로 나아갔습니다.림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. 림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. \n 바람은 여전히 그를 감싸며, 그의 마음 속 깊은 곳에 있는 꿈과 희망을 불러일으켰습니다.\n 림순은 미소 지으며 앞으로 나아갔습니다.림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. ';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,24 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { useLocation, useParams } from 'react-router';

import { DETAILED_REVIEW_API_PARAMS } from '@/apis/endpoints';
import { getDetailedReviewApi } from '@/apis/review';
import { LoginRedirectModal } from '@/components';
import { REVIEW_QUERY_KEYS } from '@/constants';
import { useGetDetailedReview, useSearchParamAndQuery } from '@/hooks';
import { ReviewDescription, ReviewSection, KeywordSection } from '@/pages/DetailedReviewPage/components';
import { DetailReviewData } from '@/types';

import * as S from './styles';

interface DetailedReviewPageContentsProps {
groupAccessCode: string;
}
const DetailedReviewPageContents = ({ groupAccessCode }: DetailedReviewPageContentsProps) => {
const { id } = useParams<{ id: string }>();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const memberId = queryParams.get(DETAILED_REVIEW_API_PARAMS.queryString.memberId);

const fetchDetailedReview = async (reviewId: number, memberId: number, groupAccessCode: string) => {
const result = await getDetailedReviewApi({ reviewId, memberId, groupAccessCode });
return result;
};
const { param: reviewId, queryString: memberId } = useSearchParamAndQuery({
paramKey: 'reviewId',
queryStringKey: DETAILED_REVIEW_API_PARAMS.queryString.memberId,
});

const { data: detailedReview } = useSuspenseQuery<DetailReviewData>({
queryKey: [REVIEW_QUERY_KEYS.detailedReview, id, memberId],
queryFn: () => fetchDetailedReview(Number(id), Number(memberId), groupAccessCode),
const { data: detailedReview } = useGetDetailedReview({
reviewId: Number(reviewId),
memberId: Number(memberId),
groupAccessCode,
});

if (!detailedReview) throw new Error(' 상세보기 리뷰 데이터를 가져올 수 없어요.');
if (!groupAccessCode) return <LoginRedirectModal />;
// TODO: 리뷰 공개/비공개 토글 버튼 기능
return (
<S.DetailedReviewPageContents>
Expand Down
14 changes: 5 additions & 9 deletions frontend/src/pages/DetailedReviewPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,12 @@ import { DetailedReviewPageContents } from './components';
const DetailedReviewPage = () => {
const { groupAccessCode } = useGroupAccessCode();

if (!groupAccessCode) return <LoginRedirectModal />;

return (
<>
{groupAccessCode ? (
<ErrorSuspenseContainer>
<DetailedReviewPageContents groupAccessCode={groupAccessCode} />
</ErrorSuspenseContainer>
) : (
<LoginRedirectModal />
)}
</>
<ErrorSuspenseContainer>
<DetailedReviewPageContents groupAccessCode={groupAccessCode} />
</ErrorSuspenseContainer>
);
};

Expand Down
11 changes: 11 additions & 0 deletions frontend/src/queryTestSetup/QueryClientWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import { EssentialPropsWithChildren } from '@/types';

const queryClient = new QueryClient();

const QueryClientWrapper = ({ children }: EssentialPropsWithChildren) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

export default QueryClientWrapper;

0 comments on commit 36ebd7a

Please sign in to comment.