PromleeBlog
sitemap
aboutMe

posting thumbnail
Custom Hook 테스트 - React로 시작하는 TDD 7편
Testing Custom Hooks - TDD with React Part 7

📅

들어가기 전에 🔗

지난 시간까지 우리는
Counter
컴포넌트 안에서 useState를 사용해 숫자를 세고, 조건에 따라 경고 메시지를 보여주었습니다.
하지만 컴포넌트 코드를 다시 한번 살펴볼까요?
화면을 그리는 코드(JSX)와 숫자를 계산하는 코드(Logic)가 한곳에 섞여 있습니다.

만약 이 "숫자 세기" 기능을 다른 페이지에서도 쓰고 싶다면 어떻게 해야 할까요?
코드를 복사해서 붙여넣어야 할까요?
이럴 때 사용하는 것이 바로
Custom Hook
(커스텀 훅)입니다.
오늘은 로직을 뇌(Hook)로, 화면을 몸(Component)으로 분리하고, 뇌만 따로 똑떼어내서 테스트하는 방법을 배워보겠습니다.

1단계: 로직 분리하기 (Refactoring) 🔗

먼저 src/hooks 폴더를 만들고, 컴포넌트에 있던 로직을 useCounter.ts라는 파일로 옮겨보겠습니다.
이 과정은 테스트 코드를 수정하지 않고 내부 구조만 바꾸는
리팩토링
(Refactoring) 단계입니다.

Custom Hook 생성 🔗

src/hooks/useCounter.ts
import { useState } from 'react';
 
// [로직] 숫자를 관리하고 증가시키는 기능만 담당합니다.
export default function useCounter() {
  const [count, setCount] = useState(0);
 
  const increment = () => {
    setCount((prev) => prev + 1);
  };
 
  // 컴포넌트에서 필요한 값과 함수를 반환합니다.
  return {
    count,
    increment,
  };
}

컴포넌트 수정 🔗

이제 Counter.tsx는 직접 계산하지 않고, 방금 만든 Hook을 가져다 쓰기만 하면 됩니다.
src/components/Counter.tsx
import CountButton from './CountButton';
import useCounter from '../hooks/useCounter'; // Hook import
 
export default function Counter() {
  // [수정] 직접 useState를 쓰지 않고 Hook을 사용합니다.
  const { count, increment } = useCounter();
 
  return (
    <div>
      <p>현재 숫자: {count}</p>
      {/* 함수 이름이 handleClick에서 increment로 바뀌었음에 주의하세요 */}
      <CountButton label="+" onClick={increment} />
 
      {count >= 3 && (
        <p style={{ color: 'red' }}>
          숫자가 너무 커요!
        </p>
      )}
    </div>
  );
}

2단계: Hook 테스트 작성 (renderHook & act) 🔗

이제 분리된 useCounter가 잘 동작하는지 테스트해야 합니다.
그런데 Hook은 컴포넌트가 아니라서 render(<useCounter />)처럼 사용할 수 없습니다.
이때 필요한 도구가 바로 renderHookact입니다.

테스트 코드 작성 🔗

src/hooks/useCounter.test.ts 파일을 만들고 아래 코드를 작성합니다.
이번에도 로그를 통해 상태 변화를 추적해 봅시다.
src/hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
import { describe, test, expect } from 'vitest';
 
describe('useCounter Hook 동작 테스트', () => {
  
  test('초기값은 0이며, increment 함수를 호출하면 값이 1 증가한다', () => {
    // 1. Hook 렌더링 (가상 환경)
    const { result } = renderHook(() => useCounter());
 
    console.log(`[Step 1] 초기값 확인: ${result.current.count}`);
    
    // 초기값 검증
    expect(result.current.count).toBe(0);
 
    // 2. 상태 변경 (act 사용 필수!)
    console.log('[Step 2] increment 함수 실행');
    act(() => {
      result.current.increment();
    });
 
    console.log(`[Step 3] 변경된 값 확인: ${result.current.count}`);
 
    // 3. 결과 검증
    expect(result.current.count).toBe(1);
  });
});

3단계: 테스트 실행 및 결과 분석 🔗

이제 Hook 테스트를 실행해 봅시다.
Terminal
npm test

실행 로그 확인 🔗

실행 로그 확인
실행 로그 확인
[Step 1] 초기값 확인: 0
[Step 2] increment 함수 실행
[Step 3] 변경된 값 확인: 1
 
 PASS  src/hooks/useCounter.test.ts
 PASS  src/components/Counter.test.tsx
로그를 보면 act 함수 안에서 increment를 호출한 직후, result.current.count 값이 0에서 1로 변한 것을 확인할 수 있습니다.

또한 기존의 Counter.test.tsx도 여전히
PASS
인 것을 주목해 주세요.
우리는 내부 구현을 완전히 뜯어고쳤지만(useState -> Custom Hook), 사용자가 보는 화면과 동작은 똑같기 때문에 기존 테스트도 깨지지 않고 잘 돌아가는 것입니다.

결론 🔗

소스 코드는 GitHub - Promleeblog/react-tdd-setup 에서 확인할 수 있습니다.
Terminal
git clone https://github.com/PROMLEE/my-tdd-app.git
cd my-tdd-app
git checkout part7
오늘은 로직을 분리하고 독립적으로 테스트하는 방법을 배웠습니다.
이제 우리 애플리케이션은 내부 로직(Hook)과 외부 화면(Component)이 모두 테스트로 관리되고 있습니다.
다음 시간에는 실제 웹 애플리케이션의 핵심인
8편. 비동기 로직 테스트
를 다뤄보겠습니다.

참고 🔗