[Next.js 13] MSW 도입 실패기

thru·2023년 11월 12일
3

까탈스러운 녀석

서론

예전에 처음으로 백엔드와 협업하는 팀 프로젝트를 할 때 API 작업이 지연돼서 마지막 주에 급하게 데이터 연결 작업을 진행했었다. 끝나고 나서 MSW라는게 있다는 걸 알았는데 그걸 썼다면 덜 힘들었겠구나 싶었다. 그래서 이번 팀 프로젝트 때는 꼭 도입하고 싶었는데 Next.js 13 버전과 관련된 이슈가 있어서 다른 방법을 사용해야 했다.


Mock 데이터

백엔드의 API가 완성되기 전에 API 응답 대신 사용할 수 있도록 만든 가짜 데이터를 말한다. Mock 데이터를 통해 프론트엔드 개발자가 미리 UI와 기능을 구현하거나 테스트하는 등의 작업을 백엔드 개발과 병렬적으로 진행할 수 있다.

dummy 방식

Mock 데이터라고 말하니까 생소해보일 수 있지만 파일 내부에 상수로 더미 데이터를 만들어서 사용하는 방식도 포함된다.

// 간단한 투두리스트 만들기
const TodoListData = [
  {
    id: 1,
    description: "book"
  },
  {
    id: 2,
    description: "running"
  },
  {
    id: 3,
    description: "sleep"
  }
];

const TodoList = () => {
  return (
    <ul>
      {TodoListData.map(({ id, description }) => (
        <li key={id}>{description}</li>
      ))}
    </ul>
  );
};

처음 react를 학습할 때 저런 코드 예시를 많이 보았을 것이다. 위에 선언되어 있는 TodoListData가 바로 Mock 데이터의 역할을 하고있다고 볼 수 있다.

이 방식의 장점은 작성하기 쉽고 직관적이라는 것이다. 그냥 데이터가 필요할 때 적당히 값을 할당해서 사용하면 된다.

단점은 나중에 API 응답으로 데이터를 변경할 때 로직의 수정이 필수적이란 것이다. 데이터를 받아오는 코드도 작성해야하고 더미 데이터론 대신할 수 없었던 POST나 DELETE 같은 요청관련 로직도 새로 짜야한다. 게다가 한 군데에 정리해두지 않고 위 코드처럼 그때그때 만들면 더미데이터를 재사용하기 쉽지 않고 데이터 형식이 달라졌을 때 수정하는 것도 번거롭다.

때문에 백엔드와 협업하는 환경에서 더미 데이터를 쓰는 건 비추천한다. 진짜 번거로웠다..


MSW

MSW는 Service Worker 라는 브라우저의 API를 사용해 Mocking 기능을 제공하는 라이브러리이다. 가장 주요한 특징은 단순 데이터를 내려주는 방식이 아니라 실제 API 요청을 가로채고 Mock 데이터를 API 응답의 형식으로 전달할 수 있다는 점이다.

MSW 방식

MSW는 API 요청을 가로채 작동하는 방식이기에 컴포넌트 내부에서 데이터 요청은 실제 API를 사용하는 것과 동일하게 작성하면 된다. 대신 요청에 대한 핸들러를 작성하고 서비스 워커를 등록하는 등의 기본 설정은 필요하다.

장점은 실제 API로의 전환이 쉽다는 것이다. 요청에 대한 핸들러만 백엔드 데이터와 일치하게 잘 짜놓았다면 내부 코드 수정없이 바로 적용할 수 있다. GET 이외의 REST API 종류도 모두 사용할 수 있고, 단순한 객체를 주고 받는 걸 넘어서 핸들러에서 데이터 처리 로직을 작성할 수 있다. 특히 Request와 Response 객체에 접근하거나 새로 만들 수 있기 때문에 header나 cookie 같이 json 만으론 전달할 수 없는 요소도 활용 가능하다.

단점이라면 최신 브라우저 API 기반이기에 일부 브라우저에서는 작동하지 않을 수 있다.


MSW in SSR

