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

리펙토링 기간 중 진행한 변경 사항 #7

Open
wants to merge 43 commits into
base: dev
Choose a base branch
from
Open

Conversation

sunub
Copy link
Collaborator

@sunub sunub commented Jan 23, 2025

프로잭트 내의 문제

  • 각 페이지마다 사용자의 인증 정보를 요구하여 반복해서 서버에 사용자 인증 정보를 요청하고 있음
  • 이로 인해서 각 페이지마다 모두 동일한 네트워크 요청을 반복하는 문제가 발생
  • 이러한 문제가 페이지를 이동할 때마다 중첩되어서 너무 많고 불필요한 네트워크 요청이 쌓이고 있음을 확인

테스트 코드 작성

test("리퀘스트 요청 횟수 테스트", async ({ page }) => {
  const apiUserTokenRequests: any = [];
  const apiUserInfoRequests: any = [];

  const targetUserTokenUrl = "http://localhost:3000/api/users/token";
  const targetUserInfoUrl = "http://localhost:3000/api/users/userInfo";

  page.on("request", (request) => {
    if (request.url().includes(targetUserTokenUrl)) {
      return apiUserTokenRequests.push(request);
    }
    if (request.url().includes(targetUserInfoUrl)) {
      return apiUserInfoRequests.push(request);
    }
  });

  await page.goto("http://localhost:3000");

  await page.getByRole("link", { name: /my/ }).click();
  await expect(page).toHaveURL(/require-login/);

  await page.getByRole("link", { name: /create vote/ }).click();
  await expect(page).toHaveURL(/require-login/);

  await page.getByRole("link", { name: /betting/ }).click();
  await expect(page).toHaveURL(/require-bettingRoomId/);

  await expect(apiUserTokenRequests.length).toBe(1);
  await expect(apiUserInfoRequests.length).toBe(0);
});
  • 문제 해결을 위해 코드를 작성하기 이전에 문제를 해결 했을 경우에 대한 성공 시나리오를 테스트 코드로 작성
  • 사용자 시나리오에 따른 결과 테스트틑 e2e 테스트에 해당하고 테스트를 위해서는 playwright로 테스트를 진행하는 것이 가장 편리할 것이라고 판단
  • 프로젝트의 문제를 해결 했을 경우에는 인증이 되지 않은 상태로 페이지를 이동하더라도 위와 같은 결과를 만들어낼 수 있을 것이라고 판단하여 테스트 코드를 작성
