Mock Service Worker

이지호_tech 2024. 5. 23. 10:44

MSW

소개

공식문서에는 MSW를

Mock Service Worker is an API mocking library that allows you to write client-agnostic mocks and reuse them across any frameworks, tools, and environments.

로 소개합니다.

 

직역하자면,

Mock Service Worker는 클라이언트에 구애받지 않고 모킹을 작성할 수 있으며, 어떠한 프레임워크, 도구, 환경에서도 재사용할 수 있는 API 모킹 라이브러리 입니다.

 

다른 모킹 서비스와 다르게 특별한 점은, 

 

• API endpoint를 실제 API의 것으로 사용할 수 있다.
• JavaScript를 이용해 웹 애플리케이션 프로젝트에 Mocking 코드를 작성한다.
• 실제 API 개발이 완료되면, MSW에서 핸들러만 제거하면 교체가 끝난다.

 

가 있습니다.

 

해당 장점들은 MSW만의 특징인 

"서비스 워커(Service Worker)가 실제 http요청을 가로채버린다"  덕분에 가능하게 됩니다.

 

정리

MSW은 API가아직 안나왓을때나 에러를 일부러 발생시킬때 주로 사용하게 되고 클라이언트의 http 요청이 전송되면 Service Worker가 네트워크 레벨에서 요청을 가로채고 Mocking된 응답 값을 반환하여 서버와의 통신을 모방합니다.

서비스 워커(Service Worker)

웹 페이지와 별도로 브라우저가 백그라운드에서 실행되는 스크립트로 응용 프로그램, 브라우저, 그리고 네트워크 사이의 프록시 서버 역할을 합니다.

 

사용 방법

핸들러

특정 API 경로로 요청이 시작되었을때 우리가 의도한 Mockup Data가 사용되게 하기 위해서는 Mockup 처리를 원하는 API 경로와 이에 따른 Mockup Data를 맵핑하는 과정이 필요합니다.

Handler는 요청을 가로채고, 검사하고, 응답을 조작할 수 있는 로직을 포함합니다.

   import { rest } from 'msw';

   export const handlers = [
     rest.get('/api/user', (req, res, ctx) => {
       return res(
         ctx.status(200),
         ctx.json({
           username: 'admin',
           email: 'admin@example.com'
         })
       );
     })
   ];

 

리졸버

Resolver는 위의 Handler에서 `(req, res, ctx) => {...}` 형태의 코드를 일컬으며 백엔드에서 API 개발시 작성되는 서비스로직 과 유사합니다. 각 API경로로 들어왔을때 보내진 정보를 갖고 데이터를 가공하여 반환하는 역할을 합니다.

 

import { ResponseResolver, RestRequest, RestContext } from 'msw';

export const mockUser: ResponseResolver<RestRequest, RestContext> = (req, res, ctx) => {
  // URL 파라미터에서 userId 추출
  const userId = req.params.userId;

  // 동적으로 사용자 정보 생성
  const userInfo = {
    id: userId,
    firstName: 'John',
    lastName: 'Doe',
    age: 30 + parseInt(userId, 10) % 40 // 나이를 동적으로 계산
  };

  return res(
    ctx.status(200), // HTTP 상태 코드 200으로 설정
    ctx.json(userInfo) // 응답 본문을 JSON 형태로 설정
  );
};

 

폴더 구조

 src 폴더에 mocks 폴더를 만들고, MSW 관련 파일을 관리하면, app.tsx와 MSW 로직의 관심사 분리가 가능합니다.

 

또한 API 핸들러를 작성하다 보면 꽤 많은 핸들러가 생기게되고, 관리가 필요하게 됩니다.

따라서 핸들러 코드 또한 MSW 코어 파일들과 분리를 하는게 좋습니다.

 

   import { rest } from 'msw';

   export const userHandlers = [
     rest.get('/api/users', (req, res, ctx) => {
       return res(
         ctx.status(200),
         ctx.json([{ id: 1, name: 'John Doe' }])
       );
     }),
     rest.get('/api/users/:userId', (req, res, ctx) => {
       const { userId } = req.params;
       return res(
         ctx.status(200),
         ctx.json({ id: userId, name: 'John Doe' })
       );
     })
   ];

 

   import { userHandlers } from './userHandlers';
   import { productHandlers } from './productHandlers';

   export const handlers = [
     ...userHandlers,
     ...productHandlers
   ];

 

