기타

프론트엔드 테스트 자동화란? 유닛·통합·E2E 테스트 한 번에 정리

마뇨기 2025. 12. 23. 15:56

 

서비스가 돌아가고 있는 와중에 예기치 못한 버그가 발견된다면 개발자들은 식은땀을 흘리면서 컴퓨터 앞에 앉는다.

버그는 유저 이탈의 충분한 계기가 될 수 있기 때문에 버그가 일어나지 않도록 사전에 방지하는 것이 중요하다.

 

그래서 서비스를 출시하거나 새로운 기능을 도입 혹은 수정하면 QA를 진행한다. 하지만, QA는 사람이 진행하기 때문에 놓치는 부분이 있을 수 있다. QA를 진행하기 전에 우리는 테스트 자동화, 즉 테스트 코드를 작성함으로써 사전에 버그가 발생하는 것을 예방할 수 있다.

 

전 직장에서는 아쉽게도 테스트 자동화를 다루지 못했다. 다만 당시에는 적용하지 못했어도, 꾸준히 관심을 갖고 지켜보던 주제 중 하나가 테스트 자동화였다. 

 

이 글에서는 프론트엔드에서 다루는 테스트 자동화에 대해 알아본 내용을 정리하고자 한다.


 


 

 프론트엔드 테스트 자동화란? 

 

 

프론트엔드에서 테스트 자동화란,
사용자가 화면에서 수행하는 행동을 사람이 직접 확인하는 대신 테스트 코드로 UI와 기능이 의도대로 동작하는지를 검증하는 과정을 의미한다.

 

결과적으로 코드 변경 이후에도 새 기능과 기존 기능이 정상 동작하는지 빠르게 확인할 수 있다.


 

 

 프론트엔드에서 테스트 자동화가 필요한 이유 

 

 

프론트엔드는 배포가 잦고 UI 변경도 빈번하다 보니, 테스트 코드를 작성하는 일이 오히려 유지보수 비용만 늘리는 것처럼 느껴질 수도 있다.

그럼에도 프론트엔드에서 테스트 자동화가 필요한 이유는 다음과 같다.

 

첫째, 사람이 직접 하는 QA만으로는 놓치는 항목이 생길 수 있다.

프론트엔드는 화면 분기와 예외 케이스가 많아, 사람이 직접 확인하는 과정에서 일부를 놓치기 쉽다.

이때 테스트 코드는 또 하나의 방어 장치가 된다. 사람이 놓칠 수 있는 부분을 코드로 반복 검증하게 해 주고 프로젝트의 신뢰성을 높여준다. 즉, 테스트 코드를 작성해두면 버그가 배포되기 전에 발견하고 수정할 가능성을 높일 수 있다.

 

 

둘째, 피드백 사이클이 빨라진다.

피드백 사이클이란 수정이 일어난 후, 의도대로 동작하는지 확인하고(피드백을 받고) 다시 수정하기까지 걸리는 한 바퀴의 시간을 의미한다.

 

프론트엔드에서는

 

코드 수정(변경 사항 반영) → 시나리오 재현(클릭/입력) → 문제 발견 → 수정

 

이런 흐름으로 피드백 사이클이 돌아간다.

 

특히 입력과 검증이 많은 화면일수록, 이 과정을 반복하는 데 드는 시간이 크게 늘어난다.
예를 들어 사용자에게 많은 정보를 입력받는 form은 케이스가 정말 많다.

  • 빈 값, 형식 오류, 길이 제한
  • 비밀번호 불일치
  • 특정 조건에서만 버튼 활성화

등등…

 

테스트 코드를 작성해 두면, 원하는 동작을 코드로 바로 확인할 수 있고, 여러 케이스를 한 번에 빠르게 검증할 수 있다.

그래서 form처럼 예외가 많은 영역에서 특히 피드백 사이클을 크게 줄일 수 있다는 장점이 있다.

 

 

셋째, 핵심 로직 변경에 따른 리스크를 줄일 수 있다

특히 비즈니스 로직(핵심 로직)은 여러 화면과 기능에서 공유되는 경우가 많아, 한 곳을 수정하면 다른 곳의 동작까지 함께 영향을 받을 수 있다. 이때 테스트는 동작은 항상 유지되어야 한다는 기준을 코드로 정해두기 때문에, 변경으로 인한 버그를 배포 전에 발견할 가능성을 높여준다.

 