MSW는 서버 환경인 node.js 에서도 동작한다. Service Worker API는 브라우저의 API라서 원래라면 동작하지 못하는 게 맞다. 대신 MSW는 http 같은 표준 요청 모듈을 확장시킨 class extention을 제공해서 해결했다. Service Worker 처럼 별도의 thread에서 작동하는 건 맞다고 한다.

세팅법은 브라우저 환경과 비슷하지만 setupWorker가 아닌 setupServer라는 별도의 API를 사용한다.

여기까진 Next.js에서도 문제 없을 것 같았지만..

in Next.js App Router

서버 컴포넌트 기반인 Next.js App router와는 동작 방식이 상충되는 부분이 있다고 한다. 첫 번째로 MSW는 어플리케이션의 어느 위치에서든 모듈 patching을 전달해 줄 수 있는 프로세스가 필요하다. 보통 다른 페이지들의 최상위에서 작동하는 root layout이 이 역할을 하는데, Next.js는 root layout과 페이지의 프로세스가 완전히 분리되어 있어 한 프로세스에서의 patching이 다른 프로세스에 영향을 미치지 못한다고 한다. 두 번째로 Next.js의 페이지 기반 프로세스는 계속 무작위 포트에서 생겨나고 사라지는 방식이라서 MSW가 모든 포트를 커버하긴 어렵다고 한다.

해당 Issue 글에 유저들이 여러 우회법이 제시하고 있지만 불안정한 방법이기에 공식적으론 Next.js 팀과의 협업을 준비하고 있다고 한다.

실제로 테스트 해보았을 때도 Client Component에선 Mocking이 정상적으로 작동했지만 Server Component에선 아니었다.

우회법을 잘 골라서 사용하면 문제를 당장은 해결할 수 있었을 것 같다. 다만 우회법을 고르고 세팅해서 실험하는데 과한 노력이 들어갈 것 같다는 우려와 나중가서 또 Next.js의 기능과 충돌할 수 있다는 우려로 인해 다른 방법을 모색해보기로 했다.


Route Handler

Next.js의 기본 기능 중에 활용할만한 게 없을까 찾아보다가 Route Handler가 MSW의 요청 핸들러와 비슷해보였다.

Route Handler는 Next.js의 App router에서 간단히 작성할 수 있는 Serverless 함수이다. 원하는 경로에 route.ts를 만들고 REST API의 메서드에 맞는 함수를 export하면 작동한다. 핸들러 내부에서는 Request와 Response 객체를 접근하거나 생성해서 활용할 수 있으며 데이터 처리 로직도 작성할 수 있다.

Mock 활용

위 특징을 보고 Mocking 용으로 활용할 수 있을 것 같아서 시도해보았다.

Mock 데이터

app/api/v1/mock/_data/user/index.ts

import { UserResponse } from "@/types/response";

export const userDetail: UserResponse = {
  status: 200,
  data: {
    userId: 1,
    basicInfo: {
      nickname: "이름",
      gender: "MALE",
      birth: "1990-01-01",
      introduce: "자기소개"
    },
    favoriteWorkingDay: {
      favoriteDate: ["MON", "THU"],
      favoriteStartTime: "12:00",
      favoriteEndTime: "18:00"
    }
  },
  serverDateTime: "2023-11-02T14:25:45"
};

json으로 저장해도 되지만 백엔드와 맞춰둔 응답 타입을 적용하고 싶어서 타입스크립트 파일로 저장했다.

Mock API

app/api/v1/mock/users/[slug]/route.ts

import { type NextRequest, NextResponse } from "next/server";

import { userDetail } from "../../_data/user";

export function GET(
  request: NextRequest,
  { params }: { params: { slug: string } }
) {
  const userId = params.slug;

  return NextResponse.json(userDetail, { status: 200 });
}

GET 함수를 export 하고 있기 때문에 GET 요청에 대해서 응답한다. POST나 DELETE 등의 메서드도 사용할 수 있다. 파일 경로는 app 이후가 그대로 url 구조에 반영되고 slug를 통해 동적 parameter를 가져올 수 있다.

NextRequestNextResponse는 js에 기본으로 있는 Request와 Response class를 Next.js에서 쿠키 등의 부가기능을 활용하기 쉽게 확장한 class 이다. 각각 헤더나 바디에 접근하거나 새로운 값을 넣어줄 수 있다.

