목록으로

사용자 정의 훅(Custom Hook) 테스트

시리즈
2025. 1. 5. PM 9:00:00
React의 사용자 정의 훅(Custom Hook)은 useState, useEffect, useMemo 등 기본 훅들을 조합해 특정 로직을 재사용하도록 만든 훅입니다. 일반적으로 UI와는 직접적으로 무관하므로, 컴포넌트 없이도 이 훅의 로직만 독립적으로 테스트하고 싶다는 상황이 생길 수 있습니다.

(1) 왜 Custom Hook 테스트가 필요한가?

  • 복잡한 비즈니스 로직이 들어 있는 훅을 재사용할 때, 매번 컴포넌트로 감싸 테스트하기는 번거롭습니다. Form 처리, API 요청, 웹소켓 연결 등을 예로 들 수 있지요.
  • UI와 로직을 분리한 구조라면, 훅만 독립적으로 테스트해 로직이 올바른지 검증합니다.
  • UI 변경과 상관없이 훅의 로직이 잘 동작하는지 빠르게 확인할 수 있으므로, 유지보수 및 리팩토링에 유리합니다.

(2) Custom Hook 테스트 도구

1) 훅 테스팅 라이브러리 추가

@testing-library/react-hooks(react-testing-library)의 renderHook을 사용하는 것입니다. @testing-libraryreact-hooks는 기본 포함되어 있지 않으므로 별도 설치합니다.

2) 실습 컴포넌트 작성

프론트엔드 실습계의 아이돌, 카운터 컴포넌트로 실습해보겠습니다.
기본적인 테스트도 작성해봅니다.

3) useCounter() 사용자 정의 훅 작성

카운터 컴포넌트의 값을 증가 또는 감소하는 비즈니스 로직 useCounter() 훅으로 분리하겠습니다.
특이사항으로 decrement 함수가 비동기 함수라는 점입니다. 이는 의도한 것으로 이 함수가 비동기 동작(예를 들면, API 호출을 하는 등)으로 비즈니스 로직을 수행한다고 가상의 상황을 산정한 것입니다.
이제 useCounter() 사용자 정의 훅을 Counter 컴포넌트에 반영합니다.

4) 사용자 정의 훅 테스트

사용자 정의 훅을 테스트하는 데 주요하게 renderHook()act() 함수가 사용됩니다.
renderHook() 함수로 사용자 정의 훅을 직접 렌더링하고, 그 결과를 RenderResult 객체로 반환하며, renderHook()이 반환하는 결과에서 result 키의 객체로 접근할 수 있습니다. RenderResult 객체는 현재 렌더링 결과로 훅이 반환하는 값을 current로 제공합니다. 그래서 훅이 반환하는 count, increment, decrement에 접근할 수 있지요.
훅 내부에서 useState()가 변화를 일으키는 부분은 리액트의 렌더링 사이클에 영향을 미치므로, act()로 감싸야 합니다. act() 안에서 상태 변화가 일어나 리렌더링이 일어나면 해당 변화까지 프로그램 진행 사이클을 진행시켜서 result.current에 변화가 반영됩니다. 이는 useEvent로 UI 상 상호작용을 하고, 그로 인해 리렌더링이 일어나 렌더링 사이클을 거쳐야 하는 경우에도 act()를 사용해야 합니다. 그렇지 않아도 테스트를 통과하기도 하지만, act() 관련 경고 메시지를 표시하지요.
맨 마지막 테스트를 보면 act()를 사용하는 부분을 비동기로 처리했습니다. 이는 decrement()가 비동기 함수이고, 이를 비동기 처리하기 위해 act()의 인자로 비동기 함수로써 전달하고, act() 자체도 비동기로 실행했습니다. 단, 이는 act()의 비동기 동작 예를 들기 위함이며, act()는 상태 변화로 렌더링 사이클을 사용자 정의 훅 결과에 반영하는 것이 주 용도입니다.

5) 훅 테스트 시 유의점

비동기 로직이 포함된 훅 중에서 시간 소요가 들어가는 경우는 await 비동기 처리와 함께 waitForNextUpdate()waitFor()함수를 사용해 상태 변화가 끝날 때까지 기다려야 합니다.
또, Context가 필요한 경우, renderHook() 함수의 옵션에서 wrapper 옵션을 사용해 Provider로 감싸줄 수 있습니다. 예를 들어, 인증 여부를 다루는 useAuth() 훅이 있고, 이 훅은 AuthProvider라는 Provider를 의존한다고 하는 경우 다음과 같이 테스트 하면 됩니다.

(3) Custom Hook 안에서 Mocking이 필요한 경우

사용자 정의 훅에서 API 호출이나 타이머, 브라우저 API 등을 사용하는 경우가 있습니다. 이럴 때도 Mocking 기법을 동일하게 적용할 수 있습니다.
예를 들면, useFetchUser 훅이 fetch를 써서 유저 정보를 가져온다고 할 때, 테스트에서는 실제 네트워크를 호출하지 않도록 global.fetch = vi.fn()으로 Mock 처리하거나, MSW(Mock Service Worker) 등을 사용해 가짜 응답을 제공합니다. 이에 대해서는 다음 편에서 다루겠습니다.

(4) 이외 챙길 요소

1) 훅과 컴포넌트 분리

  • 훅에 대한 테스트를 충분히 작성해두면, 컴포넌트 테스트를 간소화할 수 있습니다. UI 배치와 이벤트 연결만 확인하고, 비즈니스 로직은 훅에 대해서만 테스트하는 것입니다.
  • 단, 통합 관점에서 전체 화면의 흐름도 별도로 테스트(통합 테스트, E2E 테스트)해봐야 합니다.

2) Mocking과 의존성 주입

  • 훅 내부에서 사용하는 함수나 모듈을 분리해 놓으면(예: apis/puddingcamp.js), 훅 테스트 시 vi.mock(...)을 통해 원하는 시나리오(성공/실패/지연 등)를 자유롭게 구성 가능합니다.

3) Lifecycle 주기 확인

  • useEffect는 의존 배열의 변화를 감지해 재실행됩니다. 상태나 props가 바뀌었을 때도 원하는 타이밍에 호출되는지, 불필요한 호출이 없는지 확인해야 합니다.
  • 예를 들어, 부수효과를 일으키는 useEffect()가 여러 개인 경우, 검증하려는 시점에 다른 컴포넌트 상태일 수 있는 거죠.

4) React 18 이후의 이중 호출(Strict Mode)

  • 개발 모드에서 Strict Mode가 켜져 있으면, useEffect가 마운트 시점에 2번씩 불릴 수 있습니다. 이로 인해 훅 테스트도 예상치 못한 사이드 이펙트가 2번 발생할 수 있습니다.
  • 테스트에는 일반적으로 Strict Mode가 적용되지 않거나, 일관성 있게 처리할 수 있도록 설정하는 경우가 많습니다.
토이스토리 3기 모집 중!
푸딩캠프 뉴스레터를 구독하면 학습과 성장, 기술에 관해 요약된 컨텐츠를 매주 편하게 받아보실 수 있습니다.
목차