(급페이 프로젝트) 알림창 무한 스크롤을 위한 여정 (mock fetching을 위한 MSW 도입기)

Updated:

설계

급페이 헤더의 일부분이다. 클릭시 열리는 알림창을 구현하는데 해당 데이터는 무한 스크롤로 구현합니다.

초기 값은 Server component로써 헤더가 내내 갖고 있게 되고 알림을 누르고 interceptor observer로 감지가 되면 비로소 다음 데이터를 로딩하는 것으로 설계하였습니다.

알림 생성의 원리는 다음과 같습니다.

  1. 알바 계정으로 특정 가게 지원하기 버튼 클릭
  2. 해당 가게 주인 계정으로 승인 혹은 취소 버튼 클릭
  3. 알바 계정으로 알림창 클릭
  4. 여러 개일 경우 스크롤을 내려서 인피니티 스크롤 작동
  5. 호출되는 데이터중에 해당 지원 알림 확인

이 과정에서 accesstoken이 두개가 필요하고(가게 주인, 알바) 데이터 fetching 신경 쓰지 않고 mock data를 사용하자니 인피니티 스크롤을 사용해야 하기 때문에 적용하기가 쉽지 않았습니다. 그래서 찾아보다가 MSW라는 것을 알게 되었습니다.

MSW는 server를 mocking함으로써 요청을 가로채서 지정된 response를 리턴하게 해주는 mocking server worker입니다. MSW를 일부 도입하여 다른 개발이 끝나지 않은 환경에서 인피니티 스크롤 구현을 확인하고자 하였습니다.

구현

MSW

환경 설정

npx msw init ./public --save

public 폴더 안에 msw 관련 리소스들을 담는 단축키입니다.

// /mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);

// /mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);

// /mocks/index.ts
export async function initMsw() {
  if (typeof window === "undefined") { // 서버 일 경우
    const { server } = await import("../mocks/server");
    server.listen();
  } else { // 브라우저일 경우
    const { worker } = await import("../mocks/browser");
    await worker.start();
  }
}

// /mocks/msw-component
"use client";
import { useEffect, useState } from "react";

export const MSWComponent = ({ children }: { children: React.ReactNode }) => {
  const [mswReady, setMswReady] = useState(false);
  
  useEffect(() => {
    const init = async () => {
      const initMsw = await import("./index").then((res) => res.initMsw);
      await initMsw();
      setMswReady(true);
    };
  
    if (!mswReady) {
      init();
    }
  }, [mswReady]);
  
  return <>{children}</>;
};
export const metadata: Metadata = {
  description:
    "급하게 일손이 필요한 자리에 더 많은 시급을 제공해서 아르바이트생을 구할 수 있는 서비스",
  title: {
    template: "%s | 급PAY",
    default: "급PAY",
  },
  icons: {
    icon: "/icons/favicon.png",
  },
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ko">
      <MSWComponent>
        <body className="relative">
          <Header />
          <main className="min-h-screen">{children}</main>
          <Footer />
        </body>
      </MSWComponent>
    </html>
  );
}

MSWComponent를 Root에 넣어줘서 Root에 접근시 msw가 작동되도록 해줍니다.

핸들러 설계

/**
 * @author 이승현
 * @description
 * handlers를 이용해서 msw를 사용할 수 있습니다.
 * 1. app 에 있는 layout.tsx에서 MswComponent 주석 해제합니다.
 * 2. msw의 문법을 이용해서 handlers 배열에 원하는 api를 추가합니다.
 * 3. npx msw init ./public --save 로 변경된 사항에 대한 mock 파일을 생성합니다.
 *
 * @error 해결 방법
 * 무언가를 지워서 mock파일과 달라졌을 경우 에러가 발생할 수 있습니다.
 *
 * npx msw init ./public --save 으로 다시 빌딩하셔서 해결하시면 됩니다.
 */