해당 구조를 사용하면 각 핸들러 파일이 특정 도메인 또는 기능에 초점을 맞추게 되어 관리가 쉬워집니다.

 

또한, 

mocks/ 
	handlers/ 
		user.js 
		checkout.js 
		index.js

같은 식으로 최대한 API 주소를 따라서 파일을 정리하는 것이 좋습니다.

 

공식 독스 기준 추천하는 구조들

 

Structuring Handlers

모든 성공 상태를 handlers.js 파일에 정의합니다.  이렇게 하면 기본 요청 처리 로직과 런타임에 추가되는 오버라이드를 명확히 구분할 수 있습니다.

특정 요청에 대해 다른 행동이 필요할 때, 기본 핸들러를 오버라이드하여 사용합니다.  ex) 에러상황 테스트

// mocks/handlers.js
import { http, HttpResponse } from 'msw'
 
export const handlers = [
  http.get('/user', () => {
    return HttpResponse.json({ name: 'John Maverick' })
  }),
]



// Overrides
it('handles errors when fetching the user', () => {
  server.use(
    http.get('/user', () => {
      return new HttpResponse(null, { status: 500 })
    })
  )

  render(<UserComponent />)
  expect(screen.getByRole('alert')).toHaveText('Error!')
})

 

또한 복잡한 시나리오에서는 요청 처리 로직의 여러 부분을 한 번에 캡슐화하는 고차 응답 리졸버를 도입하여 여러 핸들러에 걸쳐 재사용할 수 있습니다.

import { http, HttpResponse } from 'msw'
import { withAuth } from './withAuth'
 
export const handlers = [
  http.get('/cart', withAuth(getCartResolver)),
  http.post('/checkout/:cartId', withAuth(addToCartResolver)),
]

//API 엔드포인트에 대해 인증 헤더를 확인하는 로직
function withAuth(resolver) {
  return (input) => {
    const { request } = input
 
    if (!request.headers.get('Authorization')) {
      return new HttpResponse(null, { status: 401 })
    }
 
    return resolver(input)
  }
}

 

Network behavior overrides

 

네트워크는 동적이기 때문에 .use() API를 통해 특정 네트워크 행동을 오버라이드할 수 있는 기능을 제공합니다.

 

초기 핸들러: setupWorker() 또는 setupServer() 함수에 제공된 요청 핸들러 목록입니다. 이들은 기본적으로 설정된 네트워크 행동을 설명합니다.

런타임 핸들러: .use() 함수를 사용하여 추가된 요청 핸들러입니다. 이 핸들러들은 런타임에 동적으로 추가되며, 초기 핸들러보다 우선순위를 가집니다.

 

네트워크 오버라이드 유형

- 영구 오버라이드: .use()를 호출하면 기본적으로 영구적인 오버라이드가 생성됩니다. 이 오버라이드는 제거되기 전까지 계속 적용됩니다.

- 일회성 오버라이드: { once: true } 옵션을 사용하여 일회성 오버라이드를 설정할 수 있습니다. 이 핸들러는 한 번 사용된 후 자동으로 제거됩니다.

const server = setupServer(
  http.get('/resource', () => {
    return HttpResponse.text('Fallback')
  })
)
 
server.use(
  http.get(
    '/resource',
    () => {
      return HttpResponse.text('One-time override')
    },
    //일회성 오버라이드
    { once: true }
  )
)

 

요청 핸들러 재설정

- 핸들러 재설정: .resetHandlers() 메소드를 호출하여 런타임에 추가된 모든 요청 핸들러를 제거할 수 있습니다. 이는 테스트 간에 격리된 네트워크 행동을 보장하는 데 유용합니다.
=>          ,       .


- 초기 핸들러 교체: .resetHandlers() 메소드에 새로운 초기 핸들러 목록을 제공하여 초기 핸들러를 교체할 수 있습니다.

 

import { rest } from 'msw';
import { setupServer } from 'msw/node';

// 초기 핸들러 설정
const server = setupServer(
  rest.get('/api/user', (req, res, ctx) => res(ctx.json({ username: 'initial' })))
);

beforeAll(() => server.listen());

// afterEach에서 resetHandlers()를 호출하여 각 테스트 후에 핸들러를 초기 상태로 리셋
afterEach(() => server.resetHandlers());

afterAll(() => server.close());

