8.2. 리액트 팀이 권장하는 리액트 테스트 라이브러리
- 테스트란 개발자가 만든 프로그램이 코딩을 한 의도대로 작동하는지 확인하는 일련의 작업을 의미한다. 테스트를 통해 개발자들은 처음에 설계한 대로 프로그램이 작동하는지 확인할 수 있고, 버그를 사전에 방지할 수 도 있으며, 이후에 잘못된 작동으로 인해 발생하는 비용을 줄일 수도 있다. 또 코드를 수정하게 되더라도 테스트 과정이 있다면 이후에 수정한 내용에 대해서도 예외 케이스가 없고 의도한 대로 작동할 수 있는지 확인할 수 있다. 그리고 이러한 일련의 테스트를 거친 프로그램은 사용자에게 버그가 최소화된 안정적인 서비스를 제공할 수 있는 원동력이 된다.
- 프론트엔드와 백엔드 모두 테스팅이 중요하지만 테스트하는 방법과 방법론은 사뭇 다르다. 백엔드의 테스트는 일반적으로 서버나 데이터베이스에서 원하는 데이터를 올바르게 가져올 수 있는지, 데이터 수정 간 교착 상태나 경쟁 상태가 방생하지는 않는지, 데이터 손실은 없는지, 특정 상황에서 장애가 발생하지는 않는지 등을 확인하는 과정이 주를 이룬다. 이러한 백엔드 테스트는 일반적으로 화이트박스 테스트로, 작성한 코드가 의도대로 작동하는 지 확인해야 하며, 이는 GUI가 아닌 AUI(Application User Interface; 응용 프로그램 사용자 인터페이스)에서 주로 사용해야 하기 때문에 어느 정도 백엔드에 대한 이애가 있는 사람만 가능하다.
- 반면 프론트엔드는 일반적인 사용자와 동일하거나 유사한 환경에서 수행된다. 사용자가 프로그램에서 수행할 주요 비즈니스 로직이나 모든 경우의 수를 고려해야 하며, 이 과정에서 사용자는 굳이 프론트엔드 코드를 알 필요는 없다. 즉, 블랙박스 형태로 테스트가 이뤄지며, 코드가 어떻게 됐든 상관없이 의도한 대로 작동하는지를 확인하는 데 좀 더 초점이 맞춰져 있다. 그리고 시나리오가 어느 정도 정해져 있는 백엔드와는 다르게, 프론트엔드는 사용자에게 완전히 노출된 영역이므로 어떻게 작동할지 최대한 예측해서 확인해야 한다. 사용자는 개발자의 의도대로만 사용하지 않기 때문이다.
- 프론트엔드 개발은 HTML, CSS와 같이 디자인 요소뿐만 아니라 사용자의 인터렉션, 의도치 않은 작동 등 브라우저에서 발생할 수 있는 다양한 시나리오를 고려해야 하기 때문에 일반적으로 테스팅하기가 매우 번거롭고 손이 많이 가는 작업이다. 그리고 이러한 특징 때문에 제공되는 테스팅 라이브러리도 상당히 다양한 편이다. 단순히 함수나 컴포넌트 수준에서 유닛 테스트를 할 수도 있고, 사용자가 하는 작동을 모두 흉내내서 테스트 할 수도 있다.
- 이번 절에서는 리액트로 개발된 어플리케이션을 테스팅 하는 방법, 특히 가장 널리 사용되는 React Testing Library 위주로 살펴본다. 리액트 개발 환경에서 테스트를 수행하는 방법을 살펴보고, 어떤 도움을 얻을 수 있는 지 알아보자.
8.2.1. React Testing Library란?
- React Testing Library(이하 리액트 테스팅 라이브러리)란 DOM Testing Library를 기반으로 만들어진 테스팅 라이브러리로, 리액트를 기반으로 한 테스트를 수행하기 위해 만들어졌다. 리액트 테스팅 라이브러리를 이해하려면 먼저 리액트 테스팅 라이브러리가 기반으로 하는 DOM Testion Library에 대해 먼저 알아둬야 한다. DOM Testion Library는 jsdom을 기반으로 하고 있다. jsdom이란 순수하게 자바스크립트로 작성된 라이브러리로, HTML이 없는 자바스크립트만 존재하는 환경, 예를 들어 Node.js 같은 환경에서 HTML과 DOM을 사용할 수 있도록 해주는 라이브러리다. jsdom을 사용하면 자바스크립트 환경에서도 HTML을 사용할 수 있으므로 이를 기반으로 DOM Testing Library에서 제공하는 API를 사용해 테스트를 수행할 수 있다. 다음 예제 코드를 보자.
const jsdom = require('jsdom')
const{ JSDOM } = jsdom
const dom = new JSDOM('<!DOCTYPE html><p>Hello World</p>')
console.log(dom.window.document.querySelector('p'),textcontext) //"Hello World"
이처럼 jsdom을 사용하면 마치 HTML이 있는 것처럼 DOM을 불러오고 조작할 수 있다.
- jsdom을 사용해 자바스크립트 환경에서 HTML을 사용할 수 있는 DOM Testing Library를 기반으로, 동일한 원리로 리액트 기반 환경에서 리액트 컴포넌트를 테스팅할 수 있는 라이브러리가 바로 리액트 테스팅 라이브러리다. 리액트 테스팅 라이브러리를 활용하면 실제로 리액트 콤포텉느를 렌더링하지 않고도, 즉 브라우저를 직접 실행해 눈으로 확인하지 않아도 리액트 컴포넌트가 원하는 대로 렌더링되고 있는지 확인할 수 있다. 이러한 테스트 방식은 굳이 테스트환경을 구축하는 데 복잡한 과정을 거치지 않아 간편하고, 테스트에 소요되는 시간 역시 효과적으로 단축시킬 수 있다. 그리고 컴포넌트 뿐만 아니라 Provider, 훅 등 리액트를 구성하는 다양한 요소들을 테스트 할 수 있다. 이번 절에서는 리액트 테스팅 라이브러리를 활용해 리액트를 구성하는 다양한 요소를 테스트 하는 방법을 알아본다.
8.2.2. 자바스크립트 테스트의 기초
- 본격적으로 리액트 테스트 코드를 작성하기에 앞서, 먼저 자바스크립트에서 테스트 코드는 어떻게 작성하는 지에 대해 먼저 알아보자. 만약 인수 두개의 합을 더하는 함수를 만들었다고 가정해보자.
function sum(a, b){
return a+b
}
- 이 함수에 대한 테스트 코드를 작성한다면 어떻게 작성해야 할까? 테스트 코드를 작성하기에 앞서 테스트 코드가 무엇인지 상기하자. 테스트 코드란 내가 작성한 코드가 내가 코드를 작성했던 당시의 의도와 목적에 맞는지 확인하는 코드를 의미한다. 그런 의미에서 sum 함수에 대해서는 다음과 같은 테스트 코드를 작성해 볼 수 있을 것이다.
// 테스트 1
// 함수를 실행했을 때의 실제 결과
let actual = sum(1, 2)
// 함수를 실행했을 때 기대하는 결과
let expected = 3
if(expected !== actual){
throw new Error('${expected} is not equal to ${actual}')
}
// 테스트 2
actual = sum(2, 2)
expected = 4
if(expected !== actual){
throw new Error('${expected} is not equal to ${actual}')
}
- 테스트 코드를 작성하는 방식, 테스트 코드에 사용되는 인수 등 코드의 세세한 부분에는 조금씩 차이가 있겠지만 기본적인 테스트 코드를 작성하는 방식은 다음과 같은 과정을 거친다는 점에서 비슷할 것이다.
- 테스트할 함수나 모듈을 선정한다.
- 함수나 모듈이 반환하길 기대하는 값을 적는다.
- 함수나 모듈의 실제 반환 값을 적는다.
- 3번의 기대에 따라 2번의 결과가 일치하는지 확인한다.
- 기대하는 결과를 반환한다면 테스트는 성공이며, 만약 기대와 다른 결과를 반환하면 에러를 던진다.
- 이를 위해 가장 먼저 필요한 것이 "작성한 코드가 예상대로 작동한다면 성공했다는 메시지가 출력되고, 실패하면 에러를 던진다."라는 작동을 대신해 주는 라이브러리다. 다행히도 Node.js는 assert라믄 모듈을 기본적으로 제공하며, 이 모듈을 사용하면 위와 같이 작동하도록 만들 수 있다. assert라는 이름에서도 알 수 있듯이 테스트 코드가 예상대로 작동한다고 '주장'하는 코드를 작성하면 이 코드의 성공여부에 따라 테스트 통과 또는 실패를 반환한다. Node.js의 assert는 다음과 같이 사용할 수 있다.
const assert = require('assert')
function sum(a, b){
return a+b
}
assert.equal(sum(1, 2), 3)
assert.equal(sum(2, 2), 4)
assert.equal(sum(1, 2), 4) // AssertionError [ERR_ASSERTION][ERR_ASSERTION]: 3 == 4
- 일반적으로 테스트 코드와 실제 코드는 분리해 작성한다. 따라서 sum 함수는 다른 파일에 명시적으로 분리하는 것이 일반적이지만 여기서는 간단한 예제를 보여주기 위해 함께 작성했다.
- 이전 코드에서는 반환한 값에 대한 비교과 실패에 따른 에러를 던지는 작업 모두를 일일이 작성했지만 assert를 사용한 예제에서는 이러한 작업을 모두 간결하게 표현한 것을 확인할 수 있다. 또한 마지막 줄과 같이 테스트 코드가 실패하는 경우 assert.equal 내부에서 AssertionError라는 에러도 던져 테스트 코드가 실패했음을 명시적으로 알린다.
- 이처럼 테스트 결과를 확인할 수 있도록 도와주는 라이브러리를 어설션(assertion) 라이브러리라고 한다. 이러한 어설션 라이브러리에는 Node.js가 제공하는 assert 외에도 should.js, expect.js, chai등 다양하다. 그리고 이러한 어설션 라이브러리는 단순히 동등을 비교하는 equal 외에도, 객체 자체가 동일한지 확인하는 deepEqual, 같지 않은지 비교하는 notEqual, 에러를 던지는지 여부를 확인하는 throws 등 다양한 메서드를 제공한다. 테스트 코드를 작성하는 개발자들은 이러한 어설션을 활용해 다양한 시나리오를 작성하고, 이 시나리오 상에서 코드가 올바르게 작동하는지 확인할 수 있다.
- 그렇다면, 이 어설션 라이브러리만 준비한다면 테스트 코드를 작성할 준비가 다 끝난 것일까? 그렇지 않다. 테스트 코드는 가능한 한 사람이 읽기 쉽게, 그리고 테스트의 목적이 분명하게 작성되는 것이 중요하다. 앞에서 작성한 sum을 테스트하는 assert 모듈과 함수만으로도 어느 정도 테스트 코드의 목적을 달성했다고 볼 수도 있다. 그러나 테스트 코드가 실행되는 것을 지켜보는 입장에서는 다르다. 예를 들어, 앞의 테스트 코드가 CI 환경에서 자동으로 실행되게 만들었다고 가정해 보자. 테스트 코드가 정상적으로 작동하고, 테스트도 모두 통과하겠지만, 무엇을 테스트했는지, 무슨 테스트를 어떻게 수행했는지 등 테스트에 관한 실제 정보를 알 수는 없다. 즉, 좋은 테스트 코드는 다양한 테스트 코드가 작성되고 통과하는 것 뿐만 아니라 어떤 테스트가 무엇을 테스트 하는지 일목요연하게 보여주는 것도 중요하다.
- 이러한 테스트의 기승전결을 완성해 주는 것이 바로 테스팅 프레임워크다. 테스팅 프레임워크들은 어설션을 기반으로 테스트를 수행하며, 여기에 추가로 테스트 코드 작성자에게 도움이 될 만한 정보를 알려주는 역할도 함께 수행한다. 자바스크립트에서 유명한 테스팅 프레임워크로는 Jest, Mocha, Karma, Jasmine 등이 있다. 그리고 리액트 진영에서는 리액트와 마찬가지로 메타에서 작성한 오픈소스 라이브러리인 Jest가 널리 쓰이고 있다. Jest의 경우 자체적으로 제작한 expect 패키지를 사용해 어설션을 수행한다.
- 원래 Jest는 깃허브 주소에서도 알 수 있듯 메타에서 개발해서 운영하고 있으나, 최근에 들어서는 메타의 어떠한 직원도 Jest를 유지보수 하고 있지 않았던 것으로 보인다. 이에 따라 유지보수에 많은 어려움이 있었는데, 최근에는 메타에서 오픈소스 재단으로 관리 주체가 바뀌어서 이전보다는 활발하게 유지보수될 것으로 기대해 본다.
- 이제 앞의 테스트 코드를 Jest로 완전히 새롭게 작성해 보자. 이번에는 테스트 코드와 실제 코드를 별도로 분리했다.
function sum(a, b){
return a+b
}
module.exports = {
sum,
}
- 그리고 이 코드에 대한 테스트 코드를 다음과 같이 작성할 수 있다. 아래 함수는 의도적으로 틀리도록 만든 것이다.
const { sum } = require('./math')
test('두 인수가 덧셈이 되어야 한다.', () => {
expect(sum(1, 2)).toBe(3)
});
test('두 인수가 덧셈이 되어야 한다.', () => {
expect(sum(2, 2)).toBe(3) // 에러
});
- 그리고 테스트 코드를 실행하면 다음과 같은 결과를 얻을 수 있다.
> jest
FAIL ./math.test.js
√ 두 인수가 덧셈이 되어야 한다. (5 ms)
× 두 인수가 덧셈이 되어야 한다. (5 ms)
● 두 인수가 덧셈이 되어야 한다.
expect(received).toBe(expected) // Object.is equality
Expected: 3
Received: 4
6 |
7 | test('두 인수가 덧셈이 되어야 한다.', () => {
> 8 | expect(sum(2, 2)).toBe(3) // 에러
| ^
9 | });
at Object.toBe (math.test.js:8:21)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 3.113 s
Ran all test suites.
- 앞서 단순히 Node.js의 assert를 사용했던 것과 다르게 테스트를 실행하는 콘솔에서 볼 수 있는 테스트 관련 정보가 한층 다양해졌다. Node.js의 assert만 사용했을 때는 단순히 실패에 대해서만 단편적인 정보로 알 수 있었지만 jest를 비롯한 테스트 프레임워크를 사용하면 무엇을 테스트했는지, 소요된 시간은 어느 정도인지, 무엇이 성공하고 실패했는지, 전체 결과는 어떤지에 대한 자세한 정보를 확인할 수 있다. 이렇게 어설션 라이브러리를 내장한 테스트 프레임워크를 사용하면 테스트 코드를 작성하는 것 뿐만 아니라 테스트에 대한 결과와 관련 정보를 일목요연하게 확인할 수 있다.
- 앞의 테스트 코드에서 볼 수 있는 특별한 점은 test, expect 등의 메서드를 import나 require 같은 모듈을 불러오기 위해 사용하는 구문 없이 바로 사용했다는 것. 그리고 node가 아닌 jest(npm run test)로 실행했다는 것이다. 만약 해당 코드를 jest가 아닌 node로 바로 실행했다면 에러가 발생했을 것이다. 그 이유는 test와 expect 모두 Node.js 환경의 global, 즉 전역 스코프에 존재하지 않는 메서드이기 때문이다. 이러한 메서드가 실행 될 수 있는 비밀은 Jest CLI에 있다. Jest를 비롯한 테스팅 프레임워크에는 이른바 글로벌(global)이라 해서 실행 시에 전역 스코프에 기본적으로 넣어주는 값들이 있다. 그리로 Jest는 이 값을 실제 테스트 직전에 미리 전역 스코프에 넣어준다. 이렇게 하면 일일이 테스트에 관련한 정보를 임포트하지 않고도 사용할 수 있게 되는 것이다. 이는 간결하고 빠른 테스트 코드 작성에 도움을 준다. 이 전역 스코프에 삽입되는 값은 Jest 공식 문서에서 확인할 수 있다. 만약 이러한 글로벌을 활용한 Jest의 작동 방식이 조금 어색하다면 일반적인 다른 자바스크립트 파일을 작성했을 때와 마찬가지로 명시적으로 import 구문을 작성할 수 있다.
import { expect, jest, test } from '@jest/globals'
- 이렇게 하면 node로 실행해 테스트하는 것도 가능하다. 그러나 이러한 방식은 테스틑 코드 작성을 번거롭게 하므로 선호되지는 않는다.
'FE > 모던 리액트 Deep Dive' 카테고리의 다른 글
| 5. 리액트와 상태관리 라이브러리 - 2 (0) | 2025.10.06 |
|---|---|
| 5. 리액트와 상태관리 라이브러리 - 1 (0) | 2025.10.05 |
| 3. 리액트 훅 깊게 살펴보기 - useState (0) | 2025.09.18 |
| 8. 좋은 리액트 코드 작성을 위한 환경 구축하기 - 테스트(3) (0) | 2025.09.18 |
| 8. 좋은 리액트 코드 작성을 위한 환경 구축하기 - 테스트(2) (0) | 2025.09.17 |