export const handlers = [
  http.get(
    `${process.env.NEXT_PUBLIC_BASE_URL}/users/*/alerts`,
    ({ request, params, cookies }) => {
      // params는 첫번째 인수의 *을 받습니다. 고로 searchParams를 이용해야만
      // 원하는 query를 얻을 수 있습니다.
      const offset = new URL(request.url).searchParams.get("offset");
      if (offset === "0") {
        // makeAlertMockData는 임의로 만든 Mock 함수입니다. id를 랜덤으로 주기 위해 만들었습니다.
        // 내부적으로 Math.random()을 통해서 Node.js 환경에서 랜덤한 값을 얻은 후에 id를 만들어 보내줍니다.
        const alertMockData1 = makeAlertMockData(); // 클로저를 이용해서 함수의 리턴값을 private화 합니다.
        // MSW에서 권장하는 Response 객체입니다. json, text, xml 등 다양한 형태로
        // 응답을 보낼 수 있습니다.
        return HttpResponse.json(alertMockData1);
      } else if (offset === "6") {
        const alertMockData2 = makeAlertMockData();
        console.log("다음 offset으로 이동");
        return HttpResponse.json(alertMockData2);
      } else {
        const alertMockData3 = makeAlertMockData();
        return HttpResponse.json(alertMockData3);
      }
    },
  ),
];

주석대로 handlers 함수를 조작해서 api를 가로챌 수 있습니다. 현재 users/*/alerts 정규식에 해당하는 모든 요청은 가로채지고 아래의 함수대로 흘러갈 것입니다.

각 if, else if, else 는 의미가 존재합니다.

순서대로 의미를 설명해보면

  1. 데이터가 잘 오는지 확인 (offset==="0")
  2. 다음 데이터가 잘 오는지 확인하고 offset이 움직이는지 두번째 데이터인지 확실히 확인 (offset==='6')
  3. 이후부턴 console.log가 없는 채로 데이터가 오기 때문에 인피니티 스크롤 구현이 되었는지 확실하게 확인할 수 있습니다.

mock data 설계

이후 mock data 설계가 매우 중요한데요.

가장 큰 고민은 서로 다른 데이터를 호출하고 있는지를 어떻게 확인할지 고민을 했었습니다.

생각보단 짧은 시간 후에 답을 찾게 되었는데요.

// mock.ts
export const makeAlertMockData = (): alertMockData => {
  return {
    offset: 0,
    limit: 0,
    count: 0, // 전체 개수
    hasNext: true, // 다음 내용 존재 여부
    items: [
      {
        item: {
          id: Math.random().toString(),
          createdAt: "hi",
          result: "accepted",
          read: false,
          application: {
            item: {
              id: Math.random().toString(),
//...

위처럼 id에 Math.random()을 사용하는 것입니다.

리액트에서는 백그라운드에서 workinprogress라는 fiber tree를 따로 생성해놓고 다른 부분이 생겼을 때 현재 보여지는 current tree에 commit하면서 렌더링을 수행하는데요.

여기서 “다른 부분”을 확인하는데 중요한 역할이 key입니다. 그래서 리액트는 map 메소드 처럼 배열로 컴포넌트를 만들 때 key를 꼭 사용하라고 경고창까지 나오게 되죠 (불필요한 렌더링 방지)

{alerts.length > 0 ? (
        alerts.map((item, index) => {
          if (alerts.length === index + 1)
            return (
              <IntersectionArea key={item.item.id} onImpression={onImpression}>
                <AlertCard item={item} onDelete={onDelete} />
              </IntersectionArea>
            );

이런식으로 말입니다. IntersetionArea는 제가 만든 interceptor observer 전용 컴포넌트에요 감지되면 onImpression을 호출합니다.

여기선 key를 사용하지 않지만 위의 이유 때문에 key에 고유한 값을 넣어서 사용해야만 합니다. 주로 id를 넣죠.

mock data에 Math.random()을 넣은 이유를 이제 조금 이해가 되실 겁니다!

Math.random()은 항상 다른 값을 호출하기 때문에 같은 데이터인지 아닌지 쉽게 확인할 수 있는 것입니다. 같은 데이터를 주었다면 경고창이 나오게 되겠죠.

MSW 실행

씨익

이후 성공적으로 인피니티 스크롤을 확인했습니다.

reference

https://oliveyoung.tech/blog/2024-01-23/msw-frontend/ https://www.handongryong.com/post/msw/ https://velog.io/@sangpok/Next.js-App-Router-MSW

급페이 프로젝트 중 고민했던 방식, 부딪히게 된 문제, 그것의 해결과정을 담은 글입니다!

잘못된 내용이 있다면 말씀 부탁드립니다. 감사합니다.

Categories:

Updated:

Leave a comment