
최근에 인프런에서 프론트엔드 테스트에 대한 강의를 들으면서 생각한 내용을 주절주절 적어보려고 한다.
프론트엔드에서 테스트가 필요한지? 에 대해서는 과거에는 논쟁이 있었던 것으로 기억한다. 누군가는 필요하다, 누군가는 필요하지 않다.
내가 이전에 다니던 회사는 프론트엔드 테스트 코드가 없었고, 지금 회사는 있다. 하지만 테스트 코드가 있다고 크게 더 나은 점이 있는지는 잘 모르겠다. 가끔씩 프론트엔드 측에서 코드만 수정했을 때 테스트를 수정해 주지 않아서 테스트 CI가 깨지는 일이 몇 번 있었는데 오히려 번거롭다는 생각이 들 때도 있었던 것 같다.
그런데 이번에 강의를 들으면서 프론트엔드에서도 테스트 코드가 필요하다 는 생각이 강하게 들었다. 어떠한 이유들로 테스트 코드의 필요성에 대해 느끼게 되었는지 이번 글에서는 이야기를 해 보려고 한다.
단위 테스트 (Unit Test)
단위 테스트에 대한 정의는 다음과 같다.
앱에서 테스트 가능한 가장 작은 소프트웨어(단일 함수, 단일 컴포넌트)를 실행해 예상대로 동작하는지(결괏값 또는 상태) 확인하는 테스트
우리가 Textfield를 만든다고 가정해보자. 간단한 컴포넌트이지만 생각보다 신경 써야 할 영역들이 많다.

라벨, 필수 여부, 패딩, 좌우 아이콘, 플레이스 홀더, 헬퍼 텍스트 등등 UI적인 요소 뿐만 아니라 클릭 했을 때, 강조(focus)되었을 때, 마우스 커서를 올려 놓았을 때, 엔터 키를 눌렀을 때 등 이벤트 처리에 대해서도 고려할 부분이 많다.
이걸 문서화 해놓자니 너무 번거롭고 또 요구사항이 바뀌면 문서까지 수정해 주어야 하니 일을 두 번씩 하게 된다. 그리고 여러 개발자들과 함께 일을 하는 경우 서로 생각이 다를 수도 있고, 사람이다보니 빼먹고 구현을 못할 수도 있다.
만약 우리가 이런 Textfield 하나를 만들 때에도 필요한 모든 요소와 이벤트 등에 대해서 테스트 코드를 작성해 놓았다면 어땠을까? 물론 처음에 이 디자인 시스템을 만들 때는 좀 힘들었겠지만, 이후에 Textfield 관련된 버그나 요구사항 누락 등에 대해서 커뮤니케이션을 할 일은 훨씬 줄어들었을 것이라고 장담한다. 디자이너와 개발자가 이 Textfield 하나를 만들면서 필요한 모든 요소들에 대해서 같이 고민하다 보면 이후에 이 Textfield가 쓰이는 수십, 수백여 페이지에서 하게 될 Textfield와 관련된 커뮤니케이션을 아마 미리 하게 되었을 수도 있다.
Textfield와 관련된 요구사항이 처음에 만들고 나서 추가되고 수정될 수 있다. 이 경우에도 Textfield에 이미 테스트 코드가 잘 짜여져 있고, 개발자와 디자이너가 이에 대해서 이해도가 높다면 확장 가능성을 고려해서 의사 결정을 충분히 내릴 수 있다고 생각한다. 예를 들면, 헬퍼 텍스트 자리에 유효성 검증 에러가 나타나는 경우 에러 메시지를 보여주는 요구사항이 생긴다고 했을 때 1) 기존의 헬퍼 텍스트가 있다면 가릴 것인지 2) 언제 에러 메시지를 사라지게 할 것인지 등등 헬퍼 텍스트와 관련된 추가적인 요구사항을 미리 다 생각해 볼 수 있고, 이걸 테스트 코드로 다 만들어 놓는다면 미래에 신규 입사자가 왔을 때 기존에 개발자와 디자이너가 했던 고민들에 대해 번거롭게 묻지 않아도 테스트 코드만 보고 충분히 그 컴포넌트의 명세를 이해할 수 있을 것이다.
결국은 우리가 무엇을 만들건지에 대해서 더 정확하고 엄밀하게 이해할 수 있다는 점에서 테스트 코드를 짜는 것이 매우 중요하고 꼭 필요하다.
Arrange - Act - Assert (AAA) 패턴
단위 테스트를 작성하는 패턴 중에서 대표적인 것이 바로 AAA 패턴이다.
- Arrange : 테스트를 위한 환경을 준비하기
- Act : 테스트 할 동작을 발생시키기
- Assert : 올바른 동작이 실행되었는지 검증하기, 변경사항이 제대로 바뀌었는지 검증하기
사실 우리는 이러한 패턴을 개발하면서 이미 쓰고 있다. PRD라고 하는 기획 문서에서 ~~~ 한 상황에서 ~~~ 을 실행하면 ~~~ 한 결과가 나타난다. 이런 식으로 말이다. 테스트 코드는 이러한 PRD 문서를 코드로 옮겨놓은 형태라고 생각한다.
만약에 개발된 결과물이 PRD와 다르면 보통은 버그라고 생각하는 경우가 맞다. 하지만 이게 버그일 수도 있지만, PRD에 명시되지 않은 경우일 가능성도 있다. 기획자도 사람이라 어떤 경우는 PRD가 틀릴 수도 있다. 기획자의 입장에서는 PRD의 문서 대로 결과물이 제대로 구현이 되어 있는지 항상 신경을 쓸 수 밖에 없는데, 테스트 코드가 잘 짜여져 있으면 지속적으로 기능이 추가되더라도 기존의 기능들이 PRD 대로 문제 없이 동작함을 보장할 수 있기 때문에 기획자도 신경을 덜 쓸 수가 있다.
모든 테스트는 독립적으로 실행이 되어야 한다. 순서가 바뀐다고 테스트 결과가 바뀌면 그것은 좋은 테스트라고 볼 수 없다. 그래서 테스트 전과 후에는 일관성을 보장하기 위하여 setup, teardown이라는 단계를 넣어 준다.
- setup : 테스트를 실행하기 전 수행해야 하는 작업 (e.g. beforeEach, beforeAll)
- teardown : 테스트를 실행한 뒤 수행해야 하는 작업 (e.g. afterEach, afterAll)
Spy 함수
Spy 함수의 정의는 다음과 같다.
테스트 코드에서 특정 함수가 호출되었는지, 함수의 인자로 어떤 것이 넘어왔는지, 어떤 값을 반환하는지 등을 검증할 수 있는 함수이다.
예를 들어, 다음과 같은 테스트 코드가 있다고 가정했을 때
it('텍스트를 입력하면 onChange props으로 등록된 함수가 호출된다.', async () => {
const spy = vi.fn(); // 스파이 함수
const { user } = await render(<Textfield onChange={spy} />);
const textInput = screen.getByPlaceholderText('텍스트를 입력해 주세요.');
await user.type(textInput, 'test');
expect(spy).toHaveBeenCalledWith('test');
});
스파이 함수 spy는 Textfield의 onChange 이벤트 핸들러가 호출이 될 때 검증을 해볼 수 있다.
우리가 디버깅을 할 때 이벤트 핸들러에 중단점(breakpoint)을 걸어서 해당 이벤트가 트리거 되는 시점을 찾아서 하는 경우가 많다. 테스트 코드를 잘 작성하면, (테스트 코드가 없을 때 해야 할) 상당 부분의 디버깅을 자동화된 테스트로 이미 처리할 수 있기 때문에 사람이 필요한 디버깅에 더 집중할 수 있다는 장점이 있다.

Mock 함수
모킹(Mocking)이란 다음과 같다.
실제 모듈, 객체와 동일한 동작을 하도록 만든 모의 모듈, 객체(Mock)로 실제를 대체하는 것
예를 들어 우리가 어떠한 백버튼 기능을 구현했는데 다음과 같이 useNavigate를 import 해서 쓴다고 가정해보자
// ...
import { useNavigate } from 'react-router-dom';
// ...
const handleClick = () => {
navigate(-1);
};
이 handleClick 함수를 테스트할 때 useNavigate까지 검증을 할 필요는 없다. 따라서 useNavigate()에 대한 추가적인 검증은 불필요하다. 이처럼 단위 테스트에서는 외부 모듈에 대한 검증은 완전히 분리하고 모듈의 특정 기능을 제대로 호출하는지만 검증한다.
위의 코드를 테스트 하는 경우 'react-router-dom' 모듈 중에서 다른 것들은 그대로 두고 useNavigate만 모킹을 하면 된다. 그래서 여기서는 importActual() 이라는 함수를 사용해서 'react-router-dom'을 그대로 사용하고 useNavigate 훅만 모킹을 했다. 함수가 올바르게 호출이 되었는지 확인하려면 spy 함수를 사용하면 된다.
// 실제 모듈을 모킹한 모듈로 대체하여 테스트 실행
// useNavigate 훅으로 반환받은 navigate 함수가 올바르게 호출되었는가 -> 스파이 함수
const navigateFn = vi.fn()
vi.mock('react-router-dom', async () => {
const original = await vi.importActual('react-router-dom');
return { ...original, useNavigate: () => navigateFn };
});
it('홈으로 가기 링크 클릭시 '/' 경로로 navigate 함수가 호출된다.', async () => {
// ...
expect(navigateFn).toHaveBeenNthCalledWith(1, '/');
});
모킹의 경우 setup에서 모킹을 했다면, 반드시 teardown에서 초기화를 해 주어야 한다. 그래야 다른 테스트에 영향을 주지 않는다.
통합 테스트 (Integration Test)
실제 어플리케이션을 만들다 보면 두 개 이상의 모듈을 상호 작용 해서 만드는 경우가 대부분이다. 단위 테스트로는 이러한 여러 개의 모듈을 검증하기 어렵다. 각각의 케이스에 대해서는 검증할 수는 있지만 그렇다고 그 둘이 합쳐졌을 때 원하는 대로 동작하는가는 다른 문제에게 때문이다. 따라서 통합 테스트는 다음과 같은 경우에 주로 많이 작성한다.
- 특정 상태를 기준으로 동작하는 컴포넌트 조합
- API와 함께 사용하는 컴포넌트 조합
단위 테스트와 통합 테스트의 커버하는 범위는 아래의 예시처럼 다르다.
- 단위 테스트 : 장바구니 구매 버튼을 눌렀을 때 spy 함수로 각 핸들러가 호출이 되는지를 검증할 수 있다.
- 통합 테스트 : 장바구니 구매 버튼을 눌렀을 때 1) 로그인 한 유저는 상품 추가 후 장바구니로 이동하고, 2) 비로그한 유저는 로그인 페이지로 이동하는지 각각의 상황에 대한 검증을 할 수 있다.
결국 비즈니스 로직을 기반으로 한 통합 테스트가 실제 사용자의 입장에서 동작하는 기능들을 온전하게 테스트 할 수 있는 단위라고 볼 수 있는 것이다. 이러한 통합 테스트를 꼼꼼하게 작성해 놓으면 QA가 기존에 만들어 놓았던 로직을 매번 다시 수동으로 할 필요 없으며, 팀의 개발 및 릴리즈 주기를 앞당기는데 큰 도움이 된다. 개발자 역시 해당 제품의 이해도가 올라가는 건 덤이다.
통합 테스트를 할 때 모킹을 가능한 하지 않는 것이 좋다. 이유는 그래야 앱의 실제 기능과 유사한 검증이 가능하기 때문이다. 모킹이 너무 많으면 모킹한 영역에서 에러가 발생할 시 테스트는 성공하는데 실제 동작은 깨지는 일이 발생할 가능성이 높다.
하지만 우리가 그렇다고 모킹을 아예 안 쓸 수는 없다. 예를 들면 실제 장바구니 정보를 API 통신으로 불러와서 테스트를 할 수는 없으므로, 이 경우는 MSW(Mock Service Worker)같은 도구를 사용해서 모킹을 통해 장바구니 정보를 불러왔다고 가정하고 그 이후 로직을 테스트 하면 된다. 상태 관리 역시 Zustand 같은 도구를 많이 쓰는데, 여기에서 상태를 모킹해서 예를 들면, 로그인을 한 상태라고 가정하고 어떤 동작을 수행하는지 테스트를 해볼 수 있다.
통합 테스트를 작성하다보면 하나의 컴포넌트 안에서 로직이 적절하게 할당이 되어 있는지, 너무 복잡하거나 많지는 않은지 생각을 해볼 수 있고 더 적합한 위치를 찾을 수가 있게 된다. 테스트를 짜기가 어렵다는 말이 설계가 지금 무언가 문제가 있다는 말이다. 한 눈에 읽기 좋은 코드가 잘 짜여진 코드인 것처럼 테스트를 수월하게 짤 수 있는 코드가 좋은 설계이고 이는 결국 좋은 코드로 이어질 가능성이 높다. 좋은 설계를 고민하는 개발자라면 테스트 코드 도입을 통해 그 방향으로 나아갈 수 있다.
참고자료
실무에 바로 적용하는 프런트엔드 테스트 - 1부. 테스트 기초: 단위・통합 테스트| 코드 조커, 오
현재 평점 4.9점 수강생 1,017명인 강의를 만나보세요. 이 강의를 통해 전반적인 프런트엔드 테스트 종류를 파악하고, 상황에 맞는 적절한 테스트 선택을 통해 신뢰감 있는 테스트를 작성하는 방
www.inflearn.com
'Web Frontend Developer' 카테고리의 다른 글
| [번역] package.json을 관리하는 방법 (1) | 2025.12.10 |
|---|---|
| [번역] HTTP 캐싱 완벽 가이드 (1) | 2025.11.20 |
| [요즘IT] 프론트엔드 개발자가 써본 "피그마 MCP"의 가능성과 한계 (0) | 2025.11.05 |
| [번역] 웹어셈블리(Wasm) 3.0 (0) | 2025.10.23 |
| 2024년 자바스크립트 트렌드 돌아보기 (4) | 2025.01.31 |