JiSoo's Devlog
[모던 리액트 Deep Dive] 8장 본문
ESLint를 활용한 정적 코드 분석
버그와 예기치 못한 작동을 방지하기 위한 방법 중 가장 빠르게 시도해 볼 수 있는 방법은 정적 코드 분석
정적 코드 분석
코드의 실행과는 별개로 코드 그 자체만으로 코드 스멜(잠재적으로 버그를 야기할 수 있는 코드)을 찾아내 문제의 소지가 있는 코드를 사전에 수정하는 것을 의미한다
자바스크립트에서 가장 많이 사용되는 정적 코드 분석 도구가 바로 ESLint
ESLint는 자바스크립트 코드를 정적 분석해 잠재적인 문제를 발견하고 나아가 수정까지 도와주는 도구
ESLint가 코드 분석하는 방법 ↓
1. 자바스크립트 코드를 문자열로 읽는다
2. 자바스크립트 코드를 분석할 수 있는 파서로 코드를 구조화한다
3. 구조화한 트리를 AST라 하며, 이 트리를 기준으로 각종 규칙과 대조한다
4. 규칙과 대조했을 때 이를 위반한 코드를 알리거나 수정한다
자바스크립트를 분석하는 파서에는 여러 가지가 있는데 ESLint는 기본값으로 espree를 사용한다
function hello(str) {}
↓ ↓ ↓
{
"type": "Program",
"start": 0,
"end": 22,
"range": [0, 22],
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
.
.
.
espree로 분석하면 JSON 형태로 구조화된 결과를 얻을 수 있다
espree나 다른 파서로 자바스크립트/타입스크립트 코드를 분석해 보고 싶으면 AST explorer를 방문해 확인하면 된다
JSON으로 생성된 트리에 다양한 정보가 담겨 있다
코드 분석 도구는 단순히 변수인지, 함수인지 등만 파악하는 것이 아니라 코드의 정확한 위치와 같은 세세한 정보도 분석해 알려준다
타입스크립트에도 @typescript-eslint/typescript-espree라고 하는 espree기반 파서가 있으며 이를 통해 타입스크립트 코드를 분석해 구조화한다
ESLint가 espree로 코드를 분석한 결과를 바탕으로 어떤 코드가 잘못된 코드이고 어떻게 수정해야 할지 정해야 하는데
이게 ESLint 규칙이라고 하고 특정한 규칙의 모음을 plugins라고 한다
eslint-plugin
eslint-plugin이라는 접두사로 시작하는 플러그인은 리액트, import와 같이 특정 프레임워크나 도메인과 관련된 규칙을 묶어 제공하는 패키지
eslint-plugin-react는 리액트 관련 규칙 제공
eslint-config
eslint-config는 eslint-plugin을 한데 묶어 완벽하게 한 세트로 제공하는 패키지
eslint-plugin과 eslint-config의 네이밍 관련 규칙
eslint-plugin, eslint-config라는 접두사를 준수해야 하며 반드시 한 단어로 구성해야 한다
ex) eslint-plugin-naver-financials는 불가능
일반적으로는 일부 IT 기업들에서 공개한 잘 만들어진 eslint-config를 설치해 사용하는 것이 일반적
eslint-config-airbnb
@titicaca/triple-config-kit
eslint-config-next : 리액트 기반 Next.js 프레임워크를 사용하고 있는 프로젝트에서 사용할 수 있는 eslint-config
※ 주의할 점
Prettier와의 충돌
Prettier는 코드의 포매팅을 도와주는 도구
ESLint와 마찬가지로 코드를 정적 분석해 문제를 해결한다는 점은 동일하지만 지향하는 목표가 다르다
ESLint는 코드의 잠재적인 문제가 될 수 있는 부분을 분석해 준다면 Prettier는 포매팅과 관련된 작업을 담당한다
자바스크립트에서만 작동하는 ESLint와 다르게 HTML, CSS, 마크다운 등 다양한 언어에도 적용 가능
ESLint에서도 Prettier에서 처리하는 작업을 처리할 수 있기 때문에 두 가지 모두를 자바스크립트 코드에서 실행한다면 서로 충돌하는 규칙으로 인해 에러가 발생하고 최악의 경우 두 개 모두 만족하지 못하는 코드가 만들어질 수 있다
해결하기 위한 방법
1. 서로 규칙이 충돌되지 않게 규칙을 잘 선언하기
Prettier에서 제공하는 규칙을 어기지 않도록 ESLint에서는 해당 규칙을 끄는 방법이다
이 경우 코드에 ESLint를 적용하는 작업과 코드를 포매팅하는 작업이 서로 다른 패키지에서 발생하게 된다
2. 자바스크립트나 타입스크립트는 ESLint에, 그 외의 파일은 모두 Prettier에 맡기는 것
그 대신 자바스크립트에 추가적으로 필요한 Prettier 관련 규칙은 모두 eslint-plugin-prettier를 사용
이렇게 서로 관여하는 파일을 물리적으로 분리하면 코드 충돌의 위험은 없애고 Prettier가 제공하는 모든 규칙을 사용할 수 있다
규칙에 대한 예외 처리, 그리고 react-hooks/no-exhaustive-deps
만약 일부 코드에서 특정 규칙을 임시로 제외시키고 싶다면 eslint-disable- 주석을 사용하면 된다
특정 줄만 제외하거나 파일 전체를 제외하거나, 특정 범위에 걸쳐 제외하는 것이 가능하다
// 특정 줄만 제외
console.log('hello world') //eslint-disable-line no-console
// 다음 줄 제외
// eslint-disable-next-line no-console
console.log('hello world')
// 특정 여러 줄 제외
/* eslint-disable no-console */
console.log('JavaScript debug log')
console.log('eslint is disabled now')
/* eslint-enable no-console */
// 파일 전체에서 제외
/* eslint-disable no-console */
console.log('hello world')
정말로 필요 없는 규칙이라면 "off"를 사용해 끄는 것이 옳다
eslint-disable을 많이 사용하고 있다면 그렇게 무시하는 것이 옳은지, 해당 규칙을 제거하는 게 옳은지 꼭 점검해 봐야 한다
ESLint 버전 충돌
설치하고자 하는 eslint-config, eslint-plugin이 지원하는 ESLint 버전을 확인하고 설치하고자 하는 프로젝트에서 ESLint 버전을 어떻게 지원하고 있는지 살펴봐야 한다
두 ESLint 버전을 모두 충족시킬 수 있는 환경을 만들어두고 설치하는 것이 좋다
이런 준비를 제대로 하지 않는다면 ESLint를 사용할 때마다 버전이 맞지 않는다는 오류 메시지를 마주하게 될 수도 있다
React Testing Library란?
React Testing Library는 DOM Testing Library를 기반으로 만들어진 테스팅 라이브러리로 리액트를 기반으로 한 테스트를 수행하기 위해 만들어졌다
DOM Testing Library는 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) // 'Hello world'
jsdom을 사용하면 마치 HTML이 있는 것처럼 DOM을 불러오고 조작할 수 있다
리액트 테스팅 라이브러리를 활용하면 실제로 리액트 컴포넌트를 렌덜이하지 않고도, 즉 브라우저를 직접 실행해 눈으로 확인하지 않아도 리액트 컴포넌트가 원하는 대로 렌더링 되고 있는지 확인할 수 있다
굳이 테스트 환경을 구축하는 데 복잡한 과정을 거치지 않아 간편하고 테스트에 소요되는 시간 역시 효과적으로 단축 가능하다
테스트 코드란 내가 작성한 코드가 내가 코드를 작성했던 당시의 의도와 목적에 맞는지 확인하는 코드를 의미한다
// 테스트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}`)
}
테스트 코드 작성 방식
1. 테스트할 함수나 모듈 선정
2. 함수나 모듈이 반환하길 기대하는 값 적기
3. 함수나 모듈의 실제 반환 값 적기
4. 3번의 기대에 따라 2번의 결과가 일치하는지 확인
5. 기대하는 결과를 반환한다면 테스트는 성공, 만약 기대와 다른 결과를 반환하면 에러 던지기
일반적으로 테스트 코드와 실제 코드는 분리해서 작성한다
테스트 결과를 확인할 수 있도록 도와주는 라이브러리를 어설션 라이브러리라고 한다
어설션 라이브러리는 단순히 동등을 비교하는 equal 외에도 객체 자체가 동일한지 확인하는 deepEqual, 같지 않은지 비교하는 notEqual, 에러를 던지는지 여부를 확인하는 throws 등 다양한 메서드를 제공한다
테스트 코드는 가능한 한 사람이 읽기 쉽게, 테스트의 목적이 분명하게 작성되는 것이 중요!
테스트의 기승전결을 완성해 주는 것이 테스팅 프레임워크
테스팅 프레임워크들은 어설션을 기반으로 테스팅을 수행하며 추가로 테스트 코드를 작성자에게 도움이 될 만한 정보를 알려주는 역할도 함께 수행한다
리액트 진영에서는 리액트와 마찬가지로 메타에서 작성한 오픈소스 라이브러리인 Jest가 널리 쓰이고 있다
Jest의 경우 자체적으로 제작한 expect 패키지를 사용해 어설션을 수행한다
리액트 컴포넌트 테스트 코드 작성하기
1. 컴포넌트를 렌더링한다
2. 필요하다면 컴포넌트에서 특정 액션을 수행한다
3. 컴포넌트 렌더링과 2번의 액션을 통해 기대하는 결과와 실제 결과를 비교한다
프로젝트 생성
npx create-react-app react-test --template typescript
create-react-app에는 이미 react-testing-library가 포함돼 있어 별도로 설치할 필요 없다
이렇게 생성된 프로젝트를 보면 App.test.tsx 파일이 생성돼 있다
App.test.tsx가 App.tsx에서 테스트하는 내용
1. <App />을 렌더링 한다
2. 렌더링 하는 컴포넌트 내부에서 'learn react'라는 문자열을 가진 DOM 요소를 찾는다
3. expect(linkElement).toBeInTheDocument()라는 어설션을 활용해 2번에서 찾는 요소가 document 내부에 있는지 확인한다
리액트 컴포넌트에서 테스트하는 일반적인 시나리오는 특정한 무언가를 지닌 HTML 요소가 있는지 여부이다
이를 확인하는 방법 ↓
- getBy... : 인수의 조건에 맞는 요소 반환, 해당 요소가 없거나 두 개 이상이면 에러 발생, 복수 개를 찾고 싶으면 getAllBy... 사용
- findBy... : getBy...와 거의 유사하나 Promise를 반환한다는 차이점이 있다. 즉, 비동기로 찾는다는 것을 의미하며 기본값으로 1000ms의 타임아웃을 가지고 있다
- queryBy... : 인수의 조건에 맞는 요소를 반환하는 대신, 못 찾으면 null 반환, 찾지 못해도 에러를 발생시키고 싶지 않다면 사용하면 된다
정적 컴포넌트
별도의 상태가 존재하지 않아 항상 같은 결과를 반환하는 컴포넌트를 테스트하는 방법은 어렵지 않다
테스트를 원하는 컴포넌트를 렌더링 한 다음, 테스트를 원하는 요소를 찾아 원하는 테스트를 수행하면 된다
- beforeEach : 각 테스트를 수행하기 전에 실행하는 함수로 각 테스트를 실행하기에 앞서 Static Component를 렌더링 한다
- describe : 비슷한 속성을 가진 테스트를 하나의 그룹으로 묶는 역할을 한다
- it : test와 완전히 동일하며 test의 축약어
- testId : 리액트 테스팅 라이브러리의 예약어로 get 등의 선택자로 선택하지 어렵거나 곤란한 요소를 선택하기 위해 사용할 수 있다
동적 컴포넌트
사용자가 useState를 통해 입력을 변경하는 컴포넌트
- setup 함수 : setup 함수는 내부에서 컴포넌트를 렌더링 하고 테스트에 필요한 button과 input을 반환한다
- userEvent.type : 사용자가 타이핑하는 것을 흉내해는 메서드
- jest.spyOn : Jest가 제공하는 spyOn은 실행과 관련된 정보만 얻고 싶을 때 사용
- mockImplementation : 해당 메서드에 대한 모킹 구현을 도와준다
비동기 이벤트가 발생하는 컴포넌트
MSW는 Node.js나 브라우저에서 모두 사용할 수 있는 모킹 라이브러리로 브라우저에서는 서비스 워커를 활용해 실제 네트워크 요청을 가로채는 방식으로 모킹을 구현한다
Node.js 환경에서는 https나 XMLHttpRequest의 요청을 가로채는 방식으로 작동한다
사용자 정의 훅 테스트하기
react-hooks-testing-library를 활용하면 훅을 더욱 편리하게 테스트할 수 있다
useEffectDebugger 훅은 컴포넌트명과 props를 인수로 받아 해당 컴포넌트가 어떤 props의 변경으로 인해 리렌더링 됐는지 확인해 주는 일종의 디버거 역할을 한다
- 최초 컴포넌트 렌더링 시에는 호출하지 않는다
- 이전 props를 useRef에 저장해 두고, 새로운 props를 넘겨받을 때마다 이전 props와 비교해 무엇이 렌더링을 발생시켰는지 확인한다
- 이전 props와 신규 props의 비교는 리액트의 원리와 동일하게 Object.is를 활용해 얕은 비교를 수행한다
- propcess.env.NODE_ENV === 'production' 인 경우에는 로깅을 하지 않는다
react-hooks-library를 사용하면 굳이 테스트를 위한 컴포넌트를 만들지 않아도 훅을 간편하게 테스트할 수 있다
또한 renderHook 함수에서 훅을 편리하게 테스트하기 위한 rerender, unmount 등의 함수도 제공하고 있다
테스트 커버리지는 해당 소프트웨어가 얼마나 테스트됐는지를 나타내는 지표
테스트 커버리지가 높을수록 좋고 꾸준히 테스트 코드를 작성하라는 것이다
테스트 커버리지를 100%까지 끌어올릴 수 있는 상황은 생각보다 드물다
실무에서는 테스트 코드를 작성하고 운영할 만큼 여유로운 상황이 많이 없다
그렇기 때문에 테스트 코드를 작성하기 전에 생각해 봐야 할 최우선 과제는 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것
그밖에 해볼 만한 여러 테스트
- 유닛 테스트 : 각각의 코드나 컴포넌트가 독집적으로 분리된 환경에서 의도된 대로 정확히 작동하는지 검증하는 테스트
- 통합 테스트 : 유닛 테스트를 통과한 여러 컴포넌트가 묶여 하나의 기능으로 정상적으로 작동하는지 확인하는 테스트
- 엔드 투 엔드 : 실제 사용자처럼 작동하는 로봇을 활용해 애플리케이션의 전체적인 기능을 확인하는 테스트
유닛 테스트에서 엔드 투 엔드 테스트로 갈수록 테스트가 실패할 지점이 많아지고 테스트 코드도 복잡해지며 테스트해야 할 경우의 수도 많아지고 테스트 자체를 구축하는 것도 어려워진다
하지만 뒤로 갈수록 개발자에게 있어 코드에 대한 자신감을 심어줄 수 있는 가능성 또한 커진다
'Frontend > React' 카테고리의 다른 글
[Udemy React 완벽 가이드] 리액트 쿼리/Tanstack 쿼리 (0) | 2024.06.01 |
---|---|
[Udemy React 완벽 가이드] HTTP 요청 보내기 (0) | 2024.05.30 |
[모던 리액트 Deep Dive] 7장 (0) | 2024.05.23 |
[모던 리액트 Deep Dive] 6장 (0) | 2024.05.19 |
[모던 리액트 Deep Dive] 5장 (0) | 2024.05.18 |