Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#18] useQuery, useInfiniteQuery -> useSuspenseQuery, useSuspenseInfiniteQuery 교체 #19

Merged
merged 10 commits into from
Jan 15, 2025

Conversation

lee0jae330
Copy link
Member

🔗 #18

🙋‍ Summary (요약)

  • 기존 useQuery, useInfiniteQuerySuspense를 활용할 수 있는 useSuspenseQuery, useSuspenseInfiniteQuery로 교체하였습니다.

😎 Description (변경사항)

useQuery, useInfiniteQuery -> useSuspenseQuery, useSuspenseInfiniteQuery 교체

  • React의 철학인 선언적인 프로그래밍을 적극적으로 반영하고자 기존 명령형 프로그래밍 성격이 강했던 기존 방식을 Suspense를 활용한 방식으로 바꾸고자 진행한 작업입니다.
  • Suspense 관련 학습 정리

명령적, 선언적 프로그래밍에 대한 간단한 설명

  • 명령적 프로그래밍
    • 어떻게 해야 하는가? 에 초점을 맞춘 프로그래밍 스타일
    • 작업을 수행하기 위한 절차나 명령의 흐름을 명확히 정의하는 방식

예시

if(isError)
   return <Error컴포넌트/> 
if(loading) 
  return <로딩컴포넌트/>
return  <컴포넌트 />
  • 설명 : 에러 발생 시 Error 컴포넌트를 렌더링하고 로딩 시에는 로딩 컴포넌트를 렌더링하고 그렇지 않으면 보여주려고 한 컴포넌트를 렌더링하라고 절차를 정의함

  • 선언적 프로그래밍

    • **무엇을 해야 하는가?**에 초점을 맞춘 프로그래밍 스타일
    • 결과를 정의하지만 그것을 얻는 방법(절차)은 시스템에 위임하는 방식

예시

<ErrorBoundary fallback={에러컴포넌트}>
  <Suspense fallback={로딩컴포넌트}>
      <비동기 데이터를 불러오는 컴포넌트/>
  <Suspense/>
  • 설명 : 개발자는 UI 로직에만 집중하면 됨. 에러 처리와 로딩 처리는 ErrorBounday, Suspense 컴포넌트가 처리함 (시스템에 위임)

useGetWorkspace 훅에서 useSuspenseQuery를 사용하도록 교체

  • 기존에 사용하던 useQueryuseSuspenseQuery로 교체함
기존 코드
export const useGetWorkspace = (workspaceId: string) => {
  const workspaceApi = WorkspaceApi();
  const userId = getUserId() || createUserId();
  const { initCssPropertyObj } = useCssPropsStore();
  const { initClassBlockList } = useClassBlockStore();
  const { setCanvasInfo, setName } = useWorkspaceStore();
  const { resetChangedStatusState } = useWorkspaceChangeStatusStore();
  const { setIsResetCssChecked } = useResetCssStore();
  const { setInitialImageMap, setInitialImageList } = useImageModalStore();
  const { data, isPending, isError } = useQuery({
    queryKey: workspaceKeys.detail(workspaceId),
    queryFn: () => {
      return workspaceApi.getWorkspace(userId, workspaceId);
    },
  });

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

  useEffect(() => {
    if (isError) {
      toast.error('워크스페이스 정보 불러오기 실패');
      return;
    }
    if (!data) {
      return;
    }

    if (!data.workspaceDto) {
      return;
    }
    setName(data.workspaceDto.name);
    Object.keys(data.workspaceDto.totalCssPropertyObj).forEach((className) => {
      createCssClassBlock(className);
    });

    initCssPropertyObj(data.workspaceDto.totalCssPropertyObj);
    initClassBlockList(
      Object.keys(data.workspaceDto.totalCssPropertyObj).map((className) =>
        removeCssClassNamePrefix(className)
      )
    );
    setCanvasInfo(data.workspaceDto.canvas);
    cssStyleToolboxConfig.contents = data.workspaceDto.classBlockList
      ? JSON.parse(data.workspaceDto.classBlockList)
      : [];
    setIsResetCssChecked(data.workspaceDto.isCssReset);
    setInitialImageMap(data.workspaceDto.imageMap);
    setInitialImageList(data.workspaceDto.imageList);
  }, [isError, data]);
  return { data, isPending, isError };
};
변경된 코드
export const useGetWorkspace = (workspaceId: string) => {
  const workspaceApi = WorkspaceApi();
  const userId = getUserId() || createUserId();
  const { initCssPropertyObj } = useCssPropsStore();
  const { initClassBlockList } = useClassBlockStore();
  const { setCanvasInfo, setName } = useWorkspaceStore();
  const { resetChangedStatusState } = useWorkspaceChangeStatusStore();
  const { setIsResetCssChecked } = useResetCssStore();
  const { setInitialImageMap, setInitialImageList } = useImageModalStore();
  const { data, isPending, isError } = useSuspenseQuery({
    queryKey: workspaceKeys.detail(workspaceId),
    queryFn: () => {
      resetChangedStatusState();
      return workspaceApi.getWorkspace(userId, workspaceId);
    },
  });

  useEffect(() => {
    if (!isError || !data || !data.workspaceDto) {
      return;
    }

    setName(data.workspaceDto.name);
    Object.keys(data.workspaceDto.totalCssPropertyObj).forEach((className) => {
      createCssClassBlock(className);
    });

    initCssPropertyObj(data.workspaceDto.totalCssPropertyObj);
    initClassBlockList(
      Object.keys(data.workspaceDto.totalCssPropertyObj).map((className) =>
        removeCssClassNamePrefix(className)
      )
    );
    setCanvasInfo(data.workspaceDto.canvas);
    cssStyleToolboxConfig.contents = data.workspaceDto.classBlockList
      ? JSON.parse(data.workspaceDto.classBlockList)
      : [];
    setIsResetCssChecked(data.workspaceDto.isCssReset);
    setInitialImageMap(data.workspaceDto.imageMap);
    setInitialImageList(data.workspaceDto.imageList);
  }, [isError, data]);
  return { data, isPending, isError };
};
  • 변경 전 : useGetWorkspace훅에 에러 처리를 해야 했음

  • 변경 후 : 데이터 페칭 성공 시 전역 상태를 초기화하는 작업만 진행

  • WorkspacePage 컴포넌트 변경점

기존

  if (isError) {
    return <NotFound />;
  }

  return (
    <>
      <div className="flex h-screen flex-col">
        {isPending && <Loading />}
        {isCoachMarkOpen && <CoachMark />}
        <WorkspacePageHeader />
        <WorkspaceContent />
      </div>
      <ImageTagModal />
    </>
  );
  • 에러, 로딩 처리를 해줘야 했음

변경 후

  • 에러, 로딩 처리 코드 삭제
  • App.tsx에서 설정하는 createBrowserRouter에서 ErrorBoundarySuspense를 통해 에리 및 로딩 처리 진행

useGetWorkspaceList 훅에서 useSuspenseInfiteQuery를 사용하도록 교체

  • useInfiteQuery -> useSuspenseQuery로 교체

  • useGetWorkspaceList 변경점

    • useInfiteQuery -> useSuspenseQuery만 변경
  • 기존 WorkspaceContainer 변경점

    • 기존에 사용하던 WorkspaceContainer 라는 컴포넌트명은 workspace만을 감싸고 있는 느낌이 강함. 하지만 WorkspaceHeader 컴포넌트도 감싸고 있었음
    • 의미를 명확하게 하기 위해 WorkspaceSection으로 컴포넌트명 변경
    • Workspace Item이 렌더링되는 부분을 WorkspaceContainer 컴포넌트로 분리함
    • 또한 무한스크롤, 가상스크롤 관련 로직도 제거함
    • WorkspaceContainer 컴포넌트를 Suspense로 감싸 로딩 처리 진행
기존 코드
export const WorkspaceContainer = () => {
  const { hasNextPage, fetchNextPage, isPending, isFetchingNextPage, isError, workspaceList } =
    useGetWorkspaceList();

  const { renderedData, offsetY, totalHeight } = useVirtualScroll<TWorkspace>({
    data: workspaceList,
    topSectionHeight: 594,
    renderedItemHeight: 262,
    gapY: 32,
  });

  const fetchCallback: IntersectionObserverCallback = (entries, observer) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting && hasNextPage) {
        fetchNextPage();
        observer.unobserve(entry.target);
      }
    });
  };

  const nextFetchTargetRef = useInfiniteScroll({ intersectionCallback: fetchCallback });

  return (
    <section className="w-full max-w-[1152px] px-3 pb-48">
      <WorkspaceHeader />
      {isPending && (
        <WorkspaceGrid>
          <SkeletonWorkspaceList skeletonNum={8} />
        </WorkspaceGrid>
      )}
      {isError ? (
        <WorkspaceLoadError />
      ) : (
        workspaceList &&
        (workspaceList.length === 0 ? (
          <EmptyWorkspace />
        ) : (
          <div
            style={{
              height: `${totalHeight}px`,
            }}
          >
            <WorkspaceGrid offsetY={offsetY}>
              <WorkspaceList workspaceList={renderedData} />
              {isFetchingNextPage && <SkeletonWorkspaceList skeletonNum={8} />}
            </WorkspaceGrid>
          </div>
        ))
      )}
      {!isPending && !isFetchingNextPage && hasNextPage && (
        <div ref={nextFetchTargetRef} className="h-3 w-full"></div>
      )}
    </section>
  );
};
변경된 코드
export const WorkspaceSection = () => {
  return (
    <section className="w-full max-w-[1152px] px-3 pb-48">
      <WorkspaceHeader />
      <Suspense fallback={<SkeletonWorkspaceList skeletonNum={8} />}>
        <WorkspaceContainer />
      </Suspense>
    </section>
  );
};
  • 새로운 WorkspaceContainer 컴포넌트
  • useGetWorkspaceList훅을 통한 워크스페이스 데이터 페칭 진행
  • useInfiteScroll, useVirtualScroll 훅을 통해 기존 무한스크롤, 가상스크롤 적용
코드
export const WorkspaceContainer = () => {
  const { hasNextPage, fetchNextPage, isPending, isFetchingNextPage, workspaceList } =
    useGetWorkspaceList();

  const { renderedData, offsetY, totalHeight } = useVirtualScroll<TWorkspace>({
    data: workspaceList,
    topSectionHeight: 594,
    renderedItemHeight: 262,
    gapY: 32,
  });

  const fetchCallback: IntersectionObserverCallback = (entries, observer) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting && hasNextPage) {
        fetchNextPage();
        observer.unobserve(entry.target);
      }
    });
  };

  const nextFetchTargetRef = useInfiniteScroll({ intersectionCallback: fetchCallback });

  return (
    <>
      {workspaceList &&
        (workspaceList.length === 0 ? (
          <EmptyWorkspace />
        ) : (
          <div
            style={{
              height: `${totalHeight}px`,
            }}
          >
            <WorkspaceGrid offsetY={offsetY}>
              <WorkspaceList workspaceList={renderedData} />
              {isFetchingNextPage && <SkeletonWorkspaceList skeletonNum={8} />}
            </WorkspaceGrid>
          </div>
        ))}
      {!isPending && !isFetchingNextPage && hasNextPage && (
        <div ref={nextFetchTargetRef} className="h-3 w-full"></div>
      )}
    </>
  );
};

🔥 Trouble Shooting (해결된 문제 및 해결 과정)

  • 에러 처리를 위해 ErrorBoundary를 직접 구현하였으나, 이미 react-router에서 제공하는 errorElement를 통해 에러 처리를 하였기에 기존 코드를 전부 수정하는 작업은 비용이 너무 크다고 생각이 들었습니다. Workspace 조회 시 404 에러만 따로 처리해주는 WorkspaceErrorPage를 만들었습니다.
  • 에러 처리 미들웨어에서 에러 발생 시 500 에러만 반환하는 문제가 있었습니다. 그래서 사용자 정의 에러들도 반환하도록 간단하게 수정했습니다.
  • 400 에러와 404에러의 코드와 메세지가 뒤바뀌어 있어 수정하였습니다.

🤔 Open Problem (미해결된 문제 혹은 고민사항)

  • 문서화를 별로하지 못했습니다. 수목에 진행해서 공유하겠습니다.

@lee0jae330 lee0jae330 added the refactor 리팩토링 label Jan 14, 2025
@lee0jae330 lee0jae330 self-assigned this Jan 14, 2025
@lee0jae330 lee0jae330 requested a review from a team as a code owner January 14, 2025 16:07
@lee0jae330 lee0jae330 requested review from Ujaa and inhachoi and removed request for a team January 14, 2025 16:07
@Ujaa
Copy link
Contributor

Ujaa commented Jan 15, 2025

suspense 문서화 너무 잘해주셨는데요? 읽으면서 왜 suspense를 써야하는지 알 수 있었습니다. 고생하셨습니다 ❤️

Copy link
Contributor

@inhachoi inhachoi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React의 철학인 선언적인 프로그래밍을 적극적으로 반영하고자 기존 명령형 프로그래밍 성격이 강했던 기존 방식을 Suspense를 활용한 방식으로 바꾸고자 진행한 작업입니다.

위 멘트가 상당히 개발자스러워서 멋있었습니다 ㅎㅎ

영재님의 PR은 매번 큰 울림을 주네요. 숭배하겠습니다 ❤️

<WorkspaceContainer />
<WorkspaceSection />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컴포넌트 의미가 좀 더 명확해져서 좋네요!

export const WorkspaceErrorPage = () => {
const error: any = useRouteError();
const statusCode = error?.response?.statusCode || error?.status;
console.log('error', error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.log를 제거를 깜빡하신것 같아유 😊

Comment on lines -39 to -49
if (isError) {
toast.error('워크스페이스 정보 불러오기 실패');
return;
}
if (!data) {
if (!isError || !data || !data.workspaceDto) {
return;
}

if (!data.workspaceDto) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if문 최소화 좋네요 👍

import { errorStatus } from '../utils/constants';

// eslint-disable-next-line no-unused-vars
export const errorMiddleware = (err: Error, req: Request, res: Response, _next: NextFunction) => {
console.log(err.constructor.name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.log를 제거를 깜빡하신것 같아유 😊 222

@lee0jae330 lee0jae330 merged commit 0163d1f into dev Jan 15, 2025
5 checks passed
@lee0jae330 lee0jae330 deleted the refactor/18 branch January 15, 2025 05:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
refactor 리팩토링
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[refactor] useQuery, useInfinityQuery를 Suspense 기반 useSuspenseQuery, useSuspenseInfinityQuery로 변경
3 participants