test("쿠키 유지 상태에서 탭을 닫았다가 다시 열었을 때 인증 상태 유지 확인", async ({
  browser,
}) => {
  const context = await browser.newContext({ acceptDownloads: true });
  const page = await context.newPage();

  await page.goto("http://localhost:3000");

  await page.getByPlaceholder("이메일을 입력해주세요.").fill("[email protected]");
  await page.getByPlaceholder("비밀번호를 입력해주세요.").fill("abc1234");

  await page
    .getByRole("button", { name: // })
    .last()
    .click();
  await expect(page).toHaveURL(/my-page/);

  await page.close();

  const newPage = await context.newPage();
  await newPage.goto("http://localhost:3000");

  await expect(newPage).not.toHaveURL(/login/);
  await expect(newPage).toHaveURL(/my-page/);
});
  • 이외에도 사용자가 프로젝트 내에서 페이지를 이동할 경우 계획했던 시나리오데로 이동이 가능한지를 테스트 코드를 이용하여 테스트를 진행
export type AuthState = {
  isAuthenticated: boolean;
  nickname?: string;
  isLoading: boolean;
  roomId?: string;
};
  • 사용자 정보 중 노출이 되어도 상관이 없고 강제로 변경이 되더라도 지장이 없을만한 정보를 추출 후 Recoil을 이용하여 사용자 정보 상태 생성
  • Recoil을 이용하여 만들어낸 전역 상태를 활용하기 위해서는 기존의 라우트 계층에서 이루어지던 검증을 클라이언트 계층에 분산 시켜줄 필요가 있다고 판단
  • Recoil은 atom을 활용하여 코드 분할을 손상시키지 않고 앱 전체의 모든 상태 변경을 관찰하여 관리가 가능함으로 적합하다고 판단
import { useEffect } from "react";
import { Fragment } from "react/jsx-runtime";
import { useSetRecoilState } from "recoil";
import { Auth } from "@/app/provider/RouterProvider/lib/auth";
import { useQuery } from "@tanstack/react-query";
import { authQueries } from "@/shared/lib/auth/authQuery";

function AuthProvider({ children }: { children: React.ReactNode }) {
  const setAuthState = useSetRecoilState(Auth);
  const { data, isLoading } = useQuery({
    queryKey: authQueries.queryKey,
    queryFn: authQueries.queryFn,
  });

  useEffect(() => {
    if (!isLoading) {
      if (data?.isAuthenticated) {
        setAuthState((prev) => ({
          ...prev,
          isLoading: false,
          isAuthenticated: data.isAuthenticated,
          nickname: data.userInfo.nickname,
        }));
      } else {
        setAuthState((prev) => ({
          ...prev,
          isLoading: false,
          isAuthenticated: false,
        }));
      }
    }
  }, [data, isLoading, setAuthState]);

  return <Fragment>{children}</Fragment>;
}

export { AuthProvider };
  • 이외에도 사용자 정보가 갱신이 필요할 경우, 예를 들어 새로고침을 수행한 경우에도 Recoil을 이용하여 상태가 추적이 가능하게끔 별도의 Provider 생성
export const Route = createFileRoute("/create-vote")({
  component: () => (
    <ProtectedRoute>
    <CreateVotePage />;
  </ProtectedRoute>
  ),
  beforeLoad: async () => {
      redirectWaitingRoom(roomInfo, roomId);
      redirectVoting(roomInfo, roomId);
      redirectGuest(parsedInfo.role);
      redirectFinished(roomInfo, roomId);
  },
  errorComponent: ({ error }) => (
    <ErrorComponent error={error} feature="투표 생성 페이지" to="/login">
      <CreateVoteError />
    </ErrorComponent>
  ),
});
  • ProtectedRoute에서는 사용자 인증 정보에 관련한 검증을 상태 변수를 이용하여 검증하고 이외의 서버의 요청이 필요할 경우에만 라우트 계층에서 네트워크 요청을 이용하여 검증을 진행

프로젝트 내의 문제

  • 프로젝트 빌드 시 최적화가 적절하게 이루어지지 않아 페이지 로드 시간을 저하 시키는 문제가 발생
  • Lighthouse를 이용하여 성능을 분석한 결과 개선이 필요한 점수인 65점이 평가가 되었음을 확인
  • 페이지에서 로드하는 데이터가 많지 않았음에도 불구하고 좋지 못한 점수가 나온다는 것은 최적화를 잘 진행하지 못했다는 것을 의미
  • 성능 저하의 문제는 결국 사용자 경험을 저하 시키는 중요한 문제이기 때문에 개선이 필요하다 판단

Rollup visualizer

import { visualizer } from "rollup-plugin-visualizer";

const viteConfig = {
plugins: [
  visualizer(),
]};
  • Rollup visualizer를 이용하여 현재 프로젝트의 번들 트리 구조, 번들의 크기와 모듈 간의 관계를 시각 정보를 활용하여 분석
  • 위의 정보를 이용하여 분석을 한 결과 현재 3D 모델을 위해서 사용하는 Three.js 패키지가 적절히 분리되지 않았고 react 패키지 또한 많은 패키지가 포함되어 있음을 확인 가능
(!) Some chunks are larger than 1000 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
  • build 시 현재 패키지에 1000kB를 넘어가는 청크가 존재하기 때문에 최적화가 필요함을 경고하고 있어 vite.config 설정을 이용하여 청크 스플릿이 필요하다 판단
manualChunks(id) {
  if (id.includes("node_modules")) {
    if (id.includes("@tanstack")) return "vendor-tanstack";
    if (id.includes("three")) return "vendor-three";
    if (id.includes("react")) return "vendor-react";
    if (id.includes("@socket")) return "vendor-socket";
    return "vendor";
  }

  if (id.includes("/features/")) {
    const feature = id.split("/features/")[1].split("/")[0];
    return `feature-${feature}`;
  }
}
  • 위와 같이 청크 스플릿을 수정하였지만 오히려 성능이 저하되고 있음을 발견
  • 문제 분석 결과 node_modules 이외의 애플리케이션 코드를 스플릿 할 경우 크기가 너무 작은 코드를 스플릿 하는 것은 오히려 구조를 복잡하게 만들어서 성능을 저하 시킬 수 있다는 것을 발견
manualChunks(id) {
  if (id.includes("node_modules")) {
    if (id.includes("@tanstack")) return "vendor-tanstack";
    if (id.includes("@react-three/cannon"))
      return "vendor-react-three-cannon";
    if (id.includes("@react-three/drei"))
      return "vendor-react-three-drei";
    if (id.includes("@react-three/fiber"))
      return "vendor-react-three-fiber";
    if (id.includes("three")) return "vendor-three";
    if (id.includes("@socket")) return "vendor-socket";
    if (id.includes("react")) return "vendor-react";

    return "vendor";
  }
}
  • 애플리케이션 코드를 제외하고 패키지에서 많은 비중을 차지하고 있는 패키지를 대상으로 청크 스플릿을 세분화를 진행
const Pond = lazy(() => import("./Pond"));
  • 이외에도 3D 모델을 로드하여 크기가 큰 컴포넌트의 경우 Lazy Loading을 이용하여 성능을 개선
  • 이미지 파일의 경우 최적화를 위해 png를 avif로 변경
  • svg 의 경우도 svgo를 활용하여 용량 최적화를 진행
import { Suspense, lazy } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useSuspenseQuery } from "@tanstack/react-query";
import { userInfoQueries } from "@/shared/lib/auth/authQuery";
import { AnimatedDuckCount } from "./AnimatedDuckCount";
import { PurchaseButton } from "./PurchaseButton";

const Pond = lazy(() => import("./Pond"));

function MyPage() {
  const { data: authData } = useSuspenseQuery({
    queryKey: userInfoQueries.queryKey,
    queryFn: userInfoQueries.queryFn,
  });
  const navigate = useNavigate();

  return (
    <div className="w-ful bg-layout-main relative flex flex-col items-center justify-between gap-2">
      <div className="flex h-full w-full flex-col items-center justify-evenly">
        <div className="z-20 flex flex-col items-center justify-center pb-6">
          <h1 className="text-2xl font-extrabold">마이 페이지</h1>
          <p className="text-lg">오리를 구매해서 페이지를 꾸며보세요</p>
        </div>
        <AnimatedDuckCount />
        <Suspense fallback={null}>
          <div className="absolute left-0 top-0 z-10 h-full max-h-[600px] w-full max-w-[460px] px-5">
            <Pond realDuck={authData.realDuck} />
          </div>
        </Suspense>
        <div className="z-20 flex gap-8">
          <PurchaseButton authData={authData} />
          <button
            className="bg-default text-layout-main rounded-xl px-6 py-3 text-xl"
            onClick={() => navigate({ to: "/create-vote", replace: true })}
          >
            방 만들러 가기
          </button>
        </div>
      </div>
    </div>
  );
}

export { MyPage };
  • 컴포넌트의 상태가 제대로 분리되고 있지 않아 불필요한 렌더링과 네트워크 요청을 반복하는 컴포넌트를 리펙토링 및 제거
  • tanstack query를 이용하여 캐싱이 된 데이터의 경우 상위 컴포넌트에서 props로 전달하지 않고 캐싱된 데이터를 사용하여 상태를 관리하게끔 구조를 변경

sunub added 30 commits January 9, 2025 21:02
- 기존의 여러 네트워크 요청을 요구하던 컴포[>
- tanstack query의 캐싱을 활용하여 재사용성 >
- lazy loading을 이용하여 3D 오브젝트를 포함하는 컴포넌트를 지연 로딩하여 성능 개선
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants