네...? 이걸 다 수동 테스트하라구요...? 제가요...?

송연지·2026년 7월 25일
post-thumbnail

프론트엔드 프로젝트를 진행하다 보면 처음에는 대부분 직접 화면을 눌러보면서 기능을 확인하게 됩니다.

버튼을 클릭해보고, 검색어를 입력해보고, 목록이 잘 나오는지 보고, 상태가 바뀌는지도 직접 확인합니다.

처음에는 이 방식이 제일 빠르게 느껴집니다.
눈으로 바로 확인할 수 있고, 문제가 있으면 바로 고치면 되니까요.

그런데 기능이 많아지고 화면이 늘어나면서 슬슬 이런 생각이 들기 시작했습니다.

“이거 수정했는데 다른 화면은 안 깨졌을까?”
“버튼 클릭했을 때 API 호출이 제대로 나가고 있을까?”
“실패 상황에서도 화면이 멈추지는 않을까?”
“매번 직접 다 눌러보는 게 맞을까?”

처음에는 제가 사람이 테스트 로봇이 된 줄 알았습니다.
수정하고, 실행하고, 클릭하고, 검색하고, 다시 수정하고, 또 클릭하고.

버튼 하나 고쳤을 뿐인데 확인해야 할 화면이 줄줄이 따라오는 순간, 머릿속에 자연스럽게 한 문장이 떠올랐습니다.

“네...? 이걸 다 수동 테스트하라구요...? 제가요...?”

그래서 이번에는 프론트엔드 단위 테스트 코드를 작성하면서, 어떤 테스트 도구를 사용할지 먼저 비교해보고 프로젝트에 맞는 도구를 선택해보았습니다.


프론트엔드 단위 테스트 도구에는 어떤 것들이 있을까?

프론트엔드에서 단위 테스트를 작성할 때 자주 언급되는 도구는 크게 다음과 같습니다.

- Jest
- Vitest
- Mocha
- Jasmine
- Testing Library

여기서 Jest, Vitest, Mocha, Jasmine은 테스트를 실행하고 검증하는 테스트 프레임워크에 가깝습니다.

반면 Testing Library는 테스트 실행 프레임워크라기보다는, React 컴포넌트를 사용자 관점에서 테스트할 수 있도록 도와주는 라이브러리에 가깝습니다.

즉, 역할을 나누면 이렇게 볼 수 있습니다.

테스트 실행/검증 도구:
Jest, Vitest, Mocha, Jasmine

화면 테스트 보조 도구:
Testing Library

테스트 도구를 고를 때는 단순히 “유명한가?”보다 현재 프로젝트 환경과 잘 맞는지를 기준으로 보는 것이 중요하다고 느꼈습니다.


Jest

Jest는 프론트엔드 테스트에서 가장 많이 알려진 테스트 프레임워크 중 하나입니다.

React 프로젝트에서도 오래전부터 많이 사용되었고, 자료가 많고 안정적인 편입니다.

Jest는 테스트 실행, 단언문, Mock 기능, 스냅샷 테스트 등을 한 번에 제공합니다.

describe('계산 함수 테스트', () => {
  it('두 숫자를 더한다', () => {
    expect(1 + 2).toBe(3);
  });
});

Jest의 가장 큰 장점은 레퍼런스가 많다는 점입니다.

이미 많은 프로젝트에서 사용되고 있기 때문에 문제가 생겼을 때 검색으로 해결하기 좋고, 여러 환경에서 검증된 도구라는 안정감이 있습니다.

다만 Vite 기반 프로젝트에서는 설정이 다소 번거로울 수 있다고 느꼈습니다.
TypeScript, TSX, alias 설정 등을 맞추다 보면 추가 설정이 필요할 수 있기 때문입니다.

Jest는 이런 경우에 잘 맞는다고 생각했습니다.

- 기존 프로젝트에서 이미 Jest를 사용하고 있는 경우
- 레퍼런스가 많은 도구를 선호하는 경우
- 안정적으로 검증된 테스트 환경이 필요한 경우
- Vite가 아닌 기존 React 환경에서 테스트를 구성하는 경우

Vitest

Vitest는 Vite 생태계에 맞춰 만들어진 테스트 프레임워크입니다.

Vite 기반 프로젝트에서는 설정 흐름이 자연스럽고, TypeScript와 TSX 테스트도 비교적 편하게 작성할 수 있습니다.

문법도 Jest와 매우 비슷합니다.

describe('계산 함수 테스트', () => {
  it('두 숫자를 더한다', () => {
    expect(1 + 2).toBe(3);
  });
});

Mock도 vi.fn(), vi.mock() 형태로 사용할 수 있습니다.

const mockFetchList = vi.fn();

vi.mock('@/api/sampleApi', () => ({
  fetchList: mockFetchList,
}));

Vitest의 가장 큰 장점은 Vite 프로젝트와 잘 맞는다는 점입니다.

Vite 설정, TypeScript, path alias, 빠른 실행 속도 면에서 장점이 있고, Jest와 문법이 비슷해서 테스트 코드를 처음 작성할 때도 진입 장벽이 낮았습니다.

제가 느낀 Vitest의 장점은 다음과 같습니다.

- Vite 프로젝트와 설정이 잘 맞는다
- TypeScript/TSX 테스트 작성이 편하다
- Jest와 문법이 비슷해서 익히기 쉽다
- 실행 속도가 빠른 편이다
- Mock 기능을 사용하기 쉽다

그래서 Vite + React + TypeScript 기반 프로젝트라면 Jest보다 Vitest가 더 자연스럽다고 판단했습니다.


Mocha

Mocha는 오래된 JavaScript 테스트 프레임워크 중 하나입니다.

Jest나 Vitest처럼 필요한 기능이 한 번에 포함되어 있다기보다는, 필요한 라이브러리를 조합해서 사용하는 방식에 가깝습니다.

예를 들어 단언문은 Chai, Mock이나 Spy는 Sinon을 함께 사용하는 경우가 많습니다.

Mocha는 유연성이 높다는 장점이 있습니다.

하지만 처음 테스트 환경을 구성하는 입장에서는 직접 조합해야 할 것이 많아서 조금 복잡하게 느껴질 수 있습니다.

Mocha는 이런 경우에 어울린다고 생각했습니다.

- 테스트 환경을 직접 세밀하게 구성하고 싶은 경우
- 특정 assertion/mock 라이브러리를 조합해서 쓰고 싶은 경우
- 기존 프로젝트에서 이미 Mocha 기반 테스트 환경을 사용하고 있는 경우

저는 이번 프로젝트에서는 빠르게 단위 테스트 환경을 구성하고 싶었기 때문에 Mocha보다는 Vitest가 더 적합하다고 판단했습니다.


Jasmine

Jasmine은 오래된 JavaScript 테스트 프레임워크입니다.

Jest와 비슷하게 describe, it, expect 같은 구조를 사용합니다.

describe('계산 함수 테스트', () => {
  it('두 숫자를 더한다', () => {
    expect(1 + 2).toBe(3);
  });
});

Jasmine은 자체적으로 assertion, spy 기능을 제공하는 편이라 별도 라이브러리 없이도 테스트를 작성할 수 있습니다.

다만 최근 React + Vite 기반 프론트엔드 프로젝트에서는 Jest나 Vitest에 비해 선택 빈도가 낮다고 느꼈습니다.

기존 Angular 프로젝트나 오래된 테스트 환경에서는 볼 수 있지만, 새로 구성하는 React 프로젝트라면 Vitest나 Jest를 먼저 고려하는 것이 더 자연스럽다고 생각했습니다.


Testing Library

Testing Library는 테스트 프레임워크라기보다는 React 컴포넌트를 테스트하기 위한 도구입니다.

Vitest나 Jest와 함께 사용합니다.

예를 들어 Vitest가 테스트를 실행하고 expect로 결과를 검증한다면, Testing Library는 컴포넌트를 렌더링하고 화면 요소를 찾고 사용자 동작을 재현하는 역할을 합니다.

render(<SampleButton onClick={handleClick} />);

fireEvent.click(screen.getByText('저장'));

expect(handleClick).toHaveBeenCalled();

Testing Library의 핵심은 사용자 관점의 테스트입니다.

내부 함수명이나 컴포넌트의 구현 방식보다, 실제 화면에 보이는 텍스트, 버튼, 입력창을 기준으로 테스트합니다.

예를 들어 “저장 버튼을 클릭하면 저장 요청이 호출된다”처럼 작성할 수 있습니다.

이 방식이 좋은 이유는 내부 구현이 조금 바뀌어도 사용자가 보는 화면과 동작이 같다면 테스트가 크게 흔들리지 않기 때문입니다.


도구별 특징 정리

각 도구의 특징을 간단히 정리하면 다음과 같습니다.

도구역할특징
Jest테스트 프레임워크자료가 많고 안정적이며 React 테스트에서 오래 사용됨
Vitest테스트 프레임워크Vite 기반 프로젝트와 잘 맞고 빠르며 Jest와 문법이 비슷함
Mocha테스트 프레임워크유연하지만 필요한 도구를 직접 조합하는 경우가 많음
Jasmine테스트 프레임워크자체 assertion과 spy 기능이 있지만 최근 React 프로젝트에서는 상대적으로 덜 사용됨
Testing Library컴포넌트 테스트 도구사용자 관점으로 React 컴포넌트를 테스트하기 좋음

제가 이해한 기준으로는 이렇게 정리할 수 있었습니다.

Vite + React + TypeScript 신규 프로젝트
→ Vitest + Testing Library 조합이 자연스러움

기존 React 프로젝트 또는 레퍼런스 많은 환경
→ Jest + Testing Library 조합이 안정적

테스트 환경을 직접 조합하고 싶은 경우
→ Mocha + Chai + Sinon 조합 가능

기존 Angular 또는 오래된 JS 테스트 환경
→ Jasmine을 사용하는 경우도 있음

그래서 저는 왜 Vitest를 선택했나

제가 Vitest를 선택한 가장 큰 이유는 프로젝트 환경 때문입니다.

현재 프로젝트가 Vite + React + TypeScript 기반이었기 때문에, 같은 생태계에 있는 Vitest가 가장 자연스럽다고 판단했습니다.

Jest도 충분히 좋은 도구이지만, Vite 프로젝트에서 Jest를 사용하려면 추가 설정을 맞춰야 할 수 있습니다.

반면 Vitest는 Vite 설정과 잘 맞고, TypeScript/TSX 테스트 작성도 비교적 수월했습니다.

또한 Jest와 문법이 비슷해서 테스트 코드 작성 방식이 낯설지 않았습니다.

describe('목록 페이지', () => {
  it('진입 시 목록을 조회한다', async () => {
    render(<SampleListPage />);

    await waitFor(() => {
      expect(mockFetchList).toHaveBeenCalled();
    });
  });
});

즉, 제가 Vitest를 선택한 기준은 다음과 같습니다.

1. 프로젝트가 Vite 기반이었다
2. React + TypeScript 테스트를 작성해야 했다
3. Jest와 비슷한 문법이라 학습 부담이 적었다
4. 실행 속도가 빠른 편이라 반복 테스트에 적합했다
5. Mock을 사용해 API와 외부 의존성을 분리하기 쉬웠다

테스트 도구를 고르는 과정에서 가장 크게 느낀 점은 “좋은 도구”보다 “현재 프로젝트에 덜 어색한 도구”가 중요하다는 점이었습니다.

아무리 유명한 도구라도 프로젝트 설정과 계속 충돌하면 테스트 코드를 작성하기 전에 설정에서 지칠 수 있기 때문입니다.


화면 테스트에는 왜 Testing Library를 같이 사용했나

Vitest만으로도 함수 단위 테스트는 작성할 수 있습니다.

하지만 React 컴포넌트나 페이지를 테스트하려면 화면을 렌더링하고, 버튼 클릭이나 입력 같은 사용자 동작을 재현해야 합니다.

그래서 @testing-library/react를 함께 사용했습니다.

render(<SampleListPage />);

fireEvent.change(screen.getByPlaceholderText('검색어를 입력하세요'), {
  target: { value: '검색어' },
});

Testing Library를 사용하면 테스트를 실제 사용자 흐름에 가깝게 작성할 수 있습니다.

예를 들어 다음과 같은 테스트가 가능합니다.

- 화면에 특정 문구가 보이는지
- 버튼을 클릭했을 때 함수가 호출되는지
- 입력창에 값을 입력했을 때 검색 요청이 발생하는지
- 상태 변경 스위치를 눌렀을 때 API가 호출되는지
- API 실패 시에도 화면 흐름이 유지되는지

저는 단순히 내부 함수가 호출됐는지만 보는 테스트보다, 사용자가 실제로 화면에서 하는 행동을 기준으로 테스트하는 방식이 더 적합하다고 느꼈습니다.


제가 작성한 테스트 코드 구조

테스트 파일은 실제 기능 구조와 비슷하게 나눴습니다.

회사 내부 구조가 드러나지 않도록 예시로 표현하면 다음과 같습니다.

tests/
└─ unit/
   ├─ pages/
   │  └─ 화면 단위 테스트
   ├─ components/
   │  └─ 공통 컴포넌트 테스트
   ├─ utils/
   │  └─ 유틸 함수 테스트
   └─ modules/
      └─ 주요 로직 테스트

이렇게 나누면 테스트 파일을 찾기 쉽습니다.

페이지 관련 테스트는 pages,
공통 컴포넌트 테스트는 components,
순수 함수 테스트는 utils,
특정 기능 로직 테스트는 modules처럼 구분할 수 있습니다.

테스트 파일 위치가 정리되어 있으면 나중에 기능을 수정할 때 관련 테스트를 함께 찾기 편했습니다.


테스트 파일 위쪽에는 설명을 적었습니다

각 테스트 파일 위에는 이 테스트가 무엇을 검증하는지 간단히 적었습니다.

/**
 * 테스트 대상
 *
 * 검증 항목:
 * 1. 최초 진입 시 목록을 조회한다
 * 2. 검색 기능이 동작한다
 * 3. 상태 변경 요청을 호출한다
 * 4. 실패 상황에서도 목록을 다시 조회한다
 */

이 설명은 테스트 코드의 목차 역할을 합니다.

테스트 코드가 많아지면 어떤 테스트가 어떤 기능을 보장하는지 한눈에 보기 어려워집니다.

그래서 상단에 검증 항목을 적어두면, 나중에 테스트를 추가하거나 수정할 때 기준을 잡기 쉬웠습니다.

테스트 코드가 단순히 실행되는 코드가 아니라, 해당 기능이 어떤 동작을 보장해야 하는지 보여주는 문서처럼 느껴졌습니다.


기본 테스트 구조

제가 작성한 테스트는 대체로 다음 구조를 사용했습니다.

describe('목록 관리 페이지', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('진입 시 목록을 조회한다', async () => {
    render(<SampleListPage />);

    await waitFor(() => {
      expect(mockFetchList).toHaveBeenCalled();
    });
  });
});

각 코드의 역할은 다음과 같습니다.

코드역할
describe관련 테스트를 하나의 그룹으로 묶습니다.
it하나의 동작이나 요구사항을 검증합니다.
beforeEach각 테스트 실행 전 Mock 상태를 초기화합니다.
vi.clearAllMocks()이전 테스트의 Mock 호출 기록을 초기화합니다.
renderReact 컴포넌트를 테스트 환경에 렌더링합니다.
fireEvent클릭, 입력 같은 사용자 동작을 재현합니다.
waitFor비동기 작업이 끝날 때까지 기다립니다.
expect기대한 결과와 실제 결과를 비교합니다.

이 구조를 반복해서 사용하니 테스트 파일의 형태가 어느 정도 통일됐습니다.

테스트마다 작성 방식이 다르면 읽기 어려운데, 구조를 맞춰두면 나중에 유지보수하기 훨씬 편했습니다.


API는 Mock으로 처리했습니다

테스트에서는 실제 API를 호출하지 않고 Mock으로 대체했습니다.

const mockFetchList = vi.fn();
const mockUpdateStatus = vi.fn();

vi.mock('@/api/sampleApi', () => ({
  fetchList: mockFetchList,
  updateStatus: mockUpdateStatus,
}));

이렇게 하면 외부 환경과 관계없이 원하는 상황을 만들 수 있습니다.

목록 조회 성공 상황은 이렇게 만들 수 있습니다.

mockFetchList.mockResolvedValue([
  {
    id: 1,
    name: 'Sample Item',
    enabled: true,
  },
]);

상태 변경 실패 상황은 이렇게 만들 수 있습니다.

mockUpdateStatus.mockRejectedValue(new Error('실패'));

Mock을 사용한 이유는 실제 서버나 외부 환경에 의존하지 않고, 화면 로직만 독립적으로 검증하기 위해서입니다.

성공 응답, 실패 응답, 빈 목록, 특정 상태값 등 다양한 상황을 직접 만들 수 있기 때문에 테스트 흐름을 통제하기 좋았습니다.


오히려 에러 케이스가 더 중요하다고 느꼈습니다

테스트 코드를 작성하면서 가장 신경 쓴 부분 중 하나는 실패 케이스였습니다.

처음에는 자연스럽게 성공 케이스부터 작성했습니다.

- 페이지에 진입하면 목록이 조회되는지
- 검색어를 입력하면 조회 함수가 호출되는지
- 버튼을 클릭하면 상태 변경 API가 호출되는지
- 응답이 성공하면 화면 흐름이 유지되는지

이런 테스트도 물론 중요합니다.

하지만 실제 서비스를 생각해보면 사용자가 문제를 겪는 순간은 대부분 성공 상황이 아니라 실패 상황입니다.

API 요청이 실패하거나, 서버 응답이 늦거나, 상태 변경이 정상 처리되지 않거나, 예상하지 못한 빈 데이터가 내려오는 경우가 있습니다.

개발할 때는 보통 정상 응답을 기준으로 화면을 확인하기 때문에, 실패 상황은 놓치기 쉽습니다.

성공 케이스는 개발자가 직접 확인하기 쉽습니다.
버튼을 누르고 “오 잘 된다” 하고 넘어갈 수 있습니다.

하지만 실패 케이스는 다릅니다.

서버를 일부러 실패시키기도 애매하고, 네트워크 오류를 매번 만들기도 어렵고, 특정 예외 응답을 재현하는 것도 번거롭습니다.

그래서 저는 성공 케이스보다 오히려 에러 케이스를 더 꼼꼼히 확인해야 한다고 느꼈습니다.


실패 케이스 테스트를 작성한 이유

실패 케이스 테스트의 목적은 단순히 오류를 발생시키는 것이 아닙니다.

중요한 건 오류가 발생했을 때 화면이 그대로 멈추거나, 잘못된 상태로 남지 않는지 확인하는 것입니다.

예를 들어 사용자가 상태 변경 버튼을 클릭했는데 API가 실패할 수 있습니다.

이때 화면이 이전 상태와 다르게 표시되거나, 목록이 갱신되지 않거나, 사용자가 현재 상태를 오해하게 되면 문제가 될 수 있습니다.

그래서 실패 상황에서는 다음 흐름을 확인했습니다.

사용자가 버튼 클릭
→ 상태 변경 API 호출
→ API 실패
→ 오류 처리
→ 최신 목록 다시 조회
→ 화면 상태 복구

즉, 실패했을 때도 화면이 다시 안정적인 상태로 돌아오는지를 검증했습니다.


실패 상황은 Mock으로 직접 만들었습니다

실제 테스트에서는 API 실패 상황을 Mock으로 만들었습니다.

mockUpdateStatus.mockRejectedValue(new Error('실패'));

이렇게 하면 실제 서버를 조작하지 않아도, 상태 변경 요청이 실패하는 상황을 테스트 안에서 만들 수 있습니다.

그리고 실패 이후 목록 조회가 다시 실행되는지 확인했습니다.

it('상태 변경 실패 시 목록을 다시 조회한다', async () => {
  mockFetchList.mockResolvedValue([
    {
      id: 1,
      name: 'Sample Item',
      enabled: true,
    },
  ]);

  mockUpdateStatus.mockRejectedValue(new Error('실패'));

  render(<SampleListPage />);

  const toggle = await screen.findByRole('checkbox');

  fireEvent.click(toggle);

  await waitFor(() => {
    expect(mockFetchList).toHaveBeenCalledTimes(2);
  });
});

여기서 mockUpdateStatus는 상태 변경 요청을 의미하고, mockFetchList는 목록 조회 요청을 의미합니다.

테스트의 핵심은 상태 변경 API가 실패했을 때도 목록 조회가 다시 실행되는지 확인하는 것입니다.

최초 진입 시 목록 조회 1회
상태 변경 실패 후 목록 재조회 1회
총 2회 호출

그래서 mockFetchList가 2번 호출되었는지 검증했습니다.

이 테스트는 “에러가 나면 안 된다”가 아니라, “에러가 나도 화면이 안전하게 버텨야 한다”에 가깝습니다.


에러 로그가 보여도 테스트가 통과할 수 있습니다

실패 케이스 테스트를 작성하면 테스트 실행 중 콘솔에 에러 로그가 보일 수 있습니다.

처음에는 이 로그를 보고 “테스트가 잘못된 건가?”라고 생각할 수 있습니다.

하지만 의도적으로 실패 상황을 만든 테스트라면, 에러 로그가 출력되는 것 자체는 이상한 일이 아닙니다.

중요한 건 에러가 발생한 뒤에 프로그램이 기대한 흐름대로 복구되는지입니다.

에러가 발생한다
→ catch에서 처리된다
→ 필요한 후속 동작이 실행된다
→ 테스트가 통과한다

즉, 실패 케이스 테스트에서는 “에러가 안 나는지”보다 “에러가 났을 때도 안전하게 처리되는지”를 확인했습니다.

이 부분이 테스트를 작성하면서 꽤 중요하게 느껴졌습니다.

사용자 입장에서는 내부적으로 어떤 에러가 났는지보다, 화면이 이상한 상태로 남지 않는지가 더 중요하기 때문입니다.


성공 케이스보다 실패 케이스가 더 값질 때도 있습니다

성공 케이스는 개발 중에 직접 확인하기 쉽습니다.

화면을 열고 버튼을 눌러보면 정상 동작 여부를 어느 정도 확인할 수 있습니다.

하지만 실패 케이스는 매번 직접 확인하기 어렵습니다.

서버 오류를 일부러 만들기도 어렵고, 네트워크 실패나 예외 응답을 매번 재현하기도 번거롭습니다.

그래서 실패 상황일수록 테스트 코드로 만들어두는 것이 더 의미 있다고 느꼈습니다.

성공 케이스:
직접 확인하기 비교적 쉬움

실패 케이스:
직접 재현하기 어렵고 놓치기 쉬움
그래서 테스트 코드로 고정해두는 가치가 큼

특히 사용자가 직접 조작하는 기능에서는 실패 처리 테스트가 더 중요하다고 생각했습니다.

사용자 입장에서는 요청이 성공했는지 실패했는지보다, 실패했을 때 화면이 이상한 상태로 남지 않는지가 더 중요하기 때문입니다.


제가 실패 케이스에서 확인한 것들

실패 케이스에서는 단순히 API가 실패하는지만 보지 않았습니다.

실패 이후 화면이 안정적으로 유지되는지를 중심으로 확인했습니다.

- API 실패 시 프로그램이 멈추지 않는지
- 실패 이후 필요한 후속 조회가 실행되는지
- 화면이 이전 상태와 불일치하지 않는지
- 사용자가 잘못된 상태로 오해하지 않도록 복구되는지
- catch 흐름이 실제로 동작하는지

이런 테스트를 작성해두면 나중에 리팩토링을 하거나 API 호출 구조를 바꿀 때도 안정성을 확인할 수 있습니다.

실패 처리는 평소에는 잘 보이지 않지만, 실제 운영 환경에서는 중요하게 드러나는 부분이라고 느꼈습니다.


네...? 이걸 다 수동 테스트하라구요..? 제가요?

테스트 코드를 작성하면서 가장 크게 체감한 부분은 속도였습니다.

기능을 하나 수정할 때마다 확인해야 할 게 생각보다 많았습니다.

화면 실행
→ 메뉴 이동
→ 목록 조회 확인
→ 검색어 입력
→ 버튼 클릭
→ 상태 변경 확인
→ 실패 상황 확인
→ 다른 케이스 반복

처음에는 당연히 직접 눌러보면서 확인했습니다.

그런데 확인해야 할 화면과 케이스가 늘어나니까 어느 순간 이런 생각이 들었습니다.

네...?
이걸 다 수동 테스트하라구요...?
제가요...?

버튼 하나 수정했을 뿐인데, 혹시 다른 기능이 깨졌을까 봐 관련 화면을 다시 들어가 보고, 검색도 해보고, 상태 변경도 눌러보고, 실패 상황도 생각해야 했습니다.

이쯤 되면 개발을 하는 건지, 클릭 노동을 하는 건지 살짝 헷갈리기 시작합니다.

물론 수동 테스트도 필요합니다.
실제 화면에서만 느껴지는 UI 흐름이나 사용성은 직접 확인해야 합니다.

하지만 매번 같은 동작을 반복해서 확인하는 건 너무 비효율적이었습니다.

그래서 반복적으로 확인해야 하는 흐름은 테스트 코드로 옮겼습니다.

수동 확인:
화면마다 직접 이동하고 클릭해야 함
시간이 오래 걸리고 누락 가능성이 있음

테스트 실행:
작성된 케이스를 한 번에 검증
약 50초 안에 주요 흐름 확인 가능

제가 작성한 테스트 기준으로는 수기로 하나씩 확인하면 훨씬 오래 걸릴 작업들이, 테스트 실행으로는 약 50초 안에 끝났습니다.

이때 테스트 자동화가 왜 필요한지 가장 크게 체감했습니다.

테스트 코드를 작성하는 데 처음에는 시간이 들어갑니다.
하지만 한 번 작성해두면 같은 검증을 반복할 때마다 시간을 아낄 수 있습니다.

특히 공통 로직을 수정하거나 리팩토링을 할 때 좋았습니다.

예전 같으면 수정 후에 괜히 불안해서 화면을 하나씩 다시 눌러봤을 텐데, 테스트를 실행하면 최소한 작성해둔 주요 흐름은 빠르게 확인할 수 있었습니다.

목록 조회가 여전히 되는지
검색 기능이 깨지지 않았는지
버튼 클릭 시 요청이 나가는지
실패 상황에서도 화면이 복구되는지

이런 것들을 매번 손으로 확인하지 않아도 된다는 점이 생각보다 크게 느껴졌습니다.

결국 테스트 코드는 단순히 버그를 잡기 위한 코드라기보다, 반복 확인 작업을 줄여주는 자동 점검표처럼 느껴졌습니다.

수동으로 계속 누르다 보면 사람은 지치고, 지치면 놓칩니다.
하지만 테스트는 지치지 않고 같은 흐름을 계속 확인해줍니다.

그게 제가 테스트 코드를 작성하면서 가장 만족했던 부분이었습니다.


테스트 이름은 요구사항처럼 작성했습니다

테스트 이름은 가능하면 코드 설명이 아니라 요구사항처럼 작성하려고 했습니다.

it('진입 시 목록을 조회한다', async () => {});
it('검색어를 입력하면 목록을 다시 조회한다', async () => {});
it('상태 변경 버튼을 클릭하면 변경 요청을 보낸다', async () => {});
it('상태 변경 실패 시 목록을 다시 조회한다', async () => {});

이렇게 작성하면 테스트 목록만 봐도 해당 화면이 어떤 동작을 보장해야 하는지 알 수 있습니다.

테스트 코드가 단순 검증 코드가 아니라, 기능 명세서처럼 읽히는 느낌이 들었습니다.


제가 테스트를 작성하면서 세운 기준

테스트 코드를 작성하면서 다음 기준을 지키려고 했습니다.

1. 한 테스트에서는 하나의 동작만 검증하기
2. 내부 구현보다 사용자 행동 기준으로 작성하기
3. 실제 외부 환경에 의존하지 않기
4. 성공 케이스와 실패 케이스를 함께 보기
5. 특히 실패 케이스는 복구 흐름까지 확인하기
6. 테스트 이름만 봐도 의도를 알 수 있게 작성하기

특히 “한 테스트에서 하나의 동작만 검증하기”가 중요했습니다.

