MSW 시작하기
msw의 문서를보면 setupWorker 인스턴스 생성 후 setupWorker.start를 해야합니다.
// browser.js
export const worker = setupWorker(...handlers);
// msw.js
const { worker } = await import("../mocks/browser");
await worker.start({
onUnhandledRequest: "bypass",
});
JavaScript
복사
어떤 과정을 통해 Request들을 mocking하는지 라이브러리를 간단히 파헤쳐 보았습니다.
setupWorker(…handlers)
setupWorker를 통해서 SetupWorkerApi를 생성합니다.
SetupWorkerApi
Mocking에 필요한 requests, emitter, events들을 가지고 있습니다. 또한 handler들의 유효성을 체크한 뒤 컨트롤러에 등록합니다.
export function setupWorker(...handlers: Array<RequestHandler>): SetupWorker {
return new SetupWorkerApi(...handlers)
}
JavaScript
복사
SetupWorkerApi 동작
context 생성
const context = {
isMockingEnabled,
startOptions,
requests,
emitter,
events
...
}
JavaScript
복사
사용자 커스텀 handler
Handler들은 InMemoryHandlersController 객체로 배열에 저장해둡니다.
export class InMemoryHandlersController implements HandlersController {
private handlers: Array<RequestHandler>
constructor(private initialHandlers: Array<RequestHandler>) {
this.handlers = [...initialHandlers]
}
...
}
JavaScript
복사
worker.start()를 통해 worker를 실행시킵니다.
worker.start option
옵션
// worker default option
serviceWorker: {
url: '/mockServiceWorker.js',
options: null as any,
},
// logging을 끕니다.
quiet: false,
// Service Worker 인스턴스가 활성화되기까지 대기합니다.
waitUntilReady: true,
// 핸들러에 정의되지 않은 요청 처리방법입니다. 'warn', 'bypass', 'error', callback이 있습니다.
onUnhandledRequest: 'warn',
// 모든 페이지에 등록되어져있는 목업 서버를 찾습니다.
findWorker(scriptURL, mockServiceWorkerUrl) {
return scriptURL === mockServiceWorkerUrl
},
JavaScript
복사
createStartHandler
workerInstance를 생성하며, Request와 Response 둘다 context.workerChannel에 등록해줍니다.
// createStartHandler.ts
context.workerChannel.on(
'REQUEST',
createRequestListener(context, options),
)
context.workerChannel.on('RESPONSE', createResponseListener(context))
JavaScript
복사
createStartHandler - createRequestListener
handleRequest를 동작시켜줍니다.
await handleRequest({
onPassthroughResponse() {
messageChannel.postMessage('PASSTHROUGH')
},
async onMockedResponse(response,{handler, parsedResult}) {
const responseStreamOrNull = response.body
// 모킹된 응답값 전달
messageChannel.postMessage(
'MOCK_RESPONSE',
{
...response,
body: responseStreamOrNull,
},
responseStreamOrNull
)
// quiet 옵션 - logging 여부
if (!options.quiet) {
context.emitter.once('response:mocked', () => {
handler.log({
request: requestCloneForLogs,
response: responseCloneForLogs,
parsedResult,
})
})
}
},
})
JavaScript
복사
createStartHandler - createRequestListener - handleRequest
handler에 해당 API 여부, request header 옵션에 따른 요청 처리를 진행합니다.
// handleRequest
// request:start
emitter.emit('request:start', { request, requestId })
// x-msw-intention:bypass - PASSTHROUGH 메시지 발송
emitter.emit('request:end', { request, requestId })
handleRequestOptions?.onPassthroughResponse?.(request)
return;
// mocking 된 결과 데이터
const lookupResult = await until(() => {
return executeHandlers({request,requestId,handlers})
})
// !lookupResult.data - PASSTHROUGH 메시지 발송
await onUnhandledRequest(request, options.onUnhandledRequest)
emitter.emit('request:unhandled', { request, requestId })
emitter.emit('request:end', { request, requestId })
handleRequestOptions?.onPassthroughResponse?.(request)
return
// !response - PASSTHROUGH 메시지 발송
// status === 302 && x-msw-intention:passthrough - PASSTHROUGH 메시지 발송
emitter.emit('request:end', { request, requestId })
handleRequestOptions?.onPassthroughResponse?.(request)
return
// else
emitter.emit('request:match', { request, requestId })
// 응답값
handleRequestOptions.onMockedResponse(lookupResult.data)
emitter.emit('request:end', { request, requestId })
JavaScript
복사
createStartHandler - createRequestListener - handleRequest - lookupResult
let result;
let matchingHandler;
for (const handler of handlers) {
result = await handler.run({...})
// 응답 존재 시 매칭
if (result !== null) {
matchingHandler = handler
}
// 최초로 매칭된 응답값이 존재 시
if (result?.response) {
break
}
}
if (matchingHandler) {
return {
handler: matchingHandler,
parsedResult: result?.parsedResult,
response: result?.response,
}
}
JavaScript
복사
createStartHandler - createResponseListener
strict-event-emitter 라이브러리를 사용한 Emitter에 response:mocked response:bypass와 같은 모킹에서 사용되는 동작들을 등록해줍니다.
// createResponseListener.ts
context.emitter.emit(
responseJson.isMockedResponse ? 'response:mocked' : 'response:bypass',
{
response,
request,
requestId: responseJson.requestId,
},
)
JavaScript
복사
// strict-event-emitter/Emitter.ts
/**
* 이벤트 기반 프로그래밍을 위한 라이브러리입니다.
* 이벤트 리스너를 등록하고, 해당 이벤트를 발생시킵니다.
* @example
* const emitter = new Emitter<{ hello: [string] }>()
* emitter.emit('hello', 'John')
*/
public emit<EventName extends keyof Events>(
eventName: EventName,
...data: Events[EventName]
): boolean {
const listeners = this._getListeners(eventName)
listeners.forEach((listener) => {
listener.apply(this, data)
})
return listeners.length > 0
}
JavaScript
복사
createStartHandler - keepAlive
mockServiceWorker.js의 message에 콘솔을 찍어보면 지속해서 메시지를 보내는 곳이 있는데 이곳에서 등록된 interval 입니다.
context.keepAliveInterval = window.setInterval(
() => context.workerChannel.send('KEEPALIVE_REQUEST'),
5000,
)
JavaScript
복사