개인과제

React 프로젝트 유닛 테스트: 필요성과 실제 구현 경험

choijming21 2025. 1. 19. 17:56

1. 유닛 테스트란 ?

유닛 테스트는 소프트웨어의 개별적인 단위(컴포넌트, 함수 등)가 의도한 대로 작동하는지 확인하는 테스트입니다.

 

 

 

2. 유닛테스트가 필요한 이유?

 1. 버그 예방

  • 코드 변경 시 기존 기능이 망가지지 않았는지 확인
  • 에러를 초기에 발견하여 수정 비용 절감

 

2. 코드 품질 향상

  • 테스트를 작성하며 코드의 문제점을 발견
  • 더 나은 코드 구조와 설계를 유도

 

3. 리팩토링 용이성

  • 안전한 코드 수정 가능
  • 기능이 올바르게 동작하는지 즉시 확인

 

4. 문서화 효과

  • 테스트 코드가 컴포넌트의 사용법을 보여주는 예시 역할
  • 새로운 팀원의 코드 이해도 향상

 

 

 

3. 주요 테스트 라이브러리

1. Jest

  • facebook에서 만든 JavaScrip 테스팅 프레임 워크
  • 쉬운 설정과 사용법
  • 스냅샷 테스팅 지원
  • 풍부한 matcher 함수 제공

2. React Testing Library

  • 사용자 관점에서의 테스트에 초첨
  • DOM 기반 테스팅
  • 접근성 고려한 쿼리 함수 제공

3. Enzyme

  • Airbnb에서 개발
  • 컴포넌트의 내부 구현에 접근 가능
  • 컴포넌트 렌더링 방식 선택 가능

 

 

 

4. 실제 프로젝트 테스트 구현 경험

4.1 테스트 환경 설정

필요한 패키지 설치

npm install --save-dev jest @types/jest ts-node ts-jest @testing-library/react identity-obj-proxy jest-environment-jsdom @testing-library/jest-dom jest-svg-transformer

 

Jest 설정

// jest.config.ts
export default {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  transform: {
    '^.+\\.(ts|tsx)?$': ['ts-jest', {
      tsconfig: 'tsconfig.app.json'
    }]
  },
  moduleNameMapper: {
    '^.+\\.svg$': 'jest-svg-transformer',
    '\\.(css|less|sass|scss)$': 'identity-obj-proxy',
  },
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts']
};

 

 

4.2 발생한 문제들과 해결 과정

1) TextEncoder is not defined 에러

Jest 테스트 환경에서 TextEncoder가 정의되지 않아 테스트 실행이 불가능했습니다. 

jest ReferenceError: TextEncoder is not defined

 

 

시도1: text-encoding 라이브러리 사용

import "@testing-library/jest-dom";
import { TextEncoder, TextDecoder } from 'text-encoding';

global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;

 

하지만 이 방법으로는 해결되지 않았습니다,

 

최종 해결: jsest.setyp.ts 파일에 Node.js util 모듈 사용

import "@testing-library/jest-dom";
import { TextEncoder } from "util";

global.TextEncoder = TextEncoder;

 

 

 

4.2 TypeScript 타입 에러 문제

문제 상황

'Screen' 형식에 'getByLabelText' 속성이 없습니다.
'Screen' 형식에 'getByRole' 속성이 없습니다.
'Screen' 형식에 'getByText' 속성이 없습니다.

Testing Library의 쿼리 함수들에 대한 TypeScript 타입 정의가 인식되지 않았습니다.

 

시도1: 타입 단언(Type Assertion)사용

(screen as any).getByLabelText("아이디")

작동은 했지만, TypeScript의 타입 안정성을 포기하는 방법이라 적절하지 않았습니다.

 

시도 2: @types/testing-library__react 설치

npm install --save-dev @types/testing-library__react

타입 정의만 추가했지만 여전히 문제가 해결되지 않았습니다.

 

 

최종 해결: Jest의 기본 matcher 사용

// 변경 전 - 타입 에러 발생
expect(element).toBeInTheDocument();

// 변경 후 - 타입 에러 없음
expect(element).toBeTruthy();
expect(element.textContent).toBe("로그인");

해결 이유

 

  • toBeTruthy()와 toBe()는 Jest의 기본 matcher로 TypeScript가 기본적으로 인식
  • 추가적인 타입 정의나 설정 없이도 사용 가능
  • 간단한 존재 여부나 값 비교에 충분한 기능 제공

이렇게 수정하니 불필요한 타입 설정 없이도 테스트 코드가 깔끔하게 작동하게 되었습니다.

 

 

 

4.3 비동기 동작 테스트 문제

문제 상황

TestingLibraryElementError: Unable to find an element with the text: 아이디는 4~20자 사이여야 합니다.

 

회원가입 폼의 유효성 검사 에러 메시지를 찾지 못하는 문제가 발생했습니다.

 

시도 1: async/await만 사용

const error = await screen.getByText(/아이디는 4~20자 사이여야 합니다/);

여전히 타이밍 이슈가 발생했습니다.

 

최종 해결: waitFor와 정규표현식 사용

