디버깅 실행 매뉴얼

MSW의 일반적인 문제를 디버깅하는 방법.

아래에서 Mock Service Worker를 애플리케이션에 통합할 때 개발자들이 자주 겪는 문제들을 확인할 수 있습니다. GitHub에 이슈를 올리기 전에 이 페이지를 꼭 읽어보세요. 여러분이 겪고 있는 문제에 대한 해결책이 여기에 있을 가능성이 높습니다.

시작하기 전에

Node.js 버전 확인

프로젝트에서 사용 중인 Node.js 버전을 확인하세요:

node -v

버전이 Node.js v18보다 낮다면, 최신 Node.js 버전으로 업그레이드하세요. 지원되지 않는 Node.js 버전에서 발생하는 문제는 다루지 않습니다.

MSW 버전 확인하기

먼저, 설치된 msw 패키지의 버전을 확인합니다:

npm ls msw

그런 다음, 최신 버전을 확인합니다:

npm view msw version

두 버전이 다르다면, 프로젝트의 msw 버전을 업그레이드하고 문제가 지속되는지 확인합니다.

디버깅 실행 가이드

환경과 MSW 버전을 확인했지만 문제가 여전히 해결되지 않았다면, 이제 디버깅을 시작할 때입니다. 아래에서 MSW와 관련된 예기치 않은 동작을 경험할 때 따라야 할 단계별 디버깅 실행 가이드를 확인할 수 있습니다.

1단계: 설정 확인

먼저 MSW가 올바르게 설정되었는지 확인합니다. worker/server 인스턴스를 가져와서 새로운 request:start 라이프사이클 이벤트 리스너를 추가합니다.

server.events.on('request:start', ({ request }) => {
  console.log('Outgoing:', request.method, request.url)
})

라이프사이클 이벤트 API에 대해 더 알아볼 수 있습니다.

이 리스너를 추가하면 MSW가 가로채는 모든 아웃바운드 요청에 대해 콘솔 메시지를 확인할 수 있습니다. 콘솔 메시지는 다음과 같이 표시됩니다:

Outgoing: GET https://api.example.com/some/request
Outgoing: POST http://localhost/post/abc-123

요청 메서드와 절대 요청 URL이 표시되어야 합니다. 요청 메서드가 다르다면, 요청 핸들러를 조정하여 반영하세요. _상대 요청 URL_이 표시된다면, 요청 클라이언트나 테스트 환경이 올바르게 설정되지 않은 것입니다. 요청 클라이언트의 기본 URL 옵션을 설정하여 절대 요청 URL을 생성하도록 하고, 테스트 환경에서 document.baseURI가 설정되었는지 확인하세요.

그렇지 않다면, 문제가 있는 요청이 출력되는지 확인하세요. 출력된다면 다음 단계로 진행합니다.

문제가 있는 요청이 애플리케이션에서 유일한 요청이라면, MSW 설정 이후에 더미 fetch 호출을 추가하여 이 단계를 확인할 수 있습니다. 예를 들어:

fetch('https://example.com')

문제가 있는 요청(또는 어떤 요청도)에 대해 메시지가 출력되지 않는다면, MSW가 올바르게 설정되지 않아 요청을 가로챌 수 없는 것입니다. 통합 지침을 참조하여 라이브러리를 올바르게 설정했는지 확인하세요.

2단계: 핸들러 확인

문제가 발생한 요청에 대해 생성한 요청 핸들러로 이동하여 리졸버 함수에 콘솔 출력문을 추가합니다.

// src/mocks/handlers.js
import { http } from 'msw'
 
export const handlers = [
  http.get('/some/request', ({ request }) => {
    console.log('Handler', request.method, request.url)
 
    // 나머지 응답 리졸버 코드는 여기에 작성합니다.
  }),
]

페이지나 테스트에서 문제가 발생한 요청이 실행될 때 이 콘솔 메시지가 출력되어야 합니다. 메시지가 출력된다면 다음 단계로 진행합니다.

