프론트 개발자가 임시로 API를 만들 수 있도록 도와주는 라이브러리이다. 현재 Next14 버전을 완벽하게 지원하는 것은 아니지만 사용할만한 수준이다.
백앤드 API가 완성되지 않은 상태
에서 화면 개발을 해야하거나, API 에러가 발생한 경우
를 만들어 테스트 할 때 주로 사용한다.
npx msw init public/ --save
npm install msw --save-dev
public 폴더 안에 mockServiceWorker.js 파일이 생성된다.
이 파일은 요청하는 request 를 가로채서 내부 로직에 따라 response 해주게 된다.
Next 는 서버단에서도 돌아가기 때문에 MSW 가 서버에서도 실행되어야 하지만 현재는 지원하고 있지않아 노드 서버를 이용한다.
npm i -D @mswjs/http-middleware express cors
npm i --save-dev @types/express @types/cors
// /src/mocks/https.ts
import { createMiddleware } from '@mswjs/http-middleware';
import express from 'express';
import cors from 'cors';
import { handlers } from './handlers';
const app = express();
const port = 9090;
app.use(cors({ origin: 'http://localhost:3000', optionsSuccessStatus: 200, credentials: true }));
app.use(express.json());
app.use(createMiddleware(...handlers));
app.listen(port, () => console.log(`Mock server is running on port: ${port}`));
Client 에서 실행되는 환경이다.
// /src/mocks/brower.ts
import {setupWorker} from "msw/browser";
import {handlers} from "@/mocks/handlers";
// This configures a Service Worker with the given request handlers.
const worker = setupWorker(...handlers)
export default worker
실제 response 내용을 작성하는 파일이다. http.ts
와 browser.ts
가 이 핸들러를 사용한다.
// src/mocks/handlers.ts
import {http, HttpResponse, StrictResponse} from 'msw'
import {faker} from "@faker-js/faker";
function generateDate() {
const lastWeek = new Date(Date.now());
lastWeek.setDate(lastWeek.getDate() - 7);
return faker.date.between({
from: lastWeek,
to: Date.now(),
});
}
const User = [
{id: 'elonmusk', nickname: 'Elon Musk', image: '/yRsRRjGO.jpg'},
{id: 'mju6013', nickname: 'jae_han_e', image: '/5Udwvqim.jpg'},
{id: 'zerohch0', nickname: '제로초', image: '/5Udwvqim.jpg'},
]
const Posts = [];
const delay = (ms: number) => new Promise((res) => {
setTimeout(res, ms);
})
export const handlers = [
// 로그인
http.post('/api/login', () => {
console.log('로그인');
return HttpResponse.json(User[1], {
headers: {
'Set-Cookie': 'connect.sid=msw-cookie;HttpOnly;Path=/'
}
})
}),
// 로그아웃
http.post('/api/logout', () => {
console.log('로그아웃');
return new HttpResponse(null, {
headers: {
'Set-Cookie': 'connect.sid=;HttpOnly;Path=/;Max-Age=0'
}
})
}),
// 회원가입
http.post('/api/users', async ({ request }) => {
console.log('회원가입');
// return HttpResponse.text(JSON.stringify('user_exists'), {
// status: 403,
// })
return HttpResponse.text(JSON.stringify('ok'), {
headers: {
'Set-Cookie': 'connect.sid=msw-cookie;HttpOnly;Path=/;Max-Age=0'
}
})
}),
// 추천 게시물 조회
http.get('/api/postRecommends', async ({ request }) => {
await delay(3000);
const url = new URL(request.url)
const cursor = parseInt(url.searchParams.get('cursor') as string) || 0
return HttpResponse.json(
[
{
postId: cursor + 1,
User: User[0],
content: `${cursor + 1} Z.com is so marvelous. I'm gonna buy that.`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: cursor + 2,
User: User[0],
content: `${cursor + 2} Z.com is so marvelous. I'm gonna buy that.`,
Images: [
{imageId: 1, link: faker.image.urlLoremFlickr()},
{imageId: 2, link: faker.image.urlLoremFlickr()},
],
createdAt: generateDate(),
},
{
postId: cursor + 3,
User: User[0],
content: `${cursor + 3} Z.com is so marvelous. I'm gonna buy that.`,
Images: [],
createdAt: generateDate(),
},
{
postId: cursor + 4,
User: User[0],
content: `${cursor + 4} Z.com is so marvelous. I'm gonna buy that.`,
Images: [
{imageId: 1, link: faker.image.urlLoremFlickr()},
{imageId: 2, link: faker.image.urlLoremFlickr()},
{imageId: 3, link: faker.image.urlLoremFlickr()},
{imageId: 4, link: faker.image.urlLoremFlickr()},
],
createdAt: generateDate(),
},
{
postId: cursor + 5,
User: User[0],
content: `${cursor + 5} Z.com is so marvelous. I'm gonna buy that.`,
Images: [
{imageId: 1, link: faker.image.urlLoremFlickr()},
{imageId: 2, link: faker.image.urlLoremFlickr()},
{imageId: 3, link: faker.image.urlLoremFlickr()},
],
createdAt: generateDate(),
},
]
)
}),
// 팔로우 사용자 게시물 조회
http.get('/api/followingPosts', async ({ request }) => {
await delay(3000);
return HttpResponse.json(
[
{
postId: 1,
User: User[0],
content: `${1} Stop following me. I'm too famous.`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 2,
User: User[0],
content: `${2} Stop following me. I'm too famous.`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 3,
User: User[0],
content: `${3} Stop following me. I'm too famous.`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 4,
User: User[0],
content: `${4} Stop following me. I'm too famous.`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 5,
User: User[0],
content: `${5} Stop following me. I'm too famous.`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
]
)
}),
// 게시물 조회
http.get('/api/search/:tag', ({ request, params }) => {
const { tag } = params;
return HttpResponse.json(
[
{
postId: 1,
User: User[0],
content: `${1} 검색결과 ${tag}`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 2,
User: User[0],
content: `${2} 검색결과 ${tag}`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 3,
User: User[0],
content: `${3} 검색결과 ${tag}`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 4,
User: User[0],
content: `${4} 검색결과 ${tag}`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 5,
User: User[0],
content: `${5} 검색결과 ${tag}`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
]
)
}),
// 특정 사용자의 게시물 조회
http.get('/api/users/:userId/posts', ({ request, params }) => {
const { userId } = params;
return HttpResponse.json(
[
{
postId: 1,
User: User[0],
content: `${1} ${userId}의 게시글`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 2,
User: User[0],
content: `${2} ${userId}의 게시글`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 3,
User: User[0],
content: `${3} ${userId}의 게시글`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 4,
User: User[0],
content: `${4} ${userId}의 게시글`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 5,
User: User[0],
content: `${5} ${userId}의 게시글`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
]
)
}),
// 사용자 조회
http.get('/api/users/:userId', ({ request, params }): StrictResponse<any> => {
const {userId} = params;
const found = User.find((v) => v.id === userId);
if (found) {
return HttpResponse.json(
found,
);
}
return HttpResponse.json({ message: 'no_such_user' }, {
status: 404,
})
}),
// 특정 게시물 조회
http.get('/api/posts/:postId', ({ request, params }): StrictResponse<any> => {
const {postId} = params;
if (parseInt(postId as string) > 10) {
return HttpResponse.json({ message: 'no_such_post' }, {
status: 404,
})
}
return HttpResponse.json(
{
postId,
User: User[0],
content: `${1} 게시글 아이디 ${postId}의 내용`,
Images: [
{imageId: 1, link: faker.image.urlLoremFlickr()},
{imageId: 2, link: faker.image.urlLoremFlickr()},
{imageId: 3, link: faker.image.urlLoremFlickr()},
],
createdAt: generateDate(),
},
);
}),
// 특정 게시물 답변 조회
http.get('/api/posts/:postId/comments', ({ request, params }) => {
const { postId } = params;
return HttpResponse.json(
[
{
postId: 1,
User: User[0],
content: `${1} 게시글 ${postId}의 답글`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 2,
User: User[0],
content: `${2} 게시글 ${postId}의 답글`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 3,
User: User[0],
content: `${3} 게시글 ${postId}의 답글`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 4,
User: User[0],
content: `${4} 게시글 ${postId}의 답글`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
{
postId: 5,
User: User[0],
content: `${5} 게시글 ${postId}의 답글`,
Images: [{imageId: 1, link: faker.image.urlLoremFlickr()}],
createdAt: generateDate(),
},
]
)
}),
// 팔로우 추천
http.get('/api/followRecommends', ({ request}) => {
return HttpResponse.json(User);
}),
// 트렌드 추천
http.get('/api/trends', ({ request }) => {
return HttpResponse.json(
[
{tagId: 1, title: '트랜드1', count: 1264},
{tagId: 2, title: '트랜드2', count: 1264},
{tagId: 3, title: '트랜드3', count: 1264},
{tagId: 4, title: '트랜드4', count: 1264},
{tagId: 5, title: '트랜드5', count: 1264},
{tagId: 6, title: '트랜드6', count: 1264},
{tagId: 7, title: '트랜드7', count: 1264},
{tagId: 8, title: '트랜드8', count: 1264},
{tagId: 9, title: '트랜드9', count: 1264},
]
)
}),
];
package.json 에 MSW 실행 명령어를 추가해준다.
"mock": npx tsx watch ./src/mocks/http.ts
> npm run mock
(afterLogin), (beforeLogin) 모두 적용되어야 하므로 /app/_component
에 위치시키고 /app/layout.tsx
에 추가해준다.
// /app/_component/MSWComponent.tsx
"use client";
import { useEffect } from "react";
export const MSWComponent = () => {
useEffect(() => {
if (typeof window !== 'undefined') {
if (process.env.NEXT_PUBLIC_API_MOCKING === "enabled") {
require("@/mocks/browser");
}
}
}, []);
return null;
};
// /app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import {MSWComponent} from "@/app/_component/MSWComponent";
import AuthSession from "@/app/_component/AuthSession";
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Z. 무슨 일이 일어나고 있나요? / Z',
description: 'Z.com inspired by X.com',
}
type Props = {
children: React.ReactNode
}
export default function RootLayout({children}: Props) {
return (
<html lang="en">
<body className={inter.className}>
<MSWComponent/> // MSW 적용
<AuthSession>
{children}
</AuthSession>
</body>
</html>
)
}
개발환경에서만 MSW를 사용하면 되므로 .env.local 파일에 환경변수를 추가해준다.
환경변수 앞에 NEXT_PUBLIC_
이 붙어 있으면 브라우저에서 접근 가능한 환경변수이고 없으면 서버에서만 접근이 가능하다. ( 브라우저에 노출되지 않는다.)
NEXT_PUBLIC_API_MOCKING = enabled