PromleeBlog
sitemap
aboutMe

posting thumbnail
테스트 관점에서 본 컴포넌트 구조 - React로 시작하는 TDD 11편
Improving Component Structure from a Testing Perspective - TDD with React Part 11

📅

🚀

들어가기 전에 🔗

혹시 테스트 코드를 작성하다가 이런 경험 있으신가요?

  1. "이 컴포넌트 하나 테스트하려는데 Mocking 해야 할 게 10개나 되네..."
  2. "화면 색상만 바꿨는데 테스트가 다 깨져버렸어."
  3. "Context Provider가 없다고 에러가 계속 나네."

이런 고통은 코드가
테스트하기 어렵게 짜여있기(Untestable)
때문에 발생합니다.
TDD의 가장 큰 장점 중 하나는
테스트하기 어려운 코드를 거부한다
는 점입니다.
오늘은 구조를 개선하여 테스트와 유지보수 두 마리 토끼를 잡는 방법을 알아보겠습니다.

🚀

나쁜 예: 모든 것을 혼자 하는 컴포넌트 🔗

가장 흔한 안티 패턴(Anti-Pattern)은 컴포넌트 하나가
데이터 가져오기, 데이터 가공하기, 화면 그리기
를 모두 담당하는 경우입니다.

테스트하기 어려운 컴포넌트 (Bad) 🔗

src/components/BadUserProfile.tsx
import { useEffect, useState } from 'react';
 
interface User {
  firstName: string;
  lastName: string;
  email: string;
}
 
export default function BadUserProfile() {
  const [user, setUser] = useState<User | null>(null);
 
  // [문제 1] 컴포넌트가 '데이터 가져오기'를 직접 수행함 (의존성 높음)
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users/1')
      .then((res) => res.json())
      .then((data) => {
        // [문제 2] 데이터를 변환하는 '비즈니스 로직'이 섞여 있음
        const formattedData = {
          ...data,
          // 이름은 대문자로 변환해야 한다는 규칙
          firstName: data.name.split(' ')[0].toUpperCase(),
          lastName: data.name.split(' ')[1].toUpperCase(),
        };
        setUser(formattedData);
      });
  }, []);
 
  if (!user) return <div>로딩 중...</div>;
 
  return (
    <div>
      {/* [문제 3] 화면 그리기 로직 (UI) */}
      <h1>{user.firstName} {user.lastName}</h1>
      <p>{user.email}</p>
    </div>
  );
}
이 컴포넌트를 테스트하려면 fetch를 모킹해야 하고, 비동기 처리를 기다려야(findBy) 합니다.
단순히 "이름이 굵게(h1) 나오는지"만 확인하고 싶은데도 말이죠.

🚀

개선 전략 1: UI와 로직의 분리 🔗

이 문제를 해결하는 고전적이지만 강력한 패턴이
Container - Presentational 패턴
입니다.
쉽게 말해 똑똑한 놈(로직)과 단순한 놈(UI)으로 나누는 것입니다.
(최근에는 Hook으로 로직을 분리하지만, UI를 순수하게 만드는 원칙은 동일합니다.)

1. Presentational 컴포넌트 (UI 담당) 🔗

이 컴포넌트는 데이터가 어디서 왔는지 모릅니다. 그냥 props로 받아서 그리기만 합니다.
테스트 난이도: 최하
src/components/UserProfileView.tsx
// 데이터 타입 정의
export interface UserProfileProps {
  firstName: string;
  lastName: string;
  email: string;
}
 
export default function UserProfileView({ firstName, lastName, email }: UserProfileProps) {
  return (
    <div>
      <h1>{firstName} {lastName}</h1>
      <p>{email}</p>
    </div>
  );
}

2. Container 컴포넌트 (로직 담당) 🔗

데이터를 가져오고 가공해서 UI 컴포넌트에 넘겨줍니다.
src/components/UserProfileContainer.tsx
import { useEffect, useState } from 'react';
import UserProfileView from './UserProfileView';
 
export default function UserProfileContainer() {
  const [user, setUser] = useState(null);
 
  useEffect(() => {
    // ... fetch 및 데이터 가공 로직 (혹은 Custom Hook 사용)
    // 여기서는 생략합니다.
  }, []);
 
  if (!user) return <div>로딩 중...</div>;
 
  // 가공된 데이터를 UI 컴포넌트에 주입
  return <UserProfileView {...user} />;
}

🚀

테스트 작성 및 실행 🔗

이제 분리된 UI 컴포넌트(UserProfileView)를 테스트해 보겠습니다.
fetchMSW 같은 복잡한 설정이 전혀 필요 없습니다.

UI 테스트 코드 작성 🔗

src/components/UserProfileView.test.tsx
import { render, screen } from '@testing-library/react';
import { describe, test, expect } from 'vitest';
import UserProfileView from './UserProfileView';
 
describe('UserProfileView UI 테스트', () => {
  test('props로 받은 이름과 이메일을 정확히 표시한다', () => {
    // 1. 가짜 데이터(Mock Data) 준비
    // API 호출 없이 우리가 원하는 데이터를 직접 주입합니다.
    const mockUser = {
      firstName: 'KIM',
      lastName: 'CHULSU',
      email: 'test@example.com'
    };
 
    render(<UserProfileView {...mockUser} />);
    
    // 2. 로그 확인
    console.log('[UI Test] 순수 컴포넌트 렌더링 확인');
    screen.debug();
 
    // 3. 검증
    // h1 태그 안에 이름이 들어있는지 확인
    expect(screen.getByRole('heading')).toHaveTextContent('KIM CHULSU');
    // 이메일이 문서에 존재하는지 확인
    expect(screen.getByText('test@example.com')).toBeInTheDocument();
  });
});

🚀

테스트 실행 및 결과 분석 🔗

터미널에서 테스트를 실행합니다.
Terminal
npm test -- src/components/UserProfileView.test.tsx

성공 로그 확인 🔗

성공 로그 확인
성공 로그 확인
[UI Test] 순수 컴포넌트 렌더링 확인
<body>
  <div>
    <div>
      <h1>
        KIM CHULSU
      </h1>
      <p>
        test@example.com
      </p>
    </div>
  </div>
</body>
 
PASS src/components/UserProfileView.test.tsx

결과 분석 🔗


🚀

결론 🔗

소스 코드는 GitHub - Promleeblog/react-tdd-setup 에서 확인할 수 있습니다.
Terminal
git clone https://github.com/PROMLEE/my-tdd-app.git
cd my-tdd-app
git checkout part11
오늘은 테스트를 방해하는 나쁜 구조를 개선하고 결과를 확인했습니다.

테스트를 짜기 어렵다면 억지로 짜지 말고,
구조를 개선하라
는 신호로 받아들이세요.
구조가 좋아지면 테스트는 자연스럽게 쉬워집니다.
다음 시간에는
12편. 리팩토링을 지원하는 테스트 구성
을 통해, 디자인이나 내부 구현이 바뀌어도 깨지지 않는 테스트를 만드는 비결을 알아보겠습니다.

참고 🔗