메시지가 출력되지 않는다면, MSW가 요청을 가로챌 수는 있지만 이 핸들러와 매칭되지 않는다는 의미입니다. 이는 요청 핸들러의 조건(predicate)이 실제 요청 URL과 일치하지 않을 가능성이 높습니다. 조건이 정확한지 확인하세요. 일반적으로 발생하는 문제는 다음과 같습니다:

  • 테스트나 CI 환경에서 설정되지 않은 환경 변수를 경로에 사용한 경우 (예: http.get(BASE_URL + '/path')). 요청 경로의 동적 세그먼트를 검사하고 예상된 값이 있는지 확인하세요.
  • 요청 경로에 오타가 있는 경우. 이전 단계에서 출력된 요청을 주의 깊게 검토하고 오타나 실수를 찾아보세요.

확실하지 않다면 MSW를 사용한 요청 가로채기에 대한 문서를 읽어보세요:

Intercepting requests

REST 및 GraphQL 요청을 가로채고 캡처하는 방법에 대해 알아보세요.

Step 3: 응답 확인

요청 핸들러가 호출되었지만 요청이 여전히 모의 응답을 받지 못한다면, 다음으로 확인할 곳은 모의 응답 자체입니다. 요청 핸들러에서 정의한 모의 응답으로 이동하세요.

// src/mocks/handlers.js
import { http, HttpResponse } from 'msw'
 
export const handlers = [
  http.get('/some/request', ({ request }) => {
    console.log('Handler', request.method, request.url)
 
    return HttpResponse.json({ mocked: true })
  }),
]

유효한 응답을 구성하고 있는지 확인하세요. 응답을 변수에 할당하고 출력하여 검사할 수 있습니다. 또한 더미 모의 응답을 조기에 반환하여 애플리케이션이 이를 받는지 확인할 수도 있습니다.

확실하지 않다면 MSW로 모의 응답을 만드는 방법에 대해 읽어보세요:

Mocking responses

Learn about response resolvers and the different ways to respond to a request.

모의 응답에 문제가 없다면 다음 단계로 진행하세요.

4단계: 애플리케이션 검증

이전 단계들에서 해결책을 찾지 못했다면, 문제는 애플리케이션의 요청/응답 처리 로직에 있을 가능성이 높습니다. 요청을 수행하고 응답을 처리하는 소스 코드로 이동하여 이들이 정확한지 확인하세요. 여러분이 사용하는 요청 프레임워크의 가이드라인을 꼼꼼히 따르면서 의도한 대로 요청을 수행하고 있는지 확인하세요.

문제가 계속된다면, GitHub에 새로운 이슈를 열고 최소한의 재현 저장소를 제공하세요. 문제를 신뢰할 수 있게 재현할 수 있는 저장소가 없는 이슈는 자동으로 닫힐 것입니다.

Common issues

ReferenceError: fetch가 정의되지 않음

이 오류는 현재 프로세스에서 전역 fetch 함수가 정의되지 않았음을 나타냅니다. 이는 두 가지 이유로 발생할 수 있습니다:

  1. Node.js의 오래된 버전(< v17)을 사용하고 있는 경우
  2. 환경에서 해당 전역 함수를 제한하고 있는 경우

Node.js 업그레이드

먼저, fetch 오류가 발생한 터미널에서 다음 명령어를 실행해 현재 사용 중인 Node.js 버전을 확인하세요:

node -v

최신 Node.js 버전에는 전역 Fetch API가 포함되어 있어, 전역 fetch 함수를 사용할 수 있습니다.

환경 설정 수정

Jest와 같은 도구들은 Node.js 환경을 변경하여 현재 전역 변수를 강제로 제거할 수 있습니다. 이런 도구를 사용한다면 설정에서 해당 전역 변수를 다시 추가해야 합니다.

다음은 Node.js에서 전역 Fetch API와 함께 작동하도록 Jest를 설정하는 예제입니다.

이 문제는 여러분의 환경이 어떤 이유로든 Node.js 전역 변수를 가지고 있지 않아 발생합니다. 이는 주로 jest-environment-jsdom을 사용할 때 발생하는데, 이 환경이 의도적으로 내장 API를 폴리필로 대체하여 Node.js 호환성을 깨뜨리기 때문입니다.

이 문제를 해결하려면 jest-environment-jsdom 대신 jest-fixed-jsdom 환경을 사용하세요.

npm i jest-fixed-jsdom
// jest.config.js
module.exports = {
  testEnvironment: 'jest-fixed-jsdom',
}

