Search

MSW는 동작 분석

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
복사