하나의 테스트에 너무 많은 내용을 넣으면, 실패했을 때 어디서 문제가 발생했는지 찾기 어렵습니다.

그래서 목록 조회, 검색, 상태 변경, 실패 처리처럼 동작별로 테스트를 나눠서 작성했습니다.


테스트 코드를 작성하면서 느낀 점

테스트 코드를 처음 작성할 때는 솔직히 조금 번거롭게 느껴졌습니다.

화면에서 직접 한 번 눌러보면 끝날 것 같은데, 그걸 코드로 다시 작성해야 하니까요.

하지만 기능이 많아질수록 테스트 코드의 장점이 보였습니다.

특히 기존 기능을 수정할 때 심리적인 부담이 줄었습니다.

- 이 수정 때문에 목록 조회가 깨지지는 않았는지
- 검색 기능이 여전히 동작하는지
- 버튼 클릭 시 API 요청이 나가는지
- 실패 상황에서도 화면 흐름이 유지되는지

이런 부분을 테스트가 대신 확인해주기 때문입니다.

테스트가 모든 버그를 막아주는 것은 아니지만, 적어도 기존에 보장하던 동작이 깨졌는지 빠르게 확인할 수 있습니다.

그리고 테스트를 작성하면서 느낀 가장 큰 점은, 테스트가 단순히 “성공하는 기능을 확인하는 코드”는 아니라는 것입니다.

오히려 실패했을 때 화면이 어떻게 버티는지 확인하는 코드에 더 가까울 때도 있었습니다.


정리

프론트엔드 단위 테스트 도구에는 Jest, Vitest, Mocha, Jasmine 등이 있고, React 컴포넌트 테스트에는 Testing Library를 함께 사용할 수 있습니다.

그중에서 저는 Vite + React + TypeScript 프로젝트와 가장 잘 맞는 Vitest를 선택했습니다.

Vitest는 Vite 설정과 잘 맞고, TypeScript/TSX 테스트가 편하며, Jest와 문법이 비슷해서 진입 장벽이 낮았습니다.

또한 vi.fn(), vi.mock()을 통해 API나 외부 의존성을 Mock으로 분리하기 쉬웠습니다.

제가 작성한 테스트의 핵심은 다음과 같습니다.

- 화면 진입 시 필요한 데이터가 조회되는지
- 사용자의 클릭, 입력, 검색 동작이 정상적으로 처리되는지
- 상태 변경 같은 주요 기능이 의도한 요청으로 이어지는지
- API 실패 상황에서도 화면이 멈추지 않는지
- 실패 이후 필요한 복구 흐름이 실행되는지
- 외부 환경과 분리해서 화면 로직을 안정적으로 검증할 수 있는지

테스트 코드는 단순히 오류를 찾기 위한 코드만은 아니라고 느꼈습니다.

기능이 어떤 동작을 보장해야 하는지 기록하는 문서이기도 하고, 나중에 코드를 수정할 때 안심할 수 있게 해주는 안전장치이기도 했습니다.

특히 실패 케이스 테스트는 “오류가 발생하지 않는지”를 보는 것이 아니라,
“오류가 발생해도 화면이 안전하게 버티는지”를 확인하는 테스트라고 생각했습니다.

그리고 수기로 하나씩 확인하면 오래 걸리는 주요 흐름도, 테스트 코드로 만들어두면 약 50초 안에 한 번에 점검할 수 있었습니다.

처음부터 모든 테스트를 완벽하게 작성할 필요는 없지만, 자주 수정되는 화면이나 중요한 사용자 흐름부터 하나씩 테스트를 추가해보는 것만으로도 유지보수에 꽤 도움이 된다고 느꼈습니다.

profile
프론트엔드 개발쟈!!

2개의 댓글

comment-user-thumbnail
2026년 7월 26일

도구 비교부터 차근히 정리해주셔서 잘 읽었습니다!
"에러가 나도 화면이 버티는지"라는 관점이 좋네요. 그런 방향이면 토글이 원래 상태로 돌아왔는지 보는 것도 좋을 것 같습니다. 리페치 방식을 바꿔도 안 깨지고요!

1개의 답글