푸딩캠프 이야기
토이스토리 2기 리뉴얼
토이 프로젝트를 만드는 푸딩캠프 프로그램, 토이 스토리 2기를 리뉴얼해 새롭게 선보일 예정입니다. 큰 변화는 다음과 같아요.
- 프로젝트 개발 기간 : 12주 ➡️ 6주 (총 22주 ➡️ 11주)
- 프로젝트 목표 : 출시와 운영, 그리고 결제 시스템 연동까지 한 포트폴리오 개발
- 모집 직군 : 개발자, PM/서비스 기획, UX/UI 디자인
- 모집 분야 : 소수 모집
곧 리뉴얼 출시하여 모집하는데, 뉴스레터에서 가장 먼저 알려드릴게요!
Vitest에서 Mocking과 의존성 주입(Dependency Injection) 처리하기
리액트 애플리케이션을 테스트할 때 자주 맞닥뜨리는 과제 중 하나가 외부 의존성에 대한 처리입니다. 예를 들어, HTTP 요청 라이브러리를 통해 서버와 통신하거나, 브라우저의 네이티브 API를 사용하는 코드가 들어갈 경우 실제 네트워크나 시스템 환경에 의존하지 않고 컴포넌트를 독립적으로 검증하기가 쉽지 않습니다. 예를 들면, window.fetch, localStorage가 대표적이지요.
이럴 때 사용하는 대표적인 기법이 바로 모킹(Mocking)과 의존성 주입입니다. Vitest에서는 vi.fn(), vi.mock() 등으로 Mock 함수를 생성하거나 모듈을 가짜로 치환합니다. 이를 활용해 복잡한 외부 의존성을 깔끔하게 분리하고, 테스트하려는 대상 로직에만 집중할 수 있습니다.
1. Mocking의 기본 원리와 활용 방법
Mocking이란 실제 코드나 외부 서비스 대신 테스트 목적에 맞춰 가짜(Given) 구현을 제공하는 것을 의미합니다. 쉽게 말해, 서버 API 응답이 필요하지만 실제 서버에 요청을 보내고 싶지 않을 때, 미리 준비된 테스트용 데이터를 돌려주는 가짜 함수를 만들어 쓰는 방식입니다. 이렇게 하면 네트워크나 외부 환경이 달라져도 테스트가 안정적으로 동작하며, 테스트 속도도 빨라져요.
Vitest에서 Mock은 vi.fn()을 사용해 만듭니다. 이 함수는 호출되면 아무것도 하지 않지만, 몇 번 불렸는지, 어떤 인자가 전달되었는지 등을 추적합니다.
이처럼 vi.fn()가 생성한 mockFn을 실제 로직으로 수행하진 않아도, 호출 횟수나 인자를 기록한다는 점이 핵심입니다. 또한 필요하다면 mockFn.mockReturnValue 혹은 mockFn.mockImplementation 등을 통해 특정 결과값을 반환하게 만들 수도 있고요.
보다 실제에 가깝게 HTTP 요청이 성공하면 데이터를 반환한다, 같은 기능을 흉내 내려면 mockImplementation으로 함수를 구성할 수도 있습니다. 예를 들어, 첫 번째 호출 시에는 정상 데이터, 두 번째 호출 시에는 오류를 발생시키기 같은 상황도 얼마든지 시나리오대로 설정할 수 있습니다.
2. 모듈 단위 Mocking(vi.mock)과 의존성 분리
조금 더 복잡한 상황에서는 컴포넌트 안에서 직접 호출하는 함수가 아닌, 별도의 파일로 분리된 모듈을 가져다가 사용하는 경우가 많아요. 예를 들어, api.js 파일 안에 fetchUserData라는 함수를 작성해 놓고, React 컴포넌트에서 이를 불러다 쓰는 식이지요. 이렇게 코드가 모듈화된 상황에서도 특정 테스트 시점에 해당 모듈을 가짜로 치환하려면 vi.mock()을 사용합니다.
아래 예시는 fetchUserData라는 함수를 제공하는 api.js 모듈을 가정하고, 컴포넌트가 이것을 호출해 화면에 유저 이름을 출력한다는 시나리오를 테스트하는 코드입니다.
이 fetchUserData() 함수를 컴포넌트에서 사용해보겠습니다.
테스트에서 실제 서버를 호출하지 않고, fetchUserData를 가짜로 만들어 원하는 결과를 돌려주려면 다음과 같이 vi.mock()을 사용합니다.
이 코드에서 가장 핵심적인 부분은 vi.mock('../api') 구문입니다. 이렇게 작성하면 ../api 모듈 내의 모든 함수가 자동으로 Mock 함수로 대체됩니다. fetchUserData가 실제 네트워크 요청을 보내지 않고, 우리가 직접 mockResolvedValue 등을 설정해서 원하는 데이터를 즉시 반환하도록 만듭니다. 테스트 실행 시점에는 가짜로 만들어진 fetchUserData만 사용하게 되므로, 네트워크 상태에 영향을 전혀 받지 않고 컴포넌트를 검증합니다.
만약 API 응답이 실패하는 경우도 테스트하고 싶다면, mockRejectedValue 등을 사용해 에러를 발생시키게 만들 수도 있습니다. 이렇게 시나리오별로 성공/실패를 설정해 보고, 컴포넌트가 에러 메시지를 제대로 보여주는지 확인할 수 있습니다.
3. 컴포넌트끼리 의존성이 있을 때의 Mocking 구성
실무 프로젝트에서는 한 컴포넌트가 다른 컴포넌트를 하위로 포함하고, 그 컴포넌트가 다시 별도의 API 요청이나 특정 로직을 수행하는 구조가 흔합니다. 이럴 때 상위 컴포넌트는 하위 컴포넌트의 동작까지 모두 테스트해야 할까요, 아니면 하위 컴포넌트를 Mock 처리해서 상위 컴포넌트에 집중해야 할까요?
진리대로, 상황에 따라 다릅니다. 단위(Unit) 테스트 차원에서 상위 컴포넌트의 책임 범위를 명확히 하고 싶다면, 하위 컴포넌트를 가짜로 치환하고 상위 컴포넌트가 하위 컴포넌트에게 올바른 props를 전달하는지만 검증할 수도 있습니다. 반면, 통합(Integration) 테스트 관점에서 실제 하위 컴포넌트가 제대로 동작하는지까지 포함해 전체 화면을 시험하고 싶다면, 하위 컴포넌트까지 실제로 렌더링하도록 둘 수도 있습니다.
만약 Mocking을 통해 하위 컴포넌트를 치환하기로 했다면, vi.mock('./ChildComponent') 식으로 해당 모듈을 Mock 처리하고, ChildComponent.mockImplementation(...) 등을 통해 가짜 컴포넌트를 지정할 수 있습니다. 이 가짜 컴포넌트가 props를 잘 받았다면 특정 텍스트를 보여주기 정도만 해도, 상위 컴포넌트를 테스트하기에는 충분할 수 있습니다.
4. 의존성 주입(Dependency Injection)과 테스트 용이성
Mocking과 밀접하게 연관된 개념이 의존성 주입(Dependency Injection)입니다. 의존성 주입이란, 컴포넌트가 내부에서 직접 어떤 모듈을 가져와 쓰는 대신, 외부에서 필요한 모듈 또는 함수를 전달받아 사용하는 방식을 말합니다. 이렇게 하면 테스트 시점에 실제 구현 대신 Mock 구현을 전달할 수도 있으므로, 자연스럽게 테스트 범위를 조정하기가 쉬워집니다.
예를 들어, 아래처럼 Subscription 컴포넌트에서 이메일 구독 로직을 직접 관리하지 않고, “onSubscribe”라는 함수를 props로 전달받는 식으로 바꿔 놓으면, 테스트에서 쉽게 Mock 함수를 주입할 수 있습니다.
테스트에서는 이 컴포넌트를 렌더링할 때, 실제 구현이 아닌 Mock 함수를 onSubscribe로 넘겨줍니다. 그러면 컴포넌트 내에서 이메일을 구독하는 과정을 테스트하기 위해 별도의 모듈 Mocking을 할 필요 없이, 간단히 Mock 함수가 제대로 불렸는지, 인자로 올바른 값이 전달되었는지만 확인하면 됩니다. 이러한 방식의 코드는 의존성 주입을 활용해 설계된 대표적인 예이지요.
5. 마치며
정리하면, Mocking과 의존성 주입은 리액트 테스트에서 외부 요인을 최소화하고, 특정 로직이나 컴포넌트 기능에 집중해 검증할 수 있도록 해 주는 중요한 도구입니다. Vitest가 제공하는 vi.fn(), vi.mock(), mockResolvedValue, mockRejectedValue 등의 API를 잘 활용하면, 실제 네트워크나 복잡한 하위 컴포넌트 로직에 구애받지 않고 원하는 시나리오를 자유자재로 재현할 수 있습니다.
특히 다음과 같은 상황에서 Mocking을 적용해 보길 권장합니다.
- 네트워크 요청 : 테스트 시점마다 서버 상태가 달라지지 않도록, 동일한 Mock 데이터를 반환하게 설정합니다.
- 랜덤성, 시간 : 무작위 수를 생성하거나, 특정 시간마다 동작하는 로직이 있으면 가짜 타이머나 Mock함수를 이용해 시점을 고정해 줍니다.
- 비정상 시나리오 : 서버가 에러를 던지거나, API 호출이 실패하는 경우 등도 Mock를 통해 손쉽게 시뮬레이션할 수 있습니다.
- 하위 컴포넌트 : 통합 테스트가 아니라면, 하위 컴포넌트에 대한 의존성을 제거하고 상위 컴포넌트의 책임 범위만 집중해서 검사하는 것도 방법입니다.
이 과정을 통해 외부 의존성 때문에 테스트가 어려웠던 부분을 훨씬 단순화하며, 동작이 실패했을 때나 예상과 다른 응답이 돌아올 때도 쉽게 시나리오를 재현합니다. 그만큼 Mocking과 의존성 주입 기법을 잘 익혀두면, 리액트 애플리케이션의 복잡도가 커져도 테스트 품질과 개발 효율을 모두 유지할 수 있게 됩니다.
의견을 남겨주세요