이 커스텀 환경은 jest-environment-jsdom의 상위 집합으로, 내장 Node.js 모듈이 다시 추가된 버전입니다. 하지만 Jest/JSDOM이 테스트 환경에서 깨뜨리는 많은 것들이 있어 이를 고치는 것은 문제가 많습니다. 이 설정은 임시 해결책입니다.

이 설정이 번거롭다면, Node.js 전역 변수 문제가 없고 기본적으로 ESM을 지원하는 Vitest와 같은 모던 테스트 프레임워크로 마이그레이션하는 것을 고려해 보세요.


테스트에서 모의 응답이 도착하지 않는 경우

HTTP 요청은 비동기적으로 동작합니다. 응답을 받은 후 렌더링되는 UI 엘리먼트처럼, 이러한 요청의 해결에 의존하는 코드를 테스트할 때는 비동기성을 고려해야 합니다. 이는 종종 테스트 프레임워크의 적절한 도구를 사용하여 UI 엘리먼트를 올바르게 기다리는 것을 의미합니다.

// test/suite.test.ts
import { render, screen } from '@testing-library/react'
import { Welcome } from '../components/Welcome'
 
it('환영 문구를 렌더링한다', async () => {
  render(<Welcome />)
 
  // "findBy*" 메서드를 사용하여 엘리먼트를 여러 번 찾아보고,
  // 실패할 경우 예외를 던지도록 합니다.
  // "waitFor"를 대안으로 사용할 수도 있습니다.
  await screen.findByText('Hello, Jack')
})

임의의 setTimeout/sleep 함수를 도입하지 마세요. 이는 테스트를 경쟁 조건에 노출시킬 수 있습니다! 비동기 코드를 기다리는 유일한 신뢰할 수 있는 방법은 그로부터 파생된 상태(예: 특정 UI 엘리먼트가 DOM에 나타나는 것)를 기다리는 것입니다.


오래된 모의 응답 수신 문제

SWR, React Query, Apollo 같은 모던 요청 라이브러리는 뛰어난 사용자 경험과 최적의 런타임 성능을 보장하기 위해 캐시를 도입합니다. 하지만 테스트 중에는 캐시가 자동으로 비활성화되지 않아, 서로 다른 테스트 스위트에서 오래되거나 잘못된 데이터를 받을 수 있습니다.

테스트에서 캐시를 올바르게 비활성화하는 방법은 사용 중인 요청 라이브러리의 문서를 참고하세요.

예를 들어, SWR을 사용해 캐시를 비활성화하는 방법은 다음과 같습니다:

// test/suite.test.ts
import { cache } from 'swr'
 
beforeEach(() => {
  // 각 테스트 실행 전 캐시를 초기화하여
  // 동일한 엔드포인트 요청 시 오래된 응답이 없도록 합니다.
  cache.clear()
})

jest.useFakeTimers를 사용할 때 요청이 해결되지 않는 문제

Jest에서 가짜 타이머를 사용하면 queueMicrotask를 포함한 모든 타이머 API가 모킹됩니다. queueMicrotask API는 Node.js의 전역 fetch에서 요청/응답 본문을 파싱하기 위해 내부적으로 사용됩니다. 따라서 기본 설정으로 jest.useFakeTimers()를 사용할 때, await request.text()await request.json()과 같은 본문 읽기 메서드가 제대로 해결되지 않습니다.

예를 들어, jest.useFakeTimers()를 사용할 때 Jest가 queueMicrotask 호출을 모킹하지 않도록 설정하는 방법은 다음과 같습니다:

jest.useFakeTimers({
  // Jest가 "queueMicrotask" 호출을 모킹하지 않도록 명시적으로 설정
  doNotFake: ['queueMicrotask'],
})

Jest의 가짜 타이머 문서를 참고하세요.

RTK Query 요청이 가로채지지 않는 문제

RTK Query를 사용할 때 흔히 하는 실수 중 하나는 baseQuery 설정에서 baseUrl을 설정하지 않는 것입니다. 이를 설정하지 않으면 요청이 _상대 URL_을 가지게 되며, 이는 Node.js에서 아무런 동작을 하지 않습니다. (자세한 내용은 이 이슈를 참고하세요.)

createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: new URL('/your/api/endpoint', location.origin).href,
  }),
})