김은정
Study

8장. 좋은 리액트 코드 작성을 위한 환경 구축하기

ESLint

ESLint 분석 과정

  1. 자바스크립트 코드를 문자열로 읽는다
  2. 자바스크립트 코드를 분석할 수 있는 파서(parser)로 코드를 구조화한다 (→ 이때 espree를 사용한다.)
  3. 2번에서 구조화한 트리를 AST(Abstract Syntax Tree)라 하며, 이 구조화된 트리를 기준으로 각종 규칙과 대조한다.
  4. 규칙과 대조했을 때 이를 위반한 코드를 알리거나(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 사용 시 주의점

  1. 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 등이 있다.

기본적인 테스트 코드를 작성하는 방식

  1. 테스트할 함수나 모듈을 정한다
  2. 함수나 모듈이 반환하길 기대하는 값을 정한다
  3. 함수나 모듈의 실제 반환 값을 적는다
  4. 3번의 기대에 따라 2번의 결과가 일치하는지 확인한다
  5. 기대하는 결과를 반환한다면 테스트는 성공이며, 만약 기대와 다른 결과를 반환하면 에러를 던진다

좋은 테스트 코드는,

  • 가능한 한 사람이 읽기 쉽게, 그리고 테스트의 목적이 분명하게 작성되는 것이 중요하다
  • 다양한 테스트 코드가 작성되고 통과하는 것뿐만 아니라 어떤 테스트가 무엇을 테스트하는지 일목요연하게 보여주는 것도 중요하다.
  • 이러한 테스트의 기승전결을 완성해주는 것이 테스팅 프레임워크이다

테스팅 프레임워크

  • 어설션을 기반으로 테스트를 수행하며, 여기에 추가로 테스트 코드 작성자에게 도움이 될 만한 정보를 알려주는 역할도 함께 수행한다
  • 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는 이 값을 실제 테스트 직전에 미리 전역 스코프에 넣어준다. 이렇게 하면 일일이 테스트에 관련한 정보를 임포트하지 않고도 사용할 수 있게 되는 것이다. 이는 간결하고 빠른 테스트 코드 작성에 도움을 준다.

리액트 컴포넌트 테스트

  1. 컴포넌트를 렌더링한다
  2. 필요하다면 컴포넌트에서 특정 액션을 수행한다
  3. 컴포넌트 렌더링과 2번의 액션을 통해 기대하는 결과와 실제 결과를 비교한다

리액트 컴포넌트 테스트에서 특정한 무언가를 지닌 HTML 요소 확인하기

  • getBy… : 인수의 조건에 맞는 요소를 반환하며, 해당 요소가 없거나 두 개 이상이면 에러를 발생시킨다.
  • findBy… : getBy…와 거의 유사하나 한 가지 큰 차이점은 Promise를 반환한다는 것이다. 즉, 비동기로 찾는다.
  • queryBy… : 인수의 조건에 맞는 요소를 반환하는 대신, 찾지 못한다면 null을 반환한다. getBy…와 findBy…는 찾지 못하면 에러를 발생시키기 때문에 찾지 못해도 에러를 발생시키지 않고 싶을 때 사용한다.

OO 컴포넌트 테스트하기

  1. 정적 컴포넌트
  • 별도의 상태가 존재하지 않아 항상 같은 결과를 반환하는 컴포넌트
  • 테스트를 원하는 컴포넌트를 렌더링한 다음, 테스트를 원하는 요소를 찾아 원하는 테스트를 수행한다

이때 사용되는 jest의 메서드

  • beforeEach : 각 테스트(it)를 수행하기 전에 실행하는 함수
  • describe : 비슷한 속성을 가진 테스트를 하나의 그룹으로 묶는 역할. 테스트코드가 많아지고 관리가 어려울 때 사용한다.
  • it : test의 축약어
  • testId : 리액트 테스팅 라이브러리의 예약어로, get 등의 선택자로 선택하기 어렵거나 곤란한 요소를 선택하기 위해 사용할 수 있다. testId 데이터셋을 선언해두면 이후 테스트 시에 getByTestId, findByTestId 등으로 선택할 수 있다.
  • 데이터셋 : HTML의 특정 요소와 관련된 임의 정보를 추가할 수 있는 속성
  • 각 테스트를 수행하기 전에 StaticComponent를 렌더링하고, describe로 연관된 테스트를 묶어서 it로 it 함수 내부에 정의된 테스트를 수행한다.
  1. 동적 컴포넌트
  • 사용자가 useState를 통해 입력을 변경하는 컴포넌트

이때 사용되는 jest의 메서드

  • setup 함수 : 내부에서 컴포넌트를 렌더링하고, 또 테스트에 필요한 button과 input을 반환한다
  • userEvent.type : 사용자가 타이핑하는 것을 흉내내는 메서드. 사용자의 작동을 여러 fireEvent를 통해 좀 더 자세하게 흉내 내는 모듈이다. 대부분의 이벤트를 테스트할 때는 fireEvent로 충분하고 훨씬 더 빠르기 때문에, 특별히 사용자의 이벤트를 흉내내야 할 때만 userEvent를 사용한다.
  • jest.spyOn(window, ‘alert’).mockImplementation() : 아래 두 메서드 참고
  • jest.spyOn : 어떠한 특정 메서드를 오염시키지 않고 실행이 됐는지, 또 어떤 인수로 실행됐는지 등 실행과 관련된 정보만 얻고싶을 때 사용한다. 단순히 관찰하는 용도.
  • mockImplementation : 해당 메서드에 대한 모킹(mocking) 구현을 도와줌. 비록 모의 함수로 구현된 함수이지만 함수가 실행됐는지 등의 정보는 확인할 수 있도록 도와준다.
  1. 비동기 이벤트가 발생하는 컴포넌트
  • 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를 활용해 비동기 요청이 끝난 뒤에 제대로 된 렌더링이 일어났는지 기다린 후에 확인하는 것이다. 이 두 가지만 염두에 둔다면 비동기 컴포넌트 테스트 또한 크게 다를 것이 없다.
  1. 사용자 정의 훅 테스트하기
  • react-hooks-testing-library를 사용해 기존 테스트코드 방식의 불편함을 해결한다
  • 매번 테스트가 끝난 후에는 process.env.NODE_ENV를 다시 development로 변경한다. NODE_ENV 할당문을 강제로 작성한 이유는 타입스크립트에서는 NODE_ENV를 읽기 전용 속성으로 간주하기 때문이다.
  • 훅을 두 번 연속 실행하는 것을 테스트하기 위해서는 renderHook이 반환하는 객체의 값 중 하나인 rerender 함수를 사용해야 한다.

테스트 작성 시 고려해야 할 점

  • 테스트 커버리지 : 해당 소프트웨어가 얼마나 테스트됐는지를 나타내는 지표
  • 테스트 커버리지가 높을수록 좋고 꾸준히 테스트 코드를 작성해야 한다.
  • 그러나 테스트 커버리지는 단순히 얼마나 많은 코드가 테스트되고 있는지를 나타내는 지표일 뿐, 테스트가 잘되고 있는지를 나타내는 것은 아니다. 그러므로 절대 테스트 커버리지를 맹신해서는 안된다.
  • 테스트 커버리지를 100% 끌어올릴 수 있는 상황은 생각보다 드물다.
  • TDD(Test Driven Development; 테스트 주도 개발) 개발 방법론을 차용해서 테스트를 우선시하더라도 서버 코드와는 다르게 프론트엔드 코드는 사용자의 입력이 매우 자유롭기 때문에 이러한 모든 상황을 커버해 테스트를 작성하기란 불가능하다.
  • 따라서 테스트 코드를 작성하기 전에 생각해봐야 할 최우선 과제는 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것이다.
  • 테스트가 이뤄야할 목표는 애플리케이션이 비즈니스 요구사항을 충족하는지 확인하는 것.

테스트 방법들

  • 유닛 테스트 : 각각의 코드나 컴포넌트가 독립적으로 분리된 환경에서 의도된 대로 정확히 작동하는지 검증하는 테스트
  • 통합 테스트 : 유닛 테스트를 통과한 여러 컴포넌트가 묶여서 하나의 기능으로 정상적으로 작동하는지 확인하는 테스트
  • 엔드 투 엔드: 실제 사용자처럼 작동하는 로봇을 활용해 애플리케이션의 전체적인 기능을 확인하는 테스트