app/api/v1/mock/missions/route.ts

import { type NextRequest, NextResponse } from "next/server";

import { missionDetail } from "../_data/mission";

const PostMissionSchema = z.object({
  missionCategoryId: z.number(),
  citizenId: z.number(),
  regionId: z.number(),
  latitude: z.number(),
  longitude: z.number(),
  missionInfo: z.object({
    title: z.string(),
    content: z.string(),
    missionDate: z.string(),
    startTime: z.string(),
    endTime: z.string(),
    deadlineTime: z.string(),
    price: z.number()
  })
});

export async function POST(request: NextRequest) {
  const missionPostData = await request.json();

  const result = PostMissionSchema.safeParse(missionPostData);

  if (!result.success) {
    return new NextResponse("Error", { status: 400 });
  }

  return NextResponse.json(missionDetail, { status: 201 });
}

POST 등의 요청에선 zod를 이용해 요청 데이터의 적합성 검사도 수행할 수 있다.

URL 분기

Route Handler는 별개의 url을 갖는 API로 작동하므로 환경 변수를 이용해 url을 달리 해주어야 한다.

const makeUrl = (baseUrl: string) => (path: string) => baseUrl + path;

const apiBaseUrl =
  process.env.NEXT_PUBLIC_API_MOCKING === "enabled"
    ? `${process.env.NEXT_PUBLIC_FE_URL}/api/v1/mock`
    : `${process.env.NEXT_PUBLIC_BE_URL}/api/v1`;

export const apiUrl = makeUrl(apiBaseUrl);

/*** 실제 사용 예시 ***/
const response = await fetch(apiUrl(pathname), options);

위 코드 처럼 NEXT_PUBLIC_API_MOCKING라는 환경 변수 값에 따라 Mock API와 실제 BE API url을 바꿔 사용할 수 있도록 구현했다. 이러한 방식으로 동작하기 위해 Route Handler의 디렉토리 구조는 실제 BE API url 구조와 일치하도록 설계했다.

이를 통해 컴포넌트 레벨에서는 로직의 변경 없이 Mock API와 실제 API를 바꿔 사용할 수 있다.

유의 사항

공식문서에는 언급되어있지 않지만 Route Handler는 Client Component에서만 정상적으로 사용할 수 있는 것으로 보인다. Server Component의 경우 build 중에 Route Handler와 함께 생성되는데 이 때는 서버가 작동하고 있는 상황은 아니라서 렌더링 중에 Route Handler를 사용할 수 없는 것으로 보인다. 그 결과로 배포 환경에서는 Mock data를 제대로 fetch해오지 못하는 모습을 볼 수 있었다.

Dev 모드에서는 Server Component와 Route Handler 모두 요청이 들어올 때 생성되어 작동하는 것으로 추측되고, 이로 인해 Server Component에서의 Route Handler 사용에 문제가 없었던 것으로 보인다.

이러한 문제가 존재하지만 Mock data 자체가 개발 중에 필요한 기능이라는 것을 생각했을 때 Dev 모드에서만 작동하는 것도 의미가 있다고 생각해서 계속 사용하기로 선택했다.
Route Handler는 이후 Next.js 버전에서 발전될 가능성이 높아 이후에는 서버 컴포넌트에서도 사용할 수 있도록 보완될 가능성도 있을 것 같다.


여담

Mocking 용으로 적합해 보이는데 왜 관련 사용법은 안퍼져있을까 생각해보면 사실 Route Handler는 자체적으로 별도의 BE 서버이기 때문이 아닐까 싶다. DB를 연결하면 실제 데이터도 처리가 가능해보였다. 결국 Route Handler를 Mocking 용으로 쓴다는 건 저쪽 서버가 준비 중이니 이쪽 서버를 대신 쓰자에 가까운 느낌일 것 같다.

그래도 개발할 때 설정하기 편하면 괜찮은 거 아닐까?


참조

profile
프론트 공부 중

1개의 댓글

comment-user-thumbnail
2024년 1월 19일

글 잘 봤습니다. 저도 계속 시도해보다가 잘 안되더군요... ㅠ

답글 달기