8장 좋은 리액트 코드 작성을 위한 환경 구축하기
ESLint를 활용한 정적 분석과 리액트 테스트 라이브러리를 충분히 활용한다면 웹 서비스가 서비스되기 전에 여러가지 문제를 점검하고 확인할 수 있다.
8.1 ESLint를 활용한 정적 코드 분석
ESLint는 자바스크립트 생태계에서 가장 많이 사용되는 정적 코드 분석 도구이다.
8.1.1 ESLint 살펴보기
ESLint의 코드 분석 방법
-
자바스크립트 코드를 문자열로 읽는다
-
자바스크립트 코드를 분석할 수 있는 파서로 코드를 구조화한다.
- 파서는 여러 종류가 있는데 ESLint는 기본값으로 espree를 사용한다
- espree는 코드의 정확한 위치와 같은 세세한 정보도 분석해 알려준다
-
2번에서 구조화한 트리플 AST라 하며, 이 구조화된 트리를 기준으로 각종 규칙과 대조한다
- espree로 코드를 분석한 결과를 바탕으로, 어떤 코드가 잘못된 코드이며 어떻게 수정해야 할지도 정한다. 이를 ESLint 규칙이라고 하며, 특정한 규칙의 모음을 plugins라고 한다.
- ESLint는 공식 홈페이지에서 기본적으로 몇 가지 규칙을 제공한다.
-
규칙과 대조했을 때 이를 위반한 코드를 알리거나 수정한다
8.1.2 eslint-plugin과 eslint-config
eslint-plugin
- eslint 규칙을 모아놓은 패키지다
- 예) eslint-plugin-import는 자바스크립트에서 다른 모듈을 불러오는 import와 관련된 규칙을 제공한다
eslint-config
- eslint-plugin을 한데 묶어서 완벽하게 한 세트로 제공하는 패키지다
- 내가 원하는 규칙들을 한데 모아서 설치하고 적용하는 것보다 이미 존재하는 eslint-config를 설치해서 빠르게 적용할 수 있다
네이밍 규칙
- eslint-plugin, eslint-config라는 접두사를 준수해야 한다
- 반드시 한 단어로 구성해야 한다
- 가능 :
eslint-plugin-naver
- 불가능 :
eslint-plugin-naver-financials
- 특정 스코프가 앞에 붙는 것 까지는 가능하다
- 가능 :
@titicaca/eslint-config-triple
- 불가능 :
@titicaca/eslint-config-triple-rules
IT기업에서 잘 만든 대표적인 eslint-config
8.1.3 나만의 ESLint 규칙 만들기
ESLint 규칙을 생성해 관리하면 개발자가 수동으로 수정하는 것보다 훨씬 더 빠르고 휴먼 에러도 방지할 수 있다.
이미 존재하는 규칙을 커스터마이징해서 적용하기: import React를 제거하기 위한 ESLint 규칙 만들기
- 리액트 17 버전부터는 import React 구문이 필요 없어졌다. 이에 따라 import React를 삭제하면 번들러의 크기를 줄일 수 있게 된다
- 이미 존재하는 규칙인 no-restricted-imports 규칙을 사용해서 import React를 금지할 수 있다. 이 규칙은 어떠한 모듈을 import하는 것을 금지하기 위해 만들어진 규칙이다
module.exports = {
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
name: "react",
importNames: ["default"],
message:
"import React from 'react'는 react 17부터 더 이상 필요하지 않습니다",
},
],
},
],
},
};
완전히 새로운 규칙 만들기: new Date를 금지시키는 규칙
- 한국 시간을 반환하기 위해 기기에 종속된 현재 시간인 new Date()을 사용하지 않고 ServerDate()를 만들어 이 함수만 사용하도록 규칙을 만들어야 한다
- 이 규칙을 만들기 전에 new Date() 코드를 작성한 후 espree에서 AST를 어떻게 만드는지 확인해야 한다
- AST로 type, calleename 등을 확인한 후 ESLint의 create 함수를 통해 규칙을 만들어 보자
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow use of the new Date()',
recommended: false,
},
fixable: 'code',
schema: [],
messages: {
message:
'new Date()는 클라이언트에서 실행 시 해당 기기의 시간에 의존적이라 정확하지 않습니다. ..."
},
},
create: function(context) {
return {
NewExpression: function (node) {
if(node.callee.name === "Date" && node.arguments.lenght === 0) {
context.report({
node:node,
messageId: 'message',
fix: function (fixer) {
return fixer.replaceText(node, 'ServerDate()')
}
})
}
}
}
}
}
- 위와 같이 규칙을 만들면 eslint-plugin 형태로 규칙을 묶음으로 배포하면 된다. 규칙은 하나씩 배포하는 것은 불가능하다
8.1.4 주의할 점
Prettier와의 충돌
Prettier는 코드의 포매팅과 관련된 작업(줄바꿈, 들여쓰기, 작은따옴표, 큰따옴표 등)을 담당한다
ESLint에서도 포매팅과 관련된 작업을 처리할 수 있기 때문에 두 가지 모두를 자바스크립트 코드에서 실행한다면 서로 충돌하는 규칙으로 에러가 발생할 수 있다
- 해결 방법
- 서로 규칙이 충돌되지 않게끔 규칙을 잘 선언한다
- 자바스크립트나 타입스크립트는 ESLint에, 그 외의 파일은 모두 Prettier에 맡긴다
규칙에 대한 예외 처리
만약 일부 코드에서 특정 규칙을 임시로 제외시키고 싶다면 eslint-disable-주석을 사용하면 된다
console.log("hello world");
console.log("hello world");
console.log("JavaScript debug log");
console.log("eslint is disabled now");
console.log("hello world");
react-hooks/no-exhaustive-deps
이 규칙은 useEffect나 useMemo와 같이 의존 배열이 필요한 훅에 의존성 배열을 제대로 선언했는지 확인하는 역할을 한다. 이 규칙을 예외처리 하면 잠재적인 버그를 야기할 수 있다.
- 괜찮다고 임의로 판단한 경우에는 해당 변수를 어디서 어떻게 선언할지 다시 고민해봐야 한다
- 의존성 배열이 너무 긴 경우에는 useEffect를 분리해서 의존성 배열의 가독성과 안정성을 확보해야 한다
- 마운트 시점에 한 번만 실행하고 싶은 경우에 의도적으로 []로 모든 의존성을 제거하는데 과거 클래스 컴포넌트에서 사용되던 생명주기 형태의 접근 방법으로 함수 컴포넌트의 패러다임과는 맞지 않을 가능성이 있다. 또한 컴포넌트의 상태값과 별개의 부수 효과가 되어 컴포넌트의 상태와 불일치가 일어날 수 있게 된다.
ESLint 버전 충돌
- 간혹 최신 버전의 eslint-config를 설치하면 react-scripts와 eslint-config-triple의 ESLint 의존성이 맞지 않아 에러가 발생한다.
- 이러한 에러를 방지하기 위해서 eslint-config, eslint-plugin이 지원하는 ESLint 버전을 확인하고, 또 설치하고자 하는 프로젝트의 ESLint 버전을 어떻게 지원하고 있는지 살펴봐야 한다
8.2 리액트 팀이 권장하는 리액트 테스트 라이브러리
테스트란?
- 개발자가 만든 프로그램이 코딩을 한 의도대로 작동하는지 확인하는 일련의 작업이다
- 사용자에게 버그가 최소화된 안정적인 서비스를 제공할 수 있는 원동력이 된다
- 테스트 코드와 QA의 차이점
| 항목 | 테스트 코드 | QA |
|---|
| 목적 | 기능과 동작이 코드 설계대로 작동하는지 확인 | 최종 사용자 관점에서 제품의 품질을 보장하고 경험을 최적화 |
| 책임자 | 개발자 | QA 팀 또는 전담 테스터, 가끔 개발자가 함께 진행 |
| 범위 | 특정 함수, 컴포넌트, 혹은 사용자 흐름 | 전체 애플리케이션의 기능, UI, UX, 성능 |
| 방식 | 자동화된 테스트 코드 실행 | 사람 중심의 수동 테스트 또는 자동화된 도구 사용 |
| 환경 | 개발 환경 | 실제 사용자 환경을 재현 (다양한 브라우저, 디바이스 등 포함) |
| 속도 | 빠르고 반복적인 실행 가능 | 시간이 오래 걸릴 수 있으며 반복 작업이 많음 |
| 발견할 수 있는 문제 | 코드 상의 버그, 특정 로직 오류 | 사용자 경험 문제, 엣지 케이스, 성능 문제 등 |
프론트엔드에서의 테스트
- 백엔드의 테스트는 일반적으로 화이트 박스 테스트로, 작성한 코드가 의도대로 작동하는지 확인해야 하며, AUI에서 수행해야 한다
- 프론트엔드는 블랙박스 형태로 테스트가 이뤄지며, 사용자와 동일하거나 유사한 환경에서 수행한다. 코드가 어떻게 됐든 의도한 대로 작성하는데 초점이 맞춰져 있다.
- HTML, CSS와 같이 디자인 요소뿐만 아니라 사용자의 인터랙션, 의도치 않은 작동 등 브라우저에서 발생할 수 있는 다양한 시나리오를 고려해야 하기 때문에 일반적으로 테스팅하기가 매우 번거롭다
8.2.1 React Testing Libraty란?
- DOM Testing Libraty를 기반으로 만들어진 테스팅 라이브러리로, 리액트를 기반으로 한 테스트를 수행하기 위해 만들어졌다
- DOM Testing Libraty는 jsdom을 기반으로 한다. jsdom은 자바스크립트로만 작성된 라이브러리로, HTML이 없는 자바스크립트만 존재하는 환경에서 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").textContent);
- 위 예제처럼 jsdom을 사용하면 마치 HTML이 있는 것처럼 DOM을 불러오고 조작할 수 있다
- 리액트 테스팅 라이브러리를 활용하면 실제로 리액트 컴포넌트를 렌더링하지 않고도, 리액트 컴포넌트가 원하는 대로 렌더링되고 있는지 확인할 수 있다.
8.2.2 자바스크립트 테스트의 기초
기본적인 테스트 코드를 작성하는 방식
- 테스트할 함수나 모듈을 선정한다
- 함수나 모듈이 반환하길 기대하는 값을 적는다
- 함수나 모듈의 실제 반환 값을 적는다
- 3번의 기대에 따라 2번의 결과가 일치하는지 확인한다
- 기대하는 결과를 반환한다면 테스트는 성공이며, 만약 기대와 다른 결과를 반환하면 에러를 던진다
assert 모듈
- Node.js에서 기본적으로 제공하며, 테스트 코드를 작성하면 이 코드의 성공 여부에 따라 테스트 통과 또는 실패를 반환하는 모듈이다
- 사용 예시
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);
- 이처럼 테스트 결과를 확인할 수 있도록 도와주는 라이브러리를 어설션(assertion) 라이브러리라고 한다
테스팅 프레임워크
- 테스트 코드는 가능한 한 사람이 읽기 쉽게, 그리고 테스트의 목적이 분명하게 작성되는 것이 중요하다
- 테스팅 프레임워크는 어설션을 기반으로 테스트를 수행하며, 여기에 추가로 테스트 코드 작성자에게 도움이 될 만한 정보를 알려주는 역할도 함께 수행한다
- 자바스크립트에서 유명한 테스팅 프레임워크는 Jest, Mocha, Karma, Jasmine 등이 있다
- 리액트에서는 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(1, 2)).toBe(3);
});
- assert과 다르게 테스트를 실행하는 콘솔에서 볼 수 있는 테스크 관련 정보가 한층 다양해진다
- 무엇을 테스트했는지, 소요된 시간, 무엇을 성공하고 실패했는지, 전체 결과는 어떤지에 대한 자세한 정보를 확인할 수 있다
8.2.3 리액트 컴포넌트 테스트 코드 작성하기
- 컴포넌트를 렌더링한다
- 필요하다면 컴포넌트에서 특정 액션을 수행한다
- 컴포넌트 렌더링과 2번의 액션을 통해 기대하는 결과와 실제 결과를 비교한다
프로젝트 생성
- create-react-app에는 이미 react-testing-library가 포함돼 있으므로 별도로 설치할 필요가 없다
npx craete-react-app react-test --template typescript
- 위 명령어로 생성된 프로젝트에는 App.text.tsx 파일이 생성되어 있다
import React from "react";
import { render, screen } from "@testing-library/react";
import App from "./App";
test("renders learn react link", () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
- 위 코드 내용은 아래와 같다
<App />을 렌더링한다
- 렌더링하는 컴포넌트 내부에서 "learn react"라는 문자열을 가진 DOM 요소를 찾는다
- expect(linkElement).toBeInTheDocument()라는 어설션을 활용해 2번에서 찾은 요소가 document 내부에 있는지 확인한다
HTML 요소 여부를 확인하는 메소드
- 일반적으로 리액트 컴포넌트 테스트 시나리오는 HTML 요소가 있는지 확인한다. 확인하는 방법 3가지는 아래와 같다
- getBy...: 인수의 조건에 맞는 요소를 반환하며, 해당 요소가 없거나 두 개 이상이면 에러를 발생시킨다. 복수 개를 찾고 싶다면 getAllBy...를 사용하면 된다
- findBy...: getBy...와 거의 유사하나 한 가지 큰 차이점은 Promise를 반환한다는 것이다. 즉, 비동기로 찾는다는 것을 의미하며, 기본값으로 1000ms의 타임아웃을 가지고 있다. 마찬가지로 두 개 이상이면 에러를 발생시키지만 복수 개를 찾고 싶다면 findAllBy...를 사용하면 된다. 이러한 특징 때문에 findBy는 비동기 액션 이후에 요소를 찾을 때 사용한다
- queryBy...: 인수의 조건에 맞는 요소를 반환하는 대신, 찾지 못한다면 null을 반환한다. getBy...와 findBy...는 찾지 못하면 에러를 발생시키기 때문에 찾지 못해도 에러를 발생시키지 않고 싶다면 queryBy..를 사용하면 된다. 마찬가지로 복수 개를 찾았을 때는 에러를 발생시키며, 복수 개를 찾고 싶다면 queryAllBy...를 사용하면 된다
정적 컴포넌트
- 별도의 상태가 존재하지 않아 항상 같은 결과를 반환하는 컴포넌트인 정적 컴포넌트는 테스트를 원하는 컴포넌트를 렌더링한 다음, 테스트를 원하는 요소를 찾아 원하는 테스트를 수행하면 된다.
동적 컴포넌트
- 리액트 테스팅 라이브러리에서 사용자의 입력을 흉내 내고, 또 state의 변화에 따른 컴포넌트의 변화를 테스트할 수 있다
비동기 이벤트가 발생하는 컴포넌트
- jest 등을 활용해서 fetch를 모킹하면 서버에서 오는 응답 시나리오를 모두 테스트하기에는 fetch가 할 수 있는 다양한 일을 일일이 모킹해야 하므로 테스트 코드가 길어지고 유지보수도 어렵다
- 이러한 문제를 해결하기 위해 MSW(Mock Service Worker)를 사용한다
- MSW는 모킹 라이브러리로, 브라우저에서는 서비스 워커를 활용해 실제 네트워크 요청을 가로채는 방식이고, Node.js에서는 https나 XMLHttpRequest의 요청을 가로채는 방식으로 작동한다.
8.2.4 사용자 정의 훅 테스트
- 훅이 들어가 있는 컴포넌트를 만들 경우 : 테스트 코드 작성 외에 작업이 더 추가된다
- 훅이 들어있는 컴포넌트에 대해 별도로 훅에 대한 테스트를 만들 경우 : 해당 훅이 모든 테스트 케이스를 커버하지 못할 경우 또 다른 테스트 가능한 컴포넌트를 찾아야 한다
- react-hooks-testing-library를 활용하면 훅을 편리하게 테스트할 수 있다
- renderHook 함수에서 훅을 편리하게 테스트파기 위한 rerender, unmount 등의 함수도 제공하고 있으므로 사용자 정의 훅을 테스트하고 싶다면 꼭 한번 사용해보자
8.2.5 테스트를 작성하기에 앞서 고려해야 할 점
테스트 커버리지를 맹신하면 안된다
- 흔히들 알고 있는 사실 중 하나는 테스트 커버리지가 높을 수록 좋고 꾸준히 테스트 코드를 작성해야 한다
- 하지만 테스트 커버리지는 단순히 얼마나 많은 코드가 테스트되고 있는지를 나타내는 지표일 뿐, 테스트가 잘되고 있는지를 나타내는 것은 아니다. 그러므로 절대 테스트 커버리지를 맹신해서는 안 된다
실무에서는 테스트 코드를 운영할 만큼 여유롭지 않다
- 그리고 테스트 커버리지를 100%까지 끌어올릴 수 있는 상황은 생각보다 드물다
- TDD라고 하는 개발 방법론을 차용해서 테스트를 우선시하더라도 서버 코드와는 다르게 프론트엔드 코드는 사용자의 입력이 매우 자유롭기 때문에 이러한 모든 상황을 커버해 테스트를 작성하기란 불가능하다
- 테스트를 QA에 의존해 개발을 빠르게 진행해야 할 수도 있고, 이후에 또 개발해야 할 기능이 산적해 있을 수도 있다
애플리케이션에서 가장 취약하거나 중요한 부분을 파악해야 한다
- 테스트 코드를 작성하기 전에 생각해 봐야 할 최우선 과제는 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것이다
- 테스트 코드는 반드시 사용자의 작업과 최대한 유사하게 작성돼야 한다. 예를 들어 결제를 위해 사용자가 입력하는 절차, 장바구니, 주소 입력, 결제까지의 과정을 모두 사용자와 최대한 비슷한 입장에서 테스트를 작성하는 것이 필요하다
- 이처럼 애플리케이션에서 가장 핵심이 되는 부분부터 먼저 테스트 코드를 하나씩 작성해 나가는 것이 중요하다
- 테스트 코드는 소프트웨어 품질에 대한 확신을 얻기 위해 작성하는 것이다
8.2.6 그 밖에 해볼 만한 여러 가지 테스트
- 유닛 테스트 : 각각의 코드나 컴포넌트가 독립적으로 분리된 환경에서 의도된 대로 정확히 작동하는지 검증하는 테스트
- 통합 테스트 : 유닛 테스트를 통과한 여러 컴포넌트가 묶여서 하나의 기능으로 정상적으로 작동하는지 확인하는 테스트
- 엔드 투 엔드(End to End Test) : 흔히 E2E 테스트라 하며, 실제 사용자처럼 작동하는 로봇을 활용해 애플리케이션의 전체적인 기능을 확인하는 테스트