8장. 좋은 리액트 코드 작성을 위한 환경 구축하기
ESLint
ESLint 분석 과정
- 자바스크립트 코드를 문자열로 읽는다
- 자바스크립트 코드를 분석할 수 있는 파서(parser)로 코드를 구조화한다 (→ 이때 espree를 사용한다.)
- 2번에서 구조화한 트리를 AST(Abstract Syntax Tree)라 하며, 이 구조화된 트리를 기준으로 각종 규칙과 대조한다.
- 규칙과 대조했을 때 이를 위반한 코드를 알리거나(report) 수정한다(fix).
코드 분석 도구 express
- ESLint는 JS를 분석하는 파서(parser)로, espree를 사용한다
- 단순히 변수인지, 함수인지 함수명은 무엇인지 등만 파악하는 것이 아니라 코드의 정확한 위치와 같은 아주 세세한 정보도 분석해 알려준다
ESLint 규칙으로, debugger 사용을 금지하는 규칙 생성하기
- ESLint 규칙(rules) : ESLint가 espree로 코드를 분석한 결과를 바탕으로, 어떤 코드가 잘못된 코드이며 어떻게 수정할 지 정하는 것
// no-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 : 해당 규칙과 관련된 메타 정보
- messages : 규칙을 어겼을 때 반환하는 경고 문구
- docs : 문서화에 필요한 정보
- fixable : eslint --fix로 수정했을 때 수정 가능한지 여부
- create : 코드에서 문제점을 확인하는 곳
ESLint 관련 npm 패키지, eslint-plugin
- ESLint rules을 모아놓은 패키지
- 리액트, import와 같이 특정 프레임워크나 도메인과 관련된 규칙을 묶어서 제공하는 패키지
- 규칙 : 접두사를 준수해야 하며, 반드시 한 단어로 구성해야 한다
ESLint 관련 npm 패키지, eslint-config
- eslint-plugin을 한데 묶어서 완벽하게 한 세트로 제공하는 패키지
- 규칙 : 접두사를 준수해야 하며, 반드시 한 단어로 구성해야 한다
ESLint-config 라이브러리, eslint-config-airbnb
- 가장 많이 적용되는 ESLint
ESLint-config 라이브러리, @titicaca/triple-config-kit
- 대부분의 eslint-config가 eslint-config-airbnb를 기반으로 약간의 룰을 수정해 배포되고 있는 것과 다르게 해당 패키지는 자체적으로 정의한 규칙을 기반으로 운영되고 있다
- 외부로 제공하는 규칙에 대한 테스트코드가 존재한다
- CI/CD 환경, 카나리 배포 등 일반적인 npm 라이브러리 구축 및 관리를 위한 시스템이 잘 구축돼 있다
- 별도의 frontend 규칙도 제공하고 있어 Node.js 환경 또는 리액트 환경에 맞는 규칙을 적용할 수 있다는 장점이 있다.
- Prettier와 Stylelint를 각각 별도의 룰인 @titicaca/prettier-config-triple, @titicaca/stylelint-config-triple로 모노레포를 만들어 관리하고 있어 Prettier와 Stylelint도 필요에 따라 설치해서 사용할 수 있다
ESLint-config 라이브러리, eslint-config-next
- 리액트 기반 Next.js 프레임워크를 사용하고 있는 프로젝트에서 사용할 수 있는 eslint-config
- 단순히 자바스크립트 코드를 정적으로 분석할 뿐만 아니라 페이지나 컴포넌트에서 반환하는 JSX 구문 및 _app, _document에서 작성돼 있는 HTML 코드 또한 정적 분석 대상으로 분류해 제공한다
- 이는 단순히 자바스크립트 코드에 대한 향상뿐만 아니라 전체적인 Next.js 기반 웹 서비스의 성능 향상에 도움이 될 수 있다는 점에서 매우 유용하다
import React 제거하기
- 동일한 두 코드에서, import React를 제거하는 것은 여전히 유용하다.
- 웹팩이 트리쉐이킹을 하는 데 걸리는 시간을 그만큼 줄일 수 있기 때문이다.
- 트리쉐이킹에 소요되는 시간이 없어진다면 자연스럽게 빌드 속도 또한 빨라질 것이다.
- 트리쉐이킹 : 코드 어디에서도 사용하지 않는 코드(dead code, 이른바 죽은 코드)를 삭제해서 최종 번들 크기를 줄이는 과정
ESLint 사용 시 주의점
- Prettier와의 충돌
- ESLint는 코드의 잠재적인 문제가 될 수 있는 부분을 분석해 준다면, Prettier는 포매팅과 관련된 작업, 즉 줄바꿈, 들여쓰기, 작은따옴표와 큰따옴표 등을 담당한다
- 즉, ESLint에서도 Prettier에서 처리하는 작업을 처리할 수 있기 때문에 두 가지 모두를 자바스크립트 코드에서 실행한다면 서로 충돌하는 규칙으로 인해 에러가 발생한다.
- 해결 방법 1. 서로 규칙이 충돌되지 않게끔 규칙을 잘 선언하는 것 (ESLint에서 Prettier 규칙을 끈다.)
- 해결 방법 2. 자바스크립트나 타입스크립트는 ESLint에, 그 외의 파일(마크다운, YAML, JSON 등)은 모두 Prettier에 맡긴다. 그 대신 자바스크립트에 추가적으로 필요한 Prettier 관련 규칙은 모두 eslint-plugin-prettier를 사용한다.
규칙에 대한 예외 처리, 그리고 react-hooks/no-exhaustive-deps
- eslint-disable- 주석 : 일부 코드에서 특정 규칙을 임시로 제외시키고 싶을 때 사용하는 주석
- eslint-disable-line no-exhaustive-deps : useEffect나 useMemo와 같이 의존 배열이 필요한 훅에 의존성 배열을 제대로 선언했는지 확인한다.
eslint-disable-line no-exhaustive-deps의 무지성 사용 시, 무엇이 잘못되었는가
- 임의로 판단해 괜찮다고 판단하여 사용하는 것은 대부분의 경우에 위험한 발상이며, 잠재적인 버그를 야기한다.
- 괜찮다고 임의로 판단한 경우 : 해당 변수는 컴포넌트의 상태와 별개로 동작한다는 것을 의미한다. 이 경우에는 해당 변수를 어디서 어떻게 선언할지 다시 고민해봐야 한다.
- 의존성 배열이 너무 긴 경우 : useEffect가 너무 길다는 것. useEffect를 쪼개서 의존성 배열의 가독성과 안정성을 확보해야한다.
- 마운트 시점에 한 번만 실행하고 싶은 경우 : [ ] 배열이 있다는 것은 컴포넌트 상태값과 별개의 부수 효과가 되어 컴포넌트의 상태와 불일치가 일어날 수 있게 된다. 마지막으로, 상태와 관계없이 한 번만 실행돼야 하는 것이 있다면 해당 컴포넌트에 존재할 이유가 없다. 이 경우 적절한 위치로 옮기는 것이 좋다.
ESLint의 버전 충돌
- ESLint의 버전 충돌 시, 에러가 발생한다.
- ESLint 공식 문서에는 ESLint를 peerDependencies로 설정해두라고 권장한다.
- 이러한 문제를 미연에 방지하려면 설치하고자 하는 eslint-config, eslint-plugin이 지원하는 ESLint 버전을 확인하고, 또 설치하고자 하는 프로젝트에서 ESLint 버전을 어떻게 지원하고 있는지 살펴봐야 한다.
리액트 테스트 라이브러리
테스트
- 개발자가 만든 프로그램이 코딩을 한 의도대로 작동하는지 확인하는 일련의 작업
- 처음에 설계한 대로 프로그램이 작동하는지 확인 가능
- 버그를 사전에 방지할 수 있음
- 잘못된 작동으로 인해 발생하는 비용을 줄일 수 있음
- 수정한 내용에 대해서도 예외 케이스가 없고 의도한 대로 작동할 수 있는지 확인 가능
- 사용자에게 버그가 최소화된 안정적인 서비스를 제공
프론트엔드의 테스트
- 백엔드의 테스트와 달리, 일반적인 사용자와 동일하거나 유사한 환경에서 수행된다.
- 주요 비즈니스 로직이나 모든 경우의 수를 고려해야 한다
- 사용자에게 완전히 노출된 영역이므로 최대한 예측해서 확인해야 한다. 사용자는 개발자의 의도대로만 사용하지 않는다.
React Testing Library란?
- DOM Testing Library를 기반으로 만들어진 테스팅 라이브러리
- DOM Testing Library : jsdom을 기반으로 함. → 자바스크립트 환경에서도 HTML을 사용할 수 있으므로 이를 기반으로 DOM Testing Library에서 제공하는 API를 사용해 테스트를 수행할 수 있다.
- 리액트 테스팅 라이브러리를 활용하면 실제로 리액트 컴포넌트를 렌더링하지 않고도, 즉 브라우저를 직접 실행해 눈으로 확인하지 않아도 리액트 컴포넌트가 원하는 대로 렌더링되고 있는지 확인할 수 있다.
- 이 방식은 테스트 환경을 구축하는 데 복잡한 과정을 거치지 않아 간편하고, 테스트에 소요되는 시간 역시 효과적으로 단축시킬 수 있다.
- 그리고 컴포넌트 뿐만 아니라 Provider, 훅 등 리액트를 구성하는 다양한 요소들을 테스트할 수 있다
테스트 코드
- 내가 작성한 코드가 내가 코드를 작성했던 당시의 의도와 목적에 맞는지 확인하는 코드
- Node.js는
assert
라는 모듈을 기본적으로 제공한다. - assert는 테스트 코드가 예상대로 작동한다고 ‘주장’하는 코드를 작성하면 이 코드의 성공 여부에 따라 테스트 통과 또는 실패를 반환한다.
- 테스트 코드와 실제 코드는 파일을 분리해 작성한다
- 어설션(assertion) 라이브러리 : 테스트 결과를 확인할 수 있도록 도와주는 라이브러리. assert, should.js, expect.js, chai 등이 있다.
기본적인 테스트 코드를 작성하는 방식
- 테스트할 함수나 모듈을 정한다
- 함수나 모듈이 반환하길 기대하는 값을 정한다
- 함수나 모듈의 실제 반환 값을 적는다
- 3번의 기대에 따라 2번의 결과가 일치하는지 확인한다
- 기대하는 결과를 반환한다면 테스트는 성공이며, 만약 기대와 다른 결과를 반환하면 에러를 던진다
좋은 테스트 코드는,
- 가능한 한 사람이 읽기 쉽게, 그리고 테스트의 목적이 분명하게 작성되는 것이 중요하다
- 다양한 테스트 코드가 작성되고 통과하는 것뿐만 아니라 어떤 테스트가 무엇을 테스트하는지 일목요연하게 보여주는 것도 중요하다.
- 이러한 테스트의 기승전결을 완성해주는 것이
테스팅 프레임워크
이다
테스팅 프레임워크
- 어설션을 기반으로 테스트를 수행하며, 여기에 추가로 테스트 코드 작성자에게 도움이 될 만한 정보를 알려주는 역할도 함께 수행한다
- Jest, Mocha, Karma, Jasmine 등
Jest 테스트 코드
- Node.js의 assert만 사용했을 때는 단순히 실패에 대해서만 단편적인 정보로 알 수 있었지만 Jest를 비롯한 테스트 프레임워크를 사용하면 무엇을 테스트했는지, 소요된 시간은 어느 정도인지, 무엇이 성공하고 실패했는지, 전체 결과는 어떤지에 대한 자세한 정보를 확인할 수 있다
- test, expect 등의 메서드를 import나 require 같은 모듈을 불러오기 위해 사용하는 구문 없이 바로 사용했다
- node가 아닌 jest(npm run test)로 실행했다. 만약 node로 실행했다면 Jest로 작성한 코드의 test, expect 모두 Node.js 환경의 global, 즉 전역 스코프에 존재하지 않는 메서드이기 때문에 에러가 발생했을 것이다.
- Jest를 비롯한 테스팅 프레임워크에는 이른바 글로벌 (global)이라 해서 실행 시에 전역 스코프에 기본적으로 넣어주는 값들이 있다. 그리고 Jest는 이 값을 실제 테스트 직전에 미리 전역 스코프에 넣어준다. 이렇게 하면 일일이 테스트에 관련한 정보를 임포트하지 않고도 사용할 수 있게 되는 것이다. 이는 간결하고 빠른 테스트 코드 작성에 도움을 준다.
리액트 컴포넌트 테스트
- 컴포넌트를 렌더링한다
- 필요하다면 컴포넌트에서 특정 액션을 수행한다
- 컴포넌트 렌더링과 2번의 액션을 통해 기대하는 결과와 실제 결과를 비교한다
리액트 컴포넌트 테스트에서 특정한 무언가를 지닌 HTML 요소 확인하기
- getBy… : 인수의 조건에 맞는 요소를 반환하며, 해당 요소가 없거나 두 개 이상이면 에러를 발생시킨다.
- findBy… : getBy…와 거의 유사하나 한 가지 큰 차이점은 Promise를 반환한다는 것이다. 즉, 비동기로 찾는다.
- queryBy… : 인수의 조건에 맞는 요소를 반환하는 대신, 찾지 못한다면 null을 반환한다. getBy…와 findBy…는 찾지 못하면 에러를 발생시키기 때문에 찾지 못해도 에러를 발생시키지 않고 싶을 때 사용한다.
OO 컴포넌트 테스트하기
- 정적 컴포넌트
- 별도의 상태가 존재하지 않아 항상 같은 결과를 반환하는 컴포넌트
- 테스트를 원하는 컴포넌트를 렌더링한 다음, 테스트를 원하는 요소를 찾아 원하는 테스트를 수행한다
이때 사용되는
jest의 메서드
- beforeEach : 각 테스트(it)를 수행하기 전에 실행하는 함수
- describe : 비슷한 속성을 가진 테스트를 하나의 그룹으로 묶는 역할. 테스트코드가 많아지고 관리가 어려울 때 사용한다.
- it : test의 축약어
- testId : 리액트 테스팅 라이브러리의 예약어로, get 등의 선택자로 선택하기 어렵거나 곤란한 요소를 선택하기 위해 사용할 수 있다. testId 데이터셋을 선언해두면 이후 테스트 시에 getByTestId, findByTestId 등으로 선택할 수 있다.
- 데이터셋 : HTML의 특정 요소와 관련된 임의 정보를 추가할 수 있는 속성
- 각 테스트를 수행하기 전에 StaticComponent를 렌더링하고, describe로 연관된 테스트를 묶어서 it로 it 함수 내부에 정의된 테스트를 수행한다.
- 동적 컴포넌트
- 사용자가 useState를 통해 입력을 변경하는 컴포넌트
이때 사용되는
jest의 메서드
- setup 함수 : 내부에서 컴포넌트를 렌더링하고, 또 테스트에 필요한 button과 input을 반환한다
- userEvent.type : 사용자가 타이핑하는 것을 흉내내는 메서드. 사용자의 작동을 여러 fireEvent를 통해 좀 더 자세하게 흉내 내는 모듈이다. 대부분의 이벤트를 테스트할 때는 fireEvent로 충분하고 훨씬 더 빠르기 때문에, 특별히 사용자의 이벤트를 흉내내야 할 때만 userEvent를 사용한다.
- jest.spyOn(window, ‘alert’).mockImplementation() : 아래 두 메서드 참고
- jest.spyOn : 어떠한 특정 메서드를 오염시키지 않고 실행이 됐는지, 또 어떤 인수로 실행됐는지 등 실행과 관련된 정보만 얻고싶을 때 사용한다. 단순히 관찰하는 용도.
- mockImplementation : 해당 메서드에 대한 모킹(mocking) 구현을 도와줌. 비록 모의 함수로 구현된 함수이지만 함수가 실행됐는지 등의 정보는 확인할 수 있도록 도와준다.
- 비동기 이벤트가 발생하는 컴포넌트
- MSW(Mock Service Worker) : Node.js나 브라우저에서 모두 사용할 수 있는 모킹 라이브러리
- Node.js나 브라우저에서는 fetch 요청을 하는 것과 동일하게 네트워크 요청을 수행하고, 이 요청을 중간에 MSW가 감지하고 미리 준비한 모킹 데이터를 제공하는 방식이다. 이러한 방식은 fetch의 모든 기능을 그대로 사용하면서도 응답에 대해서만 모킹할 수 있으므로 fetch를 모킹하는 것이 훨씬 수월해진다.
이때 사용되는
MSW의 메서드
- setupServer : 서버를 만드는 역할. 해당 함수 내부에서 Express나 Koa와 비슷하게 라우트를 선언할 수 있다.
- 테스트 코드를 시작하기 전에는 서버를 기동하고, 테스트 코드 실행이 종료되면 서버를 종료시킨다.
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers()); // 앞에서 선언한 setupServer의 기본 설정으로 되돌리는 역할. **서버 실패 테스트** 이후 서버를 초기화할 필요가 있기 때문.
afterAll(() => server.close());
- 원하는 값을 동기 방식으로 즉시 찾는 get 메서드 대신, 요소가 렌더링될 때까지 일정 시간 동안 기다리는 find 메서드를 사용해 요소를 검색한다
- 여기서 중요한 것은 MSW를 사용 한 fetch 응답 모킹과 findBy를 활용해 비동기 요청이 끝난 뒤에 제대로 된 렌더링이 일어났는지 기다린 후에 확인하는 것이다. 이 두 가지만 염두에 둔다면 비동기 컴포넌트 테스트 또한 크게 다를 것이 없다.
- 사용자 정의 훅 테스트하기
- react-hooks-testing-library를 사용해 기존 테스트코드 방식의 불편함을 해결한다
- 매번 테스트가 끝난 후에는 process.env.NODE_ENV를 다시 development로 변경한다. NODE_ENV 할당문을 강제로 작성한 이유는 타입스크립트에서는 NODE_ENV를 읽기 전용 속성으로 간주하기 때문이다.
- 훅을 두 번 연속 실행하는 것을 테스트하기 위해서는 renderHook이 반환하는 객체의 값 중 하나인 rerender 함수를 사용해야 한다.
테스트 작성 시 고려해야 할 점
- 테스트 커버리지 : 해당 소프트웨어가 얼마나 테스트됐는지를 나타내는 지표
- 테스트 커버리지가 높을수록 좋고 꾸준히 테스트 코드를 작성해야 한다.
- 그러나 테스트 커버리지는 단순히 얼마나 많은 코드가 테스트되고 있는지를 나타내는 지표일 뿐, 테스트가 잘되고 있는지를 나타내는 것은 아니다. 그러므로 절대 테스트 커버리지를 맹신해서는 안된다.
- 테스트 커버리지를 100% 끌어올릴 수 있는 상황은 생각보다 드물다.
- TDD(Test Driven Development; 테스트 주도 개발) 개발 방법론을 차용해서 테스트를 우선시하더라도 서버 코드와는 다르게 프론트엔드 코드는 사용자의 입력이 매우 자유롭기 때문에 이러한 모든 상황을 커버해 테스트를 작성하기란 불가능하다.
- 따라서 테스트 코드를 작성하기 전에 생각해봐야 할 최우선 과제는 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것이다.
- 테스트가 이뤄야할 목표는 애플리케이션이 비즈니스 요구사항을 충족하는지 확인하는 것.
테스트 방법들
- 유닛 테스트 : 각각의 코드나 컴포넌트가 독립적으로 분리된 환경에서 의도된 대로 정확히 작동하는지 검증하는 테스트
- 통합 테스트 : 유닛 테스트를 통과한 여러 컴포넌트가 묶여서 하나의 기능으로 정상적으로 작동하는지 확인하는 테스트
- 엔드 투 엔드: 실제 사용자처럼 작동하는 로봇을 활용해 애플리케이션의 전체적인 기능을 확인하는 테스트