그리고 이 부분이 중요한 이유는, 비즈니스 로직 버그가 운영에서 발견되면 단순히 수정으로 끝나지 않고 고객 응대, 데이터 복구, 환불 등 비용이 크게 증가하기 때문이다. 그래서 문제가 터진 뒤에 수습하는 것보다, 초기에 막는 것이 비용적으로 가장 저렴하다는 관점에서도 테스트 자동화는 큰 가치가 있다.



 

 테스트 유형은 어떤 것들이 있을까? 

 

 

이미지처럼 테스트가 커버하는 범위에 따라 테스트 유형은 크게 세 가지로 나눌 수 있다.

유닛 테스트일수록 작고 빠른 테스트이고, E2E 테스트일수록 범위가 넓고 실제 사용자 흐름에 가까운 테스트이다.

 

유닛 테스트

먼저 유닛 테스트부터 설명하지면, 유닛이라는 말 그대로 가장 작은 단위인 ‘유닛’혹은 ‘모듈’을 대상으로 하는 테스트로, 각 유닛이 예상대로 동작하는지 검증한다.

예를 들어 로그인 기능이 있다고 했을 때, 이메일 형식 검증 로직이나 비밀번호 길이 체크처럼 특정 함수나 로직이 예상한 값을 반환하는지를 검증할 수 있다.

이처럼 유닛 테스트는 개별 함수나 모듈처럼 가장 작은 단위의 동작을 검증하는 데 사용된다.

 

특히 외부 요소(API, DB, 화면 UI 등)에 의존하지 않고 로직 자체만 빠르게 확인할 수 있어, 변경이 생겼을 때 의도치 않은 결과 변화를 초기에 발견하고 수정하는 데 도움이 된다

 

 

통합 테스트

통합 테스트는 유닛 테스트에서 검증했던 요소들이 하나로 묶였을 때도 잘 동작하는지를 보는 테스트이다.

여기서 중요한 포인트는, 통합 테스트는 보통 특정 컴포넌트나 ‘하나의 화면(기능) 단위’ 안에서 상호작용을 검증한다는 점이다.

즉, Button이나 input 하나만 따로 보는 게 아니라, 한 화면 안에서 사용자 행동이 들어왔을 때 요소들이 서로 상호작용하면서 원하는 결과가 나오는지를 확인하는 테스트라고 보면 된다.

 

로그인 예시로 설명하면,

이메일과 비밀번호를 입력했을 때 폼 상태가 정상적으로 반영되는지, 잘못된 정보로 로그인을 시도했을 때 에러 메시지가 화면에 제대로 표시되는지 등을 통해 Input과 Button이 함께 동작하는 해당 화면의 흐름이 의도대로 진행되는지를 확인한다.

 

 

E2E 테스트

마지막으로 E2E 테스트는 사용자가 실제로 사용하는 것처럼 처음부터 끝까지의 흐름을 그대로 재현하는 방식이다.

통합 테스트가 한 화면(기능) 단위의 상호작용을 검증한다면, E2E 테스트는 여러 페이지 이동과 실제 라우팅까지 포함해서 사용자의 전체 시나리오를 검증한다.

 

예를 들어, 사용자에게 많은 정보를 입력받는 form은 다음과 같은 다양한 예외 케이스를 고려해야 한다.

  • 회원가입 페이지 접속 → 정보 입력 → 가입 버튼 클릭 → 완료 후 다음 화면으로 이동 확인

또는

  • 로그인 페이지 접속 → 이메일/비밀번호 입력 → 로그인 버튼 클릭 → 메인 페이지로 이동 확인

같은 흐름을 브라우저 환경에서 그대로 재현한다.

다시 말해, 사용자의 행동을 따라가며 서비스가 실제로 정상 동작하는지 확인하는 테스트라고 보면 된다.



 

 어떤 테스트를 사용해야 하는가? 

 

 

그럼 여기서 질문이 생길 수 있다. “그래서 결론적으로, 어떤 테스트를 해야 하는가?”

 

여러 기업의 기술 블로그를 참고하였을 때, 상황에 따라 다르게 사용해야 한다는 답에 도달했다.

테스트는 정해진 공식처럼 하나를 고르는 게 아니라, 상황에 따라, 또는 팀이 선호하는 방식에 따라 달라진다.

 