await waitFor(() => {
  const error = screen.getByText(/아이디는 4~20자 사이여야 합니다/);
  expect(error).toBeTruthy();
});

 

  • waitFor를 사용하여 안정적으로 요소를 찾을 때까지 대기
  • 정규표현식으로 텍스트 매칭의 유연성 확보

 

 

4.4 Form Submit 테스트 관련 오류

문제 상황

'HTMLFormElement | null' 형식의 인수는 'Element | Node | Document | Window' 형식의 매개 변수에 할당될 수 없습니다.

 

 

최종 해결: form에 role 속성 추가 및 타입 단언 사용

// 컴포넌트에 role 추가
<form role="form" onSubmit={handleSubmit}>

// 테스트 코드
const form = screen.getByRole("form") as HTMLFormElement;
fireEvent.submit(form);

 

 

 

 

5. 최종적으로 구현한 테스트 코드 예시

SignIn.tsx 컴포넌트 구현

import { render, screen, fireEvent } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import SignIn from "../pages/auth/SignIn";

const renderWithRouter = (component: React.ReactElement) => {
  return render(<BrowserRouter>{component}</BrowserRouter>);
};

describe("SignIn Component", () => {
  test("기본 UI 요소들이 렌더링되는지 테스트", () => {
    renderWithRouter(<SignIn />);

    // 버튼을 텍스트로 찾기
    const loginButton = screen.getByRole("button", { name: "로그인" });
    expect(loginButton).toBeTruthy();
    expect(loginButton.textContent).toBe("로그인");

    // input 필드들 확인
    expect(screen.getByLabelText("아이디")).toBeTruthy();
    expect(screen.getByLabelText("비밀번호")).toBeTruthy();
  });

  test("입력 필드에 값을 입력할 수 있는지 테스트", () => {
    renderWithRouter(<SignIn />);

    const idInput = screen.getByLabelText("아이디") as HTMLInputElement;
    const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;

    // 값 입력
    fireEvent.change(idInput, { target: { value: "testuser" } });
    fireEvent.change(passwordInput, { target: { value: "password123" } });

    // 값 확인
    expect(idInput.value).toBe("testuser");
    expect(passwordInput.value).toBe("password123");
  });
});

 

 

SignUp.tsx 컴포넌트 구현

import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import SignUp from "../pages/auth/SignUp";

const renderWithRouter = (component: React.ReactElement) => {
  return render(<BrowserRouter>{component}</BrowserRouter>);
};

describe("SignUp Component", () => {
  test("유효성 검사가 제대로 작동하는지 테스트", async () => {
    renderWithRouter(<SignUp />);

    const idInput = screen.getByLabelText("아이디") as HTMLInputElement;
    const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
    const nicknameInput = screen.getByLabelText("닉네임") as HTMLInputElement;
    const form = screen.getByRole("form") as HTMLFormElement;

    // 1. 아이디 유효성 검사
    fireEvent.change(idInput, { target: { value: "abc" } });
    fireEvent.submit(form);

    await waitFor(() => {
      const error = screen.getByText(/아이디는 4~20자 사이여야 합니다/);
      expect(error).toBeTruthy();
    });

    // 2. 비밀번호 유효성 검사
    fireEvent.change(idInput, { target: { value: "testuser" } });
    fireEvent.change(passwordInput, { target: { value: "test" } });
    fireEvent.submit(form);

    await waitFor(() => {
      const error = screen.getByText(/비밀번호는 8~12자 사이여야 하며/);
      expect(error).toBeTruthy();
    });

    // 3. 닉네임 유효성 검사
    fireEvent.change(passwordInput, { target: { value: "Test123!@" } });
    fireEvent.change(nicknameInput, { target: { value: "a" } });
    fireEvent.submit(form);

    await waitFor(() => {
      const error = screen.getByText(/닉네임은 2~10자 사이여야 합니다/);
      expect(error).toBeTruthy();
    });
  });
});

 

 

 

 

 

6. 최종 테스트 결과

PASS  src/__tests__/SignIn.test.tsx
 PASS  src/__tests__/SignUp.test.tsx
Test Suites: 2 passed, 2 total
Tests:       3 passed, 3 total

 

 

 

 

 

7. 결과 및 배운 점

  1. JestTypeScript를 함께 사용할 때는 타입 시스템의 차이를 이해하고 적절한 matcher를 선택해야 합니다.
  2. 비동기 테스트에서는 waitFor를 사용하여 안정적인 테스트를 작성할 수 있습니다.
  3. 컴포넌트 테스트는 사용자 관점에서 중요한 기능을 위주로 작성하는 것이 효과적입니다.
  4. 테스트 코드 작성을 통한 더 나은 컴포넌트 설계 방법 학습하였습니다.

 유닛 테스트는 코드의 품질을 보장하고 버그를 미리 발견할 수 있게 해줍니다. 특히 폼 검증이나 사용자 입력 처리와 같은 중요한 기능들을 자동으로 테스트 할 수 있어 매우 유용했습니다.

 이런 경험을 통해 테스트 코드 작성의 중요성을 실감했으며, 앞으로도 꾸준히 테스트 코드를 작성하여 프로젝트의 안정성을 높여갈 계획입니다!!