8.1 ESLint를 활용한 정적 코드 분석
8.1.1 ESLint 살펴보기
ESLint는 어떻게 코드를 분석할까?
- 자바스크립트 코드를 문자열로 읽는다.
- 자바스크립트 코드를 분석할 수 있는 파서로 코드를 구조화한다.
- 2번에서 구조화한 트리 AST를 기준으로 각종 규칙과 대조한다.
- 규칙과 대조했을 때 이를 위반한 코드를 알리거나 수정한다.
ESLint는 파서 espree를 사용하여 자바스크립트 코드를 분석한다. AST 트리 예시를 확인하고 싶다면 아래 사이트를 참조해보자.
no-debugger 규칙
Eslint에는 debugger; 선언을 금지한 규칙이 존재한다. 해당 규칙은 다음과 같이 작성되어있다.
module.exports = {
meta: {
type: 'problem',
},
docs: {
description: 'Disallow the use of `debugger`',
recommended: true,
url: '<https://eslint.org/docs/rules/no-debugger>',
},
fixable: null,
schema: [],
message: {
unexpected: "Unexpected 'debugger' statement.",
},
create(context) {
return {
DebuggerStatement(node) {
context.report({
node,
messageId: 'unexpected',
});
},
};
},
};
meta, docs에는 해당 문법에 대한 설명이 적혀있다.
create에 있는 함수에서 자바스크립트 코드의 AST를 순회하여 DebuggerStatement를 만나면 unexpected error를 리포트한다.
/Users/5kdk/sample/index.tsx
16:1 error Unexpected 'debugger' statement no-debugger
8.1.2 eslint-plugin과 eslint-config
eslint-plugin
이 접두사로 시작하는 플러그인은 규칙을 모아놓은 패키지이다.
eslint-plugin-import: import와 관련된 다양한 규칙을 제공
eslint-plugin-react: react 관련 다양한 규칙을 제공
ㄴ 예시) jsx 배열에 key가 누락된 경우를 경고해준다.
eslint-config
eslint-plugin을 한데 묶어서 한 세트로 제공하는 패키지
IT기업에서 공개한 잘 만들어진 이미 존재하는 eslint-config를 설치해서 빠르게 적용할 수 있다.
ex) eslint-config-airbnb, @titicaca/triple-config-kit, eslint-config-next
8.1.3 나만의 ESLint 규칙 만들기
1. 이미 존재하는 규칙을 커스터마이징해서 적용하기: import React를 제거하기 위한 ESLint 규칙 만들기
리액트 17버전부터는 import React 구문이 필요가 없어지면서, 이를 삭제하여 번들러의 크기를 줄일 수 있다. 빌드를 해보면 웹팩에서 제공하는 트리쉐이킹 기능을 통해 삭제되는 것을 확인할 수 있고, 트리쉐이킹에 소요되는 시간을 줄어 빌드 속도 효율화를 위해서라도 제거하는 것이 좋다.
기존에 존재하는 no-restricted-imports라는 규칙 (어떠한 모듈을 import 하는 것을 금지) 을 활용한다.
module.exports = {
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
name: "react",
importNames: ["default"],
message:
"import React from 'react'는 react 17부터 더 이상 필요하지 않습니다.",
},
],
},
],
},
};
2. 완전히 새로운 규칙 만들기: new Date를 금지시키는 규칙
new Date()를 사용하여 시간을 생성할 때는 기기의 타임존에 따라 다른 시간을 리턴한다. 이는 기기 의존적이므로 시간을 서버에 의존하도록 해야할 경우 해당 규칙이 유용할 수 있다.
이 때, new Date를 AST 트리로 변환해보면 new라는 코드는 NewExpression type과 매칭되는 구문이라는 것을 확인해볼 수 있다. 이를 활용하면 다음과 같이 new Date()를 ServerDate()로 변환하는 규칙 코드를 작성할 수 있다.
/**
* @type {import('eslint').Rule.RuleModule}
*/
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow the use of the new Date()",
recommended: false,
},
fixable: "code",
schema: [],
messages: {
message: "new Date() is not allowed",
},
},
create: function (context) {
return {
// NewExpression이라고 하는 타입의 선택자를 키로 선언해서 new 생성자를 사용할 때 ESLint가 실행되게 함
NewExpression(node) {
// 해당 node를 기준으로 찾고자 하는 생성자인지 검증
if (node.callee.name === "Date" && node.arguments.length === 0) {
context.report({
node,
messageId: "message",
fix: function (fixer) {
// ServerDate() 함수로 대체하는 코드
return fixer.replaceText(node, "ServerDate()");
},
});
}
},
};
},
};
이렇게 작성한 규칙들은 eslint-plugin 형태로 묶음으로만 배포할 수 있다.
8.1.4 주의할 점
Eslint와 Prettier가 지향하는 목표
- Eslint는 코드의 잠재적인 문제가 될 수 있는 부분을 분석해준다.
- Prettier는 포매팅과 관련된 작업(줄바꿈, 들여쓰기, 큰따옴표)를 담당한다.
둘의 충돌을 해결하는 방법
- Prettier에서 제공한느 규칙을 어기지 않도록 Eslint에서는 해당 규칙을 끈다.
- 자바스크립트나 타입스크립트는 ESLint에, 그 외의 파일은 모두 Prettier에게 맡긴다. 그 대신 자바스크립트에 추가적으로 필요한 Prettier 관련 규칙은 eslint-plugin-prettier를 사용한다.
규칙에 대한 예외 처리, 그리고 react-hooks/no-exhausitve-deps
eslint-disable- 주석을 사용하면 특정 규칙을 임시로 제외할 수 있다. 그러나 이는 잠재적인 버그를 야기할 수 있다. 모든 규칙은 존재하는 이유가 있으며, 정말로 필요 없는 규칙이라면 ‘off’를 사용해 끄는 것이 옳다.
eslint-disable을 많이 사용하고 있다면 그렇게 무시하는 것이 옳은지, 아니면 해당 규칙을 제거하는 것이 옳은지 꼭 점검해 봐야 한다.
8.1.5 정리
ESLint는 이제 기본이 되어 create-react-app, create-next-app을 설치하면 기본적으로 ESLint도 함께 제공되는 등, 코딩 스타일을 유지할 수 있도록 도와준다.
ESLint에 포함된 대부분의 규칙을 숙지하고 공감한다면 본인만의 혹은 조직에서 사용할 eslint-config를 만들어서 코드 스멜을 제거하고, 프론트엔드 조직을 탄탄하게 다져보자.
8.2 리액트 팀이 권장하는 리액트 테스트 라이브러리
테스트는 버그를 방지하고, 수정 내용에 대해서도 의도한 대로 작동하는 지 확인할 수 있게 한다. 또한, 사용자에게 버그가 최소화된 안정적인 서비스를 제공할 수 있는 원동력이 된다.
백엔드에서는 데이터를 올바르게 가져오는 지, 수정 간 교착 상태가 발생하지 않는 지, 데이터 손실이 없는 지 등을 테스트한다. 반면 프론트엔드는 사용자가 프로그램에서 수행할 주요 비즈니스 로직이나 모든 경우의 수를 고려해야 한다.
단순히 함수나 컴포넌트 수준에서 유닛 테스트를 할 수도 있고, 사용자가 하는 작동을 모두 흉내 내서 테스트할 수도 있다.
8.2.1 React Testing Library란?
React Testing Library는 DOM Testing Library를 기반으로 만들어진 테스팅 라이브러리다.
jsdom을 사용하여 마치 HTML이 있는 것처럼 DOM을 조작할 수 있다. 따라서 실제로 리액트 컴포넌트를 렌더링하지 않고도 원하는대로 렌더링 되고 있는 지 확인할 수 있다.
8.2.2 자바스크립트 테스트의 기초
assertion 라이브러리 사용 ⇒ 작성한 코드가 예상대로 작동하는 지에 대한 통과 또는 실패를 반환한다.
Jest와 같은 테스팅 프레임워크를 사용하면 이 뿐만 아니라 도움이 될 만한 정보를 함께 알려준다.
ㄴ 테스트에 소요된 시간
ㄴ 전체 결과에 대한 자세한 정보
test('두 인수가 덧셈이 되어야 한다', () => {
expect(sum(1,2)).toBe(3)
})
8.2.3 리액트 컴포넌트 테스트 코드 작성하기
- getBy: 인수의 조건에 맞는 요소 1개를 반환, 요소가 없거나 두 개 이상이면 에러 발생
- findBy: Promise를 반환하여 1000ms의 타임아웃을 기본적으로 가지고 있다.
- queryBy: 인수의 조건에 맞는 요소를 반환하는 대신 찾지 못한다면 null을 반환한다.
정적 컴포넌트 테스트하기
import { render, screen } from "@testing-library/react";
import StaticComponent from "./index";
beforeEach(() => {
render();
});
describe("링크 확인", () => {
it("링크가 3개 존재한다.", () => {
const ul = screen.getByTestId("ul");
expect(ul.children.length).toBe(3);
});
it("링크 목록의 스타일이 square이다.", () => {
const ul = screen.getByTestId("ul");
expect(ul).toHaveStyle("list-style-type: square");
});
});
describe("리액트 링크 테스트", () => {
it("리액트 링크가 존재한다.", () => {
const reactLink = screen.getByText("리액트");
expect(reactLink).toBeVisible();
});
it("리액트 링크가 올바른 주소로 존재한다.", () => {
const reactLink = screen.getByText("리액트");
expect(reactLink.tagName).toEqual("A");
expect(reactLink).toHaveAttribute("href", "<https://reactjs.org/>");
});
//...
});
동적 컴포넌트 테스트하기
사용자의 키보드 입력을 받아 아를 alert로 띄우는 button
userEvent.type
사용자가 타이핑하는 것을 흉내 내는 메서드
userEvent.click을 수행하면 내부적으로 fireEvent.mouseOvr, fireEvent.mosueMove, fireEvent.mouseDown, fireEvent.mouseUp, fireEvent.click을 실행하여 작동을 흉내낸다.
jest.spyOn(window, ‘alert’)
spyOn은 특정 메서드의 동작을 오염시키지 않고, 실행이 됐는 지, 어떤 인수로 실행됐는 지 실행과 관련된 정보를 얻고 싶을 때 사용한다.
const spyFn = jest.spyOn(calc, 'add');
const result = calc.add(1, 2);
expect(spyFn).toBeCalledTiems(1);
expect(spyFn).toBeCalledWith(1, 2);
expect(result).toBe(3);
mockImplementation
모킹 구현을 통해 Node.js에 존재하지 않는 window.alert를 테스트할 수 있도록 한다.
const alertMock = jest
.spyOn(window, 'alert')
.mockImplementation((_: string) => undefined)
비동기 이벤트가 발생하는 컴포넌트 테스트하기
API 응답에 따라 다른 렌더링의 결과를 가지는 컴포넌트를 테스트할 때에는, fetch 응답에 대한 모킹이 필요하다.
- jest.spyOn을 사용
- MSW를 사용
jest.spyOn(window, "fetch")
.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(MOCK_TODO_RESPONSE),
})
) as jest.Mock
);
import { rest } from "msw";
import { setupServer } from "msw/node";
import { fireEvent, render, screen } from "@testing-library/react";
import {FetchComponent} from "./index";
const MOCK_TODO_RESPONSE = {
userId: 1,
id: 1,
title: "delectus aut autem",
completed: false,
}
const server = setupServer(
rest.get("/todos/:id", (req, res, ctx) => {
const todoId = req.params.id
if(Number(todoId)) {
return res(
ctx.json({...MOCK_TODO_RESPONSE, id: Number(todoId)})
)
}else{
return res(
ctx.status(404)
)
}
})
)
// 테스트 코드를 시작하기 전에 서버를 가동한다.
beforeAll(() => server.listen())
// setupServer의 기본 설정으로 되돌린다.
// 503을 리턴하도록 덮어씌운 작업을 되돌린다.
afterEach(() => server.resetHandlers())
// 테스트 코드 실행이 종료되면 서버를 종료시킨다.
afterAll(() => server.close())
beforeEach(() => {
render(<FetchComponent />)
})
// 테스트 코드
describe("FetchComponent 테스트", () => {
it("버튼을 클릭하면 fetch 요청을 보낸다.", async () => {
const button = screen.getByRole("button", {name: /1번/})
fireEvent.click(button)
// 요소가 렌더링될 때까지 일정 시간동안 기다리는 find 메서드
const data = await screen.findByText(MOCK_TODO_RESPONSE.title)
expect(data).toBeInTheDocument()
})
it("버튼을 클릭하고 서버 요청에서 에러가 발생하면 에러 문구를 노출한다.", async () => {
// 서버 기본 작업을 덮어씌운다.
server.use(
rest.get("/todos/:id", (req, res, ctx) => {
return res(
ctx.status(503)
)
})
)
const button = screen.getByRole("button", {name: /1번/})
fireEvent.click(button)
const error = await screen.findByText(/에러가 발생했습니다./)
expect(error).toBeInTheDocument()
})
})
8.2.4 사용자 정의 훅 테스트하기
훅에 대한 모든 테스트 케이스를 커버하도록 하고, 더 쉽게 테스트할 수 있도록 react-hooks-testing-library를 사용할 수 있다.
renderHook, rerender, unmount등의 함수를 제공한다.
사용자 정의 훅 테스트 예시
// useCounter.js
import { useState } from 'react';
const useCounter = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return { count, increment, decrement };
};
export default useCounter;
// useCounter.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';
test('useCounter 훅이 정상적으로 동작하는지 확인', () => {
const { result, rerender, unmount } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(0);
// 훅을 다시 렌더링하여 상태가 초기화되는지 확인
rerender();
expect(result.current.count).toBe(0);
// 훅을 언마운트하여 정리되는지 확인
unmount();
});
8.2.5 테스트를 작성하기에 앞서 고려해야 할 점
테스트 커버리지가 만능은 아니다. 테스트 커버리지는 단순히 얼마나 많은 코드가 테스트되고 있는지를 나타내는 지표일 뿐, 테스트가 잘되고 있는지를 나타내는 것이 아니다. 그러므로 절대 테스트 커버리지를 맹신해서는 안된다. 또한, TDD를 사용하더라고 하더라도 테스트 커버리지를 100%까지 끌어올릴 수 있는 상황은 드물다. 서버와는 다르게 프런트엔드 코드는 사용자의 입력이 매우 자유롭기 때문에 모든 상황을 커버해 작성하기란 불가능하다. 때로는 QA에 의존해 개발을 빠르게 해야할 수 있다.
최우선 과제는 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것이다. 과제에서의 중요 우선순위를 가린다면, 그 시나리오에 집중하여 가장 핵심이 되는 부분부터 먼저 테스트 코드를 하나씩 작성해 나가는 것이 중요하다. 테스트 코드는 개발자가 단순 코드 작성만으로는 쉽게 이룰 수 없는 목표인 소프트웨어 품질에 대한 확신을 얻기 위해 작성하는 것이다.
8.2.6 그밖에 해볼 만한 여러 가지 테스트
Unit Test
각각의 코드나 컴포넌트가 독립적으로 분리된 환경에서 의도된 대로 정확히 작동하는지 검증하는 테스트
통합 테스트
유닛 테스트를 통과한 여러 컴포넌트가 묶여서 하나의 기능으로 정상적으로 작동하는지 확인하는 테스트
End to End Test
실제 사용자처럼 작동하는 로봇을 활용해 애플리케이션의 전체적인 기능을 확인하는 테스트
8.2.7 정리
테스트할 수 있는 방법은 여러 가지가 있지만, 테스트가 이뤄야 할 목표는 애플리케이션이 비즈니스 요구사항을 충족하는 지 확인하는 것 한 가지뿐이다. 처음부터 E2E 테스팅 라이브러리를 사용한다면 테스트 코드를 작성하기도 전에 지치거나 다른 급한 밀에 우선순위가 밀려날지도 모른다. 조금씩, 핵심적인 부분부터 테스트 코드를 작성하다 보면 소프트웨어의 품질에 확신을 갖게 될 것이다.
'책정리 > 모던 리액트 Deep Dive' 카테고리의 다른 글
12장 모든 웹 개발자가 관심을 가져야 할 핵심 웹 지표 (0) | 2024.06.28 |
---|---|
10장 리액트 17과 18의 변경 사항 살펴보기 (1) | 2024.06.03 |
3장 리액트 훅 깊게 살펴보기 (0) | 2024.05.15 |
모던 리액트 Deep Dive 2.1 - 2.2 :: JSX와 리액트 파이버 (0) | 2024.02.27 |
자바스크립트의 동등 비교 (==, ===, Object.is) (0) | 2024.02.12 |