← Blog
테스트

프론트엔드 테스트 코드 전략 (유닛/통합/E2E + 이중 루프 TDD)

한 줄 요약

테스트 코드는 단순 버그 방지를 넘어 개발 생산성과 코드 품질을 높이는 도구이며, 유닛/통합/E2E를 상황에 맞게 조합하고 이중 루프 TDD로 사용자 요구사항과 기술 구현을 함께 검증한다.

왜 알아야 했나

  • 어떤 상황에서 필요했는지

핵심 개념

🟢 테스트 코드의 이점 (Benefit)

영상에서는 단순한 '버그 방지'를 넘어, 개발자의 생산성과 코드의 질을 높여주는 장점들을 깊게 다룹니다.

1. 압도적으로 빨라지는 피드백 사이클 (특히 복잡한 폼/퍼널) 회원가입처럼 여러 단계(퍼널, Funnel)를 거쳐 정보를 입력받는 화면을 상상해 보세요. 코드를 조금 수정할 때마다 브라우저에서 직접 1단계부터 5단계까지 클릭하고 타이핑하며 확인하는 것은 엄청난 시간 낭비입니다. 테스트 코드가 있다면 이 과정을 단 몇 초 만에 자동으로 뚫어주기 때문에 결과적으로 개발 속도가 훨씬 빨라집니다.

2. 언제나 최신 상태인 '살아있는 스펙 문서' 스타트업이나 애자일 조직에서는 기획서나 위키 문서가 제때 업데이트되지 않아 죽은 문서가 되기 일쑤입니다. 하지만 테스트 코드는 거짓말을 하지 않는 실행 가능한 코드입니다. 새로 합류한 동료가 "이 컴포넌트는 어떤 조건일 때 이런 에러를 띄우는구나" 하고 기능을 파악할 수 있는 가장 완벽하고 신뢰할 수 있는 명세서(Spec) 역할을 합니다.

3. 배포의 자신감과 심리적 안정성 기존 코드를 수정하거나 대규모 리팩토링을 할 때 "이거 고쳤다가 다른 곳이 터지면 어떡하지?"라는 두려움을 없애줍니다. 견고한 테스트 코드가 든든한 방어막(Safety Net) 역할을 해주기 때문에, 두려움 없이 과감하게 코드를 개선하고 자신 있게 배포할 수 있습니다.

4. 좋은 설계로 이끄는 강제력 (의존성 분리) "이 컴포넌트는 외부 요인이 너무 많이 얽혀 있어서 테스트를 짜기가 힘드네?"라는 생각이 드는 순간, 개발자는 자연스럽게 코드를 관심사별로 분리(Decoupling)하게 됩니다. 테스트를 작성하려는 노력 자체가 코드의 결합도를 낮추고 유지보수하기 좋은 구조로 만들어 주며, 결국 코드의 수명을 획기적으로 늘려줍니다.

  1. 어드민(B), 사용자용 웹(A) 사이트의 레거시 코드를 새로운 프레임워크로 이식하는 과정에서 테스트 코드가 있었으면 레거시 코드의 기능을 하나하나 확인하지 않더라도 테스트코드를 통해 기능 이식이 됐는가를 빠르게 판단할 수 있었을 것 같음.

🔴 테스트 코드의 단점 및 실무적 한계 (Trade-off)

영상에서도 "테스트 자동화가 무조건 정답은 아니다"라고 말하며 현실적인 고민들을 짚어줍니다.

1. 막대한 초기 작성 및 유지보수 비용 테스트 코드도 결국 '유지보수해야 하는 또 하나의 짐'입니다. 프론트엔드는 기획이나 UI/UX 변경이 매우 잦은 분야인데, 버튼 하나 위치가 바뀌거나 구조가 조금만 바뀌어도 기존에 짜둔 테스트 코드가 줄줄이 실패(Red)하게 됩니다. 이를 다시 고치는 데 실제 기능 개발보다 더 많은 시간이 드는 배보다 배꼽이 큰 상황이 발생할 수 있습니다.

2. 맹목적인 테스트(TDD)의 비효율성 단순히 "모든 곳에 테스트를 작성해야 해!"라는 강박으로 앞서 보여주셨던 단순한 구분선이나 시각적 UI 버튼까지 유닛 테스트로 짜는 것은 명백한 자원 낭비입니다. 들인 시간(비용) 대비 얻어가는 안정성(ROI)이 현저히 떨어집니다. 테스트 종류별 비용과 신뢰도 구조


1. 유닛 테스트 (Unit Test, 단위 테스트)

정의

애플리케이션을 구성하는 가장 작은 모듈(주로 개별 함수, 커스텀 훅)이 독립적으로 의도한 대로 작동하는지 검증하는 테스트입니다.

용도

  • 외부 의존성(DOM, 네트워크, DB 등)을 배제하고, 코드 내부 로직 자체의 정확성을 보장합니다.

  • 다양한 예외 상황(Edge Case)을 빠르게 시뮬레이션하여 버그를 사전에 차단합니다.

  • 코드 리팩토링 시 기존 기능이 망가지지 않았음을 보장하는 안전망 역할을 합니다.

사용법

  • 주요 도구: Jest, Vitest

  • 작성 패턴: describe로 그룹을 묶고, it(또는 test)으로 개별 케이스를 작성한 뒤, expect로 결과값을 단언(Assert)합니다.

  • 특징: 외부 의존성이 필요한 경우 vi.fn(), jest.fn() 등을 활용해 가짜(Mock) 함수로 대체하여 완전히 격리된 환경을 만듭니다. 테스트 파일은 주로 대상 파일 바로 옆에 나란히 위치(Co-location)시킵니다.

🎯 필요한 상황 (적용 타겟)

UI보다는 "데이터를 다루거나 계산하는 순수 자바스크립트 로직"에 집중해야 합니다. 화면이 없어서 눈으로 확인하기 힘든 엣지 케이스들을 테스트하는 데 매우 적합합니다.

  • ✅ 복잡한 계산 로직: 장바구니 상품들의 가격, 할인 쿠폰, 배송비를 종합해서 최종 결제 금액을 계산하는 함수

  • ✅ 데이터 변환 (Formatting): 서버에서 온 2023-10-24T12:00:00Z 형식의 날짜를 23년 10월 24일 오후 12시로 바꿔주는 유틸리티 함수

  • ✅ 정규식 및 검증 (Validation): 사용자가 입력한 비밀번호가 '영문, 숫자, 특수문자 포함 8자리 이상'인지 검사하는 로직

  • ❌ 비추천 (단순 UI): 단순 버튼, 구분선 등 시각적(Visual) 요소는 테스트 코드를 짜는 대신 스토리북(Storybook) 등으로 눈으로 확인하는 것이 훨씬 효율적입니다.


2. 통합 테스트 (Integration Test)

정의

두 개 이상의 개별 단위(컴포넌트, 상태 관리, 훅 등)가 결합되어 올바르게 상호작용하는지 검증하는 테스트입니다.

용도

  • 유닛 테스트만으로는 잡을 수 없는 '컴포넌트 간의 연결 부위' 결함을 찾아냅니다.

  • 실제 사용자가 브라우저에서 화면을 조작하는 흐름과 가장 유사하게 동작을 검증합니다.

  • 구현 디테일(내부 변수명 등)이 바뀌어도 사용자 경험(기능)이 유지되면 테스트가 통과하므로 유지보수성이 좋습니다.

사용법

  • 주요 도구: Testing Library (React, Svelte 등), MSW (API 모킹)

  • 작성 패턴: render로 여러 컴포넌트가 얽힌 덩어리(또는 페이지)를 렌더링하고, userEvent를 통해 사용자의 클릭/타이핑을 시뮬레이션한 뒤, screen 객체로 화면에 나타난 변화를 단언합니다.

  • 특징: 실제 API 서버 대신 MSW 등의 도구를 사용하여 네트워크 통신만 Mocking하고, 나머지는 실제와 가깝게 둡니다.

🤝 필요한 상황 (적용 타겟)

"프론트엔드 테스트의 꽃"입니다. 사용자가 특정 목적을 달성하기 위한 흐름을 테스트하며, 개별 컴포넌트가 아니라 여러 컴포넌트가 조립된 '페이지'나 '복잡한 폼(Form)' 수준에서 진행합니다.

  • ✅ 회원가입 폼 작성: 사용자가 이메일을 입력하고(Input) 👉 중복 확인 버튼을 누르고(Button) 👉 API 응답을 받아 '사용 가능한 이메일입니다'라는 메시지가 뜨는지(Text) 확인

  • ✅ 검색 필터링: 사용자가 드롭다운에서 '서울'을 선택하고 👉 체크박스에서 '한식'을 누르면 👉 해당 조건에 맞는 식당 리스트가 화면에 새로 렌더링되는지 확인

  • ✅ 모달 창 상호작용: 삭제 버튼을 누르면 확인 모달이 뜨고, 모달 안의 '네, 삭제합니다'를 눌렀을 때 비로소 리스트에서 아이템이 사라지는지 확인


3. E2E 테스트 (End-to-End Test, 종단간 테스트)

정의

실제 브라우저 환경에서, 데이터베이스부터 백엔드 서버, 프론트엔드 UI까지 모든 시스템을 결합하여 애플리케이션의 시작부터 끝까지 전체 흐름을 검증하는 테스트입니다.

용도

  • "실제 사용자가 프로덕션을 사용할 때 치명적인 문제가 없는가?"에 대한 가장 확실한 대답을 제공합니다.

  • 브라우저 호환성 문제나 복잡한 네트워크 타이밍 이슈를 발견할 수 있습니다.

사용법

  • 주요 도구: Playwright, Cypress, Selenium

  • 작성 패턴: 브라우저를 열고 특정 URL로 이동(visit)한 뒤, 실제 사용자처럼 페이지를 이동하며 상호작용하고 최종 목적지(예: 완료 페이지) 도달을 단언합니다.

  • 특징: 프론트엔드 프로젝트 코드 외부에 완전히 독립적인 폴더(예: 루트 디렉토리의 /e2e)를 구성하여 작성합니다.

🌐 필요한 상황 (적용 타겟)

테스트 작성과 실행이 가장 무겁고 유지보수가 힘들기 때문에, "이 기능이 고장 나면 회사의 매출이 깎이거나 서비스가 멈추는 치명적인 기능"에만 최소한으로 작성합니다.

  • ✅ 핵심 비즈니스 흐름 (Happy Path): 쇼핑몰의 경우 '상품 검색 👉 장바구니 담기 👉 로그인 👉 결제 완료'로 이어지는 전체 사이클이 실제 서버와 연동하여 무사히 통과하는지 확인

  • ✅ 주요 인증 흐름: 소셜 로그인(구글, 카카오)을 거쳐 메인 대시보드에 정상적으로 진입하는지 확인

  • ❌ 비추천 (사소한 흐름): 사소한 UI 변화나 수많은 엣지 케이스를 모두 E2E로 검증하려 하면 유지보수 지옥에 빠집니다. 최소한의 방어선만 구축해야 합니다.


💡 한눈에 보는 요약 비교

구분유닛 테스트 (Unit)통합 테스트 (Integration)E2E 테스트 (End-to-End)
테스트 대상순수 함수, 개별 로직, 훅상호작용하는 컴포넌트 그룹전체 시스템 환경 (DB~UI)
속도⚡ 매우 빠름🏃 빠름🐢 느림
유지보수 난이도쉬움보통어려움 (코드 변경에 취약)
의존성(외부 환경)완벽히 차단 (모두 Mocking)네트워크(API) 정도만 Mocking100% 실제 환경 결합
프론트엔드 전략복잡한 계산식, 데이터 변환, 정규식 검증사용자 상호작용(폼, 모달, 필터링) 흐름 검증 (비중 1순위)장바구니 결제, 주요 로그인 등 치명적인 핵심 흐름

이중 루프 TDD

이중 루프 TDD(Double-Loop TDD)는 이름 그대로 두 개의 톱니바퀴(루프)가 맞물려 돌아가는 형태의 개발 방식입니다. 앞서 말씀드린 '통합 테스트 먼저, 유닛 테스트 나중' 방식의 단점을 완벽하게 보완해 주는 실무적인 접근법이죠.

이 방식을 이해하면 "대체 테스트를 왜, 어떤 순서로 짜야 하는가?"에 대한 답이 아주 명확해집니다.

이 두 개의 루프는 각각 역할이 다릅니다.

  1. 바깥 루프 (Outer Loop - 통합/E2E 테스트): "사용자가 원하는 기능이 제대로 동작하는가?" (비즈니스 요구사항 검증)

  2. 안쪽 루프 (Inner Loop - 유닛 테스트): "개발자가 만든 함수와 컴포넌트가 제대로 만들어졌는가?" (기술적 구현 검증)


🔄 이중 루프 TDD의 진행 과정

  1. 바깥 루프 시작 (통합 테스트 작성): 사용자의 시나리오를 바탕으로 실패하는 통합 테스트를 하나 작성합니다.

  2. 안쪽 루프 진입 (유닛 테스트 작성): 위 통합 테스트를 통과시키기 위해 필요한 아주 작은 단위의 기능(컴포넌트나 함수)에 대한 실패하는 유닛 테스트를 작성합니다.

  3. 코드 구현: 유닛 테스트를 통과할 만큼만의 실제 코드를 작성합니다.

  4. 리팩토링: 코드를 깔끔하게 다듬습니다. (여기까지가 안쪽 루프 1회전 완료)

  5. 반복: 다른 컴포넌트가 더 필요하다면 2~4번(안쪽 루프)을 계속 반복합니다.

  6. 바깥 루프 완료: 안쪽 루프를 돌며 부품들을 다 만들고 조립하다 보면, 처음에 실패했던 1번의 통합 테스트가 마침내 통과(Pass)하게 됩니다.

예시 / 코드

헷갈렸던 점

실무 적용

  • 어디에 써먹을 수 있나
  • 관련 작업: [[]]

더 공부할 것

  • [ ]

🔗 참고