예를 들어, 폼이 많고 검증 로직이 복잡한 서비스라면 단위/통합 테스트가 특히 효과적이고, 결제나 로그인처럼 문제가 생기면 서비스 장애나 사용자 이탈로 이어질 수 있는 핵심 흐름이 있다면, E2E로 최소한의 안전망을 두는 게 도움이 된다.

토스의 경우 인터뷰에서는 통합 테스트를 주로 다룬다고 언급된 반면, 기술 블로그에서는 모든 테스트 유형을 활용하여 문제를 해결하고 있다고 한다.

 

그래서 중요한 건 “어떤 테스트가 더 좋은가”가 아니라, 현재 상황과 리스크에 맞는 테스트를 선택하여 적용하는 것이 좋지 않을까 싶다.



 

 그럼 어떤 테스트 프레임워크를 사용하면 될까? 

 

 

테스트 유형이 다르듯, 프론트엔드 테스트 자동화를 위한 테스트 프레임워크 역시 목적에 따라 나뉜다.
중요한 것은 특정 프레임워크가 더 좋다기보다는, 어떤 테스트를 하고 싶은지에 따라 적절한 프레임워크를 선택하는 것이다.

프론트엔드에서 주로 사용되는 테스트 프레임워크는 다음과 같다.

 

 

Jest

Jest는 프론트엔드에서 가장 널리 사용되는 테스트 프레임워크 중 하나로, 주로 유닛 테스트와 간단한 통합 테스트에 사용된다.
개별 함수나 로직 단위를 빠르게 검증하는 데 적합하며, 실행 속도가 빠르고 설정이 비교적 단순하다는 장점이 있다.

특히 비즈니스 로직이나 상태 계산처럼 UI와 직접적인 의존성이 없는 부분을 테스트할 때 효과적이다.
코드를 수정한 뒤, 의도한 결과가 유지되는지를 빠르게 확인할 수 있어 변경에 대한 안정감을 제공한다.

 

Cypress

Cypress는 실제 브라우저 환경에서 E2E 테스트를 시각적이고 직관적으로 수행하게 해주는 프레임워크이다.
페이지 이동, 입력, 클릭 등 사용자의 행동을 그대로 재현하며, 서비스의 전체 흐름이 정상적으로 이어지는지를 검증한다.

특히, 테스트 실행 과정을 GUI로 시각화하여 디버깅이 매우 편리하고, 실제 사용자 경험을 테스트할 수 있다는 장점이 있다.

로그인, 회원가입, 결제처럼 한 번 깨지면 바로 장애로 이어질 수 있는 시나리오에 대해 최소한의 안전망을 마련하는 데 적합하다.

 

정리하면, Jest는 빠른 실행과 강력한 Mocking에 유리하고, Cypress는 실제 사용자처럼 동작하며 디버깅이 편리한 시각적 환경을 제공하는 차이가 있다.

 

테스트 프레임워크 선택에 정답은 없다

이처럼 각 테스트 프레임워크는 잘하는 영역이 다르다.
그래서 실제 현업에서는 하나의 프레임워크만 사용하는 경우보다는, 테스트 목적과 범위에 따라 각각 사용하는 프레임워크가 다르다고 한다.



 

 테스트 코드는 어떻게 작성할까?

 

 

마지막으로, 프론트엔드에서 테스트 코드를 어떻게 작성하는지를 간단히 살펴보고 글을 마무리해보겠다.

 

테스트 코드는 복잡한 설정이나 거창한 구조에서 시작하지 않아도 된다.
핵심은 "기능이 어떤 상황에서 어떻게 동작해야 하는가"를 코드로 명확히 적어두는 것이다.
프론트엔드 테스트 역시 사용자의 행동이나 로직의 결과를 기준으로 작성된다.

 

Jest 예시 (유닛 테스트)

Jest를 사용한 유닛 테스트는 보통 특정 함수나 로직의 결과를 검증하는 형태로 작성된다.

// sum.ts
export const sum = (a: number, b: number) => a + b;
// sum.test.ts
import { sum } from "./sum";

test("두 숫자를 더하면 합이 반환된다", () => {
  expect(sum(1, 2)).toBe(3);
});

 

여기서 sum.test.ts는 보통 테스트 코드를 작성하는 파일이며, 관례적으로 *.test.ts 또는 *.spec.ts 같은 이름을 사용한다. Jest는 이런 파일을 찾아 실행하면서, 작성된 테스트가 통과하는지 확인한다.

 