test('returns initial user', async () => {
  // 첫 번째 테스트에서는 초기 핸들러를 사용
});

test('returns new user', async () => {
  // 두 번째 테스트에서는 새로운 핸들러를 추가
  server.use(
    rest.get('/api/user', (req, res, ctx) => res(ctx.json({ username: 'new user' })))
  );

  // 테스트 실행
});

 

Avoid request assertions

요청의 유효성을 검증하는 가장 좋은 방법은 요청 핸들러에서 그 행동을 충실히 설명하는 것입니다.

예를들어, 로그인 엔드포인트를 설명할 때, 테스트에서 요청 본문에 이메일 데이터 필드가 포함되었는지를 단언할 필요는 없습니다. 

대신, 요청 핸들러에서 이메일이 없으면 에러 응답을 반환하면 됩니다.

export const handlers = [

  http.post('/login', async ({ request }) => {

    const data = await request.formData();

    const email = data.get('email');

    if (!email) {

      return new HttpResponse('Missing email', { status: 400 });

    }

  }),

];

 

선언하지 않은 http 요청을 수행하지 않도록 하려면, worker.start() 또는 server.listen()의 onUnhandledRequest 옵션을 사용합니다.

 

server.listen({

  // MSW가 일치하는 요청 핸들러가 없는 요청을

  // 만났을 때 에러를 발생시키도록 설정합니다.

  onUnhandledRequest: 'error',

});

 

Custom request predicate

다른 기준에 따라 요청을 가로채기 위한 사용자 정의 매칭 로직을 생성할 수도 있습니다. 

예를 들어, 특정 쿼리 파라미터를 가진 요청을 가로채거나, JSON 요청 본문에 특정 속성이 있는 요청을 가로채는 것입니다.

 

// handlers.js

import { http, HttpResponse } from 'msw'

import { withSearchParams } from './withSearchParams'

export const handlers = [
  http.get(
    '/user',
    withSearchParams(
      // "GET /user" 요청 중 "userId" 쿼리 파라미터가 있는 요청만 매칭
      (params) => params.has('userId'),
      ({ request, params, cookies }) => {
        return HttpResponse.json({
          name: 'John Maverick',
        })
      }
    )
  ),
]

function withSearchParams(predicate, resolver) {
  return (args) => {
    const { request } = args
    const url = new URL(request.url)
 
    if (!predicate(url.searchParams)) {
      return passthrough()
    }
 
    return resolver(args)
  }
}

 

Dynamic mock scenarios

개발 중이거나 작업을 발표할 때, 런타임에 다양한 모의 시나리오로 전환할 필요가 있을 수 있습니다. 

데모 중에 소스 코드를 수정하는 것은 유저에게 좋은 경험이 아닙니다.

이를 해결하는 한 가지 방법은 여러 시나리오(핸들러 오버라이드)를 선언하고 런타임 기준, 예를 들어 쿼리 파라미터에 따라 조건부로 적용하는 것입니다.

// mocks/browser.js
import { handlers } from './handlers'
import { scenarios } from './scenarios'
 
// Since the worker is registered on the client, you can read
// the pages query parameters before assigning request handlers.
const scenarioName = new URLSearchParams(window.location.search).get('scenario')
const runtimeScenarios = scenarios[scenarioName] || []
 
const worker = setupWorker(...runtimeScenarios, ...handlers)

// mocks/scenarios.js
import { http, HttpResponse } from 'msw'
 
export const scenarios = {
  success: [
    http.get('/user', () => {
      return HttpResponse.json({ name: 'John Maverick' })
    }),
  ],
  error: [
    http.get('/user', () => {
      return new HttpResponse(null, { status: 500 })
    }),
  ],
}

 

URL의 쿼리 파라미터에 따라 적절한 시나리오를 동적으로 적용하게됩니다.

URL에 ?scenario=testScenario와 같은 쿼리 파라미터를 추가함으로써, testScenario에 해당하는 네트워크 요청 핸들러를 활성화할 수 있습니다

'' 카테고리의 다른 글

Github Action build error 해결  (0) 2024.07.24
Next js Lottie 사용할때 Document is not defined Build 에러  (0) 2024.06.26
JS - 클로저 (Closure)  (0) 2024.04.24
JS - 스코프(Scope)  (0) 2024.04.24
Babel 이란? +babel-plugin-macros  (1) 2024.04.19