Skip to content

Commit

Permalink
feat: [LINKER-73] msw Mock Server 구축 + ky 인터페이스 개선 (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
useonglee authored Jan 17, 2024
1 parent 97506c9 commit cd7b6f6
Show file tree
Hide file tree
Showing 27 changed files with 707 additions and 38 deletions.
328 changes: 325 additions & 3 deletions .pnp.cjs

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
],
"scripts": {
"dev": "yarn workspace web dev",
"dev:mock": "yarn workspace web dev:mock",
"build": "yarn workspace web build && yarn workspace web export",
"lint": "yarn workspace web lint",
"test": "yarn workspace web test",
Expand Down
17 changes: 3 additions & 14 deletions packages/ky/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
import ky from 'ky';
import kyInstance, { createKyApis, prefix } from './kyInstance';

const kyInstance = ky.create({
retry: 0,
credentials: 'include',
hooks: {
beforeRequest: [
(request) => {
// TODO(@useonglee): 토큰 로직 추가하기
},
],
},
});

export default kyInstance;
export const ky = createKyApis(kyInstance);
export const API_URL = prefix;
49 changes: 49 additions & 0 deletions packages/ky/kyInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-disable require-await */
import ky, { KyInstance, Options } from 'ky';

const kyInstance = ky.create({
retry: 0,
credentials: 'include',
timeout: 15_000,
hooks: {
beforeRequest: [
(request) => {
// TODO(@useonglee): 토큰 로직 추가하기
},
],
},
});

export const prefix =
process.env.NEXT_PUBLIC_MSW_MOCK === 'enabled'
? 'http://localhost:8000'
: `${process.env.NEXT_PUBLIC_API_URL}/api`;

export const createKyApis = (instance: KyInstance) => ({
get: async <T = unknown>(url: string, options?: Options) => {
return instance
.get(`${prefix}${url}`, options)
.json<{ data: T }>()
.then((data) => data.data);
},
post: async <T = unknown>(url: string, options?: Options) => {
return instance
.post(`${prefix}${url}`, options)
.json<{ data: T }>()
.then((data) => data.data);
},
put: async <T = unknown>(url: string, options?: Options) => {
return instance
.put(`${prefix}${url}`, options)
.json<{ data: T }>()
.then((data) => data.data);
},
delete: async <T = unknown>(url: string, options?: Options) => {
return instance
.delete(`${prefix}${url}`, options)
.json<{ data: T }>()
.then((data) => data.data);
},
});

export default kyInstance;
35 changes: 35 additions & 0 deletions packages/ky/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"noImplicitAny": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"./src/**/*.tsx",
"./src/**/*.ts",
"./jest.config.ts",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { renderHook } from '@testing-library/react-hooks';
import useBodyScrollLock from './useBodyScrollLock';

describe('useBodyScrollLock', () => {
it('마운트되면 스크롤 막히고 언마운트되면 풀린다.', () => {
test('마운트되면 스크롤 막히고 언마운트되면 풀린다.', () => {
const { unmount } = renderHook(() => useBodyScrollLock());

expect(document.body.style.overflow).toBe('hidden');
Expand All @@ -13,7 +13,7 @@ describe('useBodyScrollLock', () => {
expect(document.body.style.overflow).not.toBe('hidden');
});

it('여러번 마운트 될 경우 unmount될 때 까지 hidden을 유지한다.', () => {
test('여러번 마운트 될 경우 unmount될 때 까지 hidden을 유지한다.', () => {
const { unmount } = renderHook(() => useBodyScrollLock());

const { unmount: unmount2 } = renderHook(() => useBodyScrollLock());
Expand Down
1 change: 1 addition & 0 deletions services/web/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=https://api.im-linker.com
1 change: 1 addition & 0 deletions services/web/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=https://api.im-linker.com
22 changes: 22 additions & 0 deletions services/web/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"jsc": {
"parser": {
"syntax": "typescript",
"jsx": false,
"dynamicImport": false,
"privateMethod": false,
"functionBind": false,
"exportDefaultFrom": false,
"exportNamespaceFrom": false,
"decorators": false,
"decoratorsBeforeExport": false,
"topLevelAwait": false,
"importMeta": false
},
"transform": null,
"target": "es5",
"loose": false,
"externalHelpers": false,
"keepClassNames": false
}
}
15 changes: 14 additions & 1 deletion services/web/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,24 @@ const customJestConfig: Config.InitialOptions = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@app/(.*)$': '<rootDir>/src/app/$1',
'^@__server__/(.*)$': '<rootDir>/src/__server__/$1',
},
testPathIgnorePatterns: ['<rootDir>/.next/'],
testPathIgnorePatterns: ['<rootDir>/.next/', 'node_modules/'],
testEnvironment: 'jest-environment-jsdom',
transform: {
'\\.css\\.ts$': '@vanilla-extract/jest-transform',
'^.+\\.(t|j)sx?$': [
'@swc/jest',
{
jsc: {
transform: {
react: {
runtime: 'automatic',
},
},
},
},
],
},
};

Expand Down
14 changes: 13 additions & 1 deletion services/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"private": true,
"scripts": {
"dev": "next dev",
"mock": "ts-node ./src/__server__/http.ts",
"dev:mock": "concurrently --kill-others \"yarn dev\" \"yarn mock\"",
"build": "next build",
"start": "next start",
"lint": "next lint",
Expand All @@ -14,6 +16,7 @@
"analyze": "ANALYZE=true next build"
},
"dependencies": {
"@linker/ky": "workspace:^",
"@linker/lds": "workspace:^",
"@linker/react": "workspace:^",
"@linker/styles": "workspace:^",
Expand All @@ -34,26 +37,35 @@
"@jest/types": "^29.6.3",
"@linker/eslint-config": "workspace:^",
"@linker/eslint-plugin": "workspace:^",
"@mswjs/http-middleware": "^0.9.2",
"@next/bundle-analyzer": "^14.0.3",
"@swc/core": "^1.3.102",
"@swc/jest": "^0.2.29",
"@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1",
"@types/cors": "^2",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.10",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@vanilla-extract/dynamic": "^2.1.0",
"@vanilla-extract/jest-transform": "^1.1.1",
"@vanilla-extract/next-plugin": "^2.3.2",
"concurrently": "^8.2.2",
"cors": "^2.8.5",
"eslint": "^8",
"eslint-config-next": "14.0.3",
"express": "^4.18.2",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-environment-node": "^29.7.0",
"msw": "^2.0.9",
"next-router-mock": "^0.9.10",
"ts-node": "^10.9.1",
"ts-node": "^10.9.2",
"tslib": "^2.6.2",
"typescript": "^5.2.2"
},
"msw": {
Expand Down
9 changes: 3 additions & 6 deletions services/web/src/__server__/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { http, HttpResponse } from 'msw';
import { cdnHandler } from './mocks/cdn';
import { feedHandlers } from './mocks/feed';

export const handlers = [
http.get('/api/test', () => {
return HttpResponse.json({ result: true }, { status: 200 });
}),
];
export const handlers = [...cdnHandler, ...feedHandlers];
28 changes: 28 additions & 0 deletions services/web/src/__server__/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable no-console */
import { createMiddleware } from '@mswjs/http-middleware';
import cors from 'cors';
import express from 'express';
import next from 'next';

import { handlers } from './handlers';

const app = express();
const port = 8000;

const nextApp = next({ dev: true, port });

nextApp.prepare().then(() => {
app.use(
cors({
origin: 'http://localhost:3000',
optionsSuccessStatus: 200,
credentials: true,
}),
);
app.use(express.json());
app.use(createMiddleware(...handlers));

app.listen(port, () => console.log(`Server listening on port: ${port}`));
});

export default nextApp;
29 changes: 29 additions & 0 deletions services/web/src/__server__/mockApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { API_URL } from '@linker/ky';
import { DefaultBodyType, http, HttpResponse, PathParams, ResponseResolver } from 'msw';
import { HttpRequestResolverExtras } from 'msw/lib/core/handlers/HttpHandler';

type Resolver = ResponseResolver<HttpRequestResolverExtras<PathParams>> | DefaultBodyType;

type MockApi = Record<keyof typeof http, (endpoint: string, resolver: Resolver) => HttpResponse>;

const methods = Object.keys(http);

export const mockApi: MockApi = new Proxy({} as MockApi, {
get(_target, key: keyof typeof http, _receiver) {
if (!methods.includes(key)) {
throw new Error('invalid method');
}

return (endpoint: string, resolver: Resolver) => {
const url = `${API_URL}${endpoint}`;

return http[key as keyof typeof http](url, (info) => {
if (typeof resolver !== 'function') {
return HttpResponse.json({ result: info });
}

return resolver(info);
});
};
},
});
7 changes: 7 additions & 0 deletions services/web/src/__server__/mocks/cdn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { http, HttpResponse } from 'msw';

export const cdnHandler = [
http.get('https://static.im-linker.com/*', () => {
return HttpResponse.json({ result: '' }, { status: 200 });
}),
];
59 changes: 59 additions & 0 deletions services/web/src/__server__/mocks/feed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* eslint-disable max-len */
import { API_URL } from '@linker/ky';
import { http, HttpResponse } from 'msw';

export const feedHandlers = [
http.get(`${API_URL}/v1/my`, () => {
return HttpResponse.json({ data: my }, { status: 200 });
}),

http.get(`${API_URL}/v1/schedules/upcoming/recommendation`, () => {
return HttpResponse.json({ data: upcomingSchedules }, { status: 200 });
}),
];

const my = {
name: '김태준',
profileImgUrl:
'https://postfiles.pstatic.net/MjAyMjA5MTdfMTE1/MDAxNjYzMzc3MDc1MTA2.bToArUww9E15OT_Mmt5mz7xAkuK98KGBbeI_dsJeaDAg.WJAhfo5kHehNQKWLEWKURBlZ7m_GZVZ9hoCBM2b_lL0g.JPEG.drusty97/IMG_0339.jpg?type=w966',
job: 'Json 상하차 담당',
association: 'Yapp23기 Web1팀',
email: '[email protected]',
tags: [
{
id: 1,
name: '스포츠',
},
{
id: 2,
name: '게임',
},
],
contactsNum: 0,
scheduleNum: 0,
};

const upcomingSchedules = {
scheduleId: '49b258bc-7a6e-4ce5-9ce0-901abcb38485',
title: '일정 1',
profileImgUrl:
'https://postfiles.pstatic.net/MjAyMjA5MTdfMTE1/MDAxNjYzMzc3MDc1MTA2.bToArUww9E15OT_Mmt5mz7xAkuK98KGBbeI_dsJeaDAg.WJAhfo5kHehNQKWLEWKURBlZ7m_GZVZ9hoCBM2b_lL0g.JPEG.drusty97/IMG_0339.jpg?type=w966',
startTs: '2024-01-13 17:55:04',
endTs: '2024-01-13 18:55:04',
recommendations: [
{
tag: {
id: 1,
name: '스포츠',
},
contents: [
{
id: 1,
title: '스포츠 뉴스',
newsProvider: '연합뉴스',
thumbnailUrl: 'https://r.yna.co.kr/global/home/v01/img/yonhapnews_logo_600x600_kr01.jpg',
},
],
},
],
};
4 changes: 3 additions & 1 deletion services/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
"noEmit": true,
"noImplicitAny": true,
"esModuleInterop": true,
"module": "esnext",
"module": "CommonJS",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"outDir": "dist",
"incremental": true,
"preserveValueImports": false,
"plugins": [
{
"name": "next"
Expand Down
Loading

0 comments on commit cd7b6f6

Please sign in to comment.