그리고 예시에서 사용된 함수들은 각각 다음 의미를 가진다.

  • test()
    하나의 테스트 케이스를 정의하는 함수이다.
    첫 번째 인자로 테스트 설명(문장)을 받고, 두 번째 인자로 실제 검증 로직(함수)을 받는다.
  • expect()
    테스트에서 검증하고 싶은 값(결과)을 감싸는 함수이다.
    값이 기대한 결과가 맞는지를 검사하기 위한 출발점이라고 보면 된다.
  • toBe()
    Jest의 매처(matcher) 중 하나로, expect()로 감싼 값이 기대값과 같은지를 비교한다.
    위 예시에서는 sum(1, 2)의 결과가 3인지 확인한다.

즉, 위 코드는
"1과 2를 더했을 때 결과가 3이어야 한다"는 규칙을 테스트 코드로 고정해두는 형태라고 보면 된다.

 

 

Cypress 예시 (E2E 테스트)

Cypress는 실제 사용자처럼 화면을 열고, 입력하고, 클릭하고, 이동하는 흐름을 브라우저 환경에서 그대로 재현하는 방식으로 테스트를 작성한다.

// login.cy.ts
it("로그인 후 메인 페이지로 이동한다", () => {
  cy.visit("/login");
  cy.get("input[name=email]").type("test@test.com");
  cy.get("input[name=password]").type("1234");
  cy.get("button[type=submit]").click();

  cy.url().should("include", "/main");
});

 

여기서 login.cy.ts는 Cypress에서 사용하는 E2E 테스트 파일이다.
보통 *.cy.ts(또는 *.spec.ts)처럼 파일명을 짓고, Cypress가 이 파일들을 실행하면서 브라우저를 띄워 테스트 시나리오를 검증한다.

 

그리고 예시에서 사용된 함수들은 각각 다음 의미를 가진다.

  • it()
    하나의 테스트 케이스를 정의하는 함수이다.
    "로그인 후 메인 페이지로 이동한다"처럼 사람이 읽을 수 있는 설명을 적고, 그 안에 사용자 행동을 순서대로 작성한다.
  • cy
    Cypress가 제공하는 전역 객체로, 브라우저에서 수행할 행동(visit, click, type 등)검증(assertion) 을 연결해서 작성할 수 있게 해준다.
  • cy.visit()
    특정 URL(경로)로 이동한다.
    예시에서는 /login 페이지에 접속하는 단계다.
  • cy.get()
    화면에서 특정 요소를 선택(selector로 찾기)하는 함수이다.
    찾은 요소에 이어서 .type(), .click() 같은 동작을 연결할 수 있다.
  • .type()
    선택된 input 요소에 텍스트를 입력한다.
  • .click()
    선택된 요소(버튼 등)를 클릭한다.
  • cy.url()
    현재 브라우저의 URL을 가져온다. 보통 페이지 이동이 잘 되었는지를 확인할 때 사용한다.
  • .should()
    Cypress의 검증(assertion) 메서드로, 특정 조건이 만족되는지 확인한다.
    예시에서는 URL에 /main이 포함되어 있는지 검사하며, 결과적으로 “로그인 이후 메인 페이지로 이동했는가?”를 검증하는 역할을 한다.

즉, 위 코드는
"사용자가 로그인 화면에 들어가서 정보를 입력하고 로그인 버튼을 누르면, 메인 페이지로 이동해야 한다"라는 흐름을 실제 사용자 행동처럼 재현해 확인하는 테스트라고 보면 된다.


마무리

 

프론트엔드 테스트 자동화를 처음 접했을 때는 유닛·통합 테스트처럼 테스트 유형이 어떻게 구분되는지 선뜻 감이 오지 않았다. 그러던 중 인프런에서 제이쓴님의 ‘2시간으로 끝내는 프론트엔드 테스트 기본기’라는 짧고 알찬 강의를 들으며 테스트에 대한 이해가 한층 정리되었다.

 

테스트 자동화는 모든 버그를 막아주는 마법은 아니지만, 프론트엔드 개발에서 불확실성을 줄여주는 현실적인 방법 중 하나라고 생각한다. 전 직장에서는 테스트 코드를 작성하지 않아, 운영 중 발생한 버그를 대응할 때 아쉬움이 남았다. 곧 새로운 프로젝트도 시작하는 만큼, 이번에는 테스트 코드를 적극적으로 작성하며 프론트엔드 테스트 자동화에 대한 이해와 역량을 더 키워가고 싶다.

 

 

 

참고한 자료: