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

ESLint
강석우's avatar
Mar 30, 2024
8장 좋은 리액트 코드 작성을 위한 환경 구축하기

8.1 ESLint를 활용한 정적 코드 분석

8.1.1 ESLint 살펴보기

ESLint의 코드 분석 방법

  1. 자바스크립트 코드를 문자열로 읽는다.

  2. 자바스크립트 코드를 분석할 수 있는 파서로 코드를 구조화한다.

    1. ESLint는 espree를 사용한다.

    2. espree는 단순히 변수인지, 함수인지, 함수명이 무엇인지 파악하는 것 뿐만 아니라 코드의 정확한 위치와 같은 아주 세세한 정보도 분석해 알려준다.

  3. 2번에서 구조화한 트리를 AST라 하며, 이 구조화 된 트리를 기준으로 각종 규칙과 대조한다.

  4. 규칙과 대조했을 때 이를 위반한 코드를 알리거나 수정한다.

ESLint가 espree로 코드를 분석한 결과를 바탕으로, 어떤 코드가 잘못된 코드이며 어떻게 수정해야 할지도 정해야 하는데 이를 rules 라고 한다. 또한 특정 규칙의 모음을 plugins라 한다

8.1.2 eslint-plugin 과 eslint-config

eslint-plugin

리액트, import 와 같이 특정 프레임워크나 도메인과 관련된 규칙(rule)들을 모아놓은 패키지

  • eslint-plugin-import

    • 자바스크립트가 다른 모듈을 가져올 때 import 와 관련된 규칙들을 제공한다.

  • eslint-plugin-react

    • 리액트 관련 규칙을 경고를 해주는 패키지다. 예시로 배열에 키값이 선언되어있지 않다는 메세지를 종종 볼 수 있을텐데 이는 위의 패키지에서 제공하는 메세지이다.

  • 이 외에도 다양한 플러그인들이 있다 만약 본인의 프로젝트에 맞는 plugin이 있다면 찾아서 적용하도록 하자.

eslint-config

eslint-plugin을 한데로 묶어서 완벽하게 한 세트로 제공하는 패키지

  • eslint-config-airbnb

    • react 프로젝트를 만들게 되면 가장 먼저 손에 꼽히는 eslint-config

  • @titicaca/triple-config-kit

    • airbnb 기반이 아닌 본인들만의 eslint를 새로 제작하였다.

    • 외부로 제공하는 규칙에 대한 테스트코드가 존재한다.

    • 별도의 frontend 규칙도 제공하고 있어 환경에 맞게 규칙을 제공할 수 있다.

  • eslint-config-next

    • Nextjs 프레임워크를 사용하고 있는 프로젝트에서 사용할 수있는 config 이다.

    • 자바스크립트 뿐만 아니라 HTML또한 정적으로 분석해준다.

8.1.3 나만의 ESLint 규칙 만들기

이미 존재하는 규칙을 커스텀해서 만들기

리액트 17버전 부터는 import React 구문이 필요 없어졌다.
그렇다면 지금까지 작성된 import React를 찾아서 메세지를 띄워주는 커스텀을 해보자

no-restricted-imports

module.exports = {
  rules: {
    "no-restricted-imports": [
      "error",
      {
        paths: [
          {
            // 모듈명
            name: "react",
            // 모듈의 이름
            importNames: ["default"],
            // 경고 메세지
            message:
              "import react from React 는 더이상 필요하지 않습니다. 제거해주세요",
          },
        ],
      },
    ],
  },
};
  • importNames를 통해서 React의 다른 모듈이 아니라 "react" 를 가져오는 부분만 해당시킨다.

완전히 새로운 규칙 만들기 : new Date() 금지시키기

new Date 를 espree 로 분석하기

new Date 를 espree로 분석하게되면 여러 정보가 나오게 되는데 추리면 아래와 같다.

  • type : NewExpression

  • name : Date

  • arguments : []

위의 정보를 토대로 아래와 같은 eslint 코드를 작성할 수 있다.

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.length === 0) {
          context.report({
            node: node,
            messageId: "message",
            fix: function (fixer) {
              return fixer.replaceText(node, "ServerDate()");
            },
          });
        }
      },
    };
  },
};
  • meta 필드

    • 해당 규칙과 관련된 정보를 나타내는 필드다.

    • 실제의 동작과는 관련이 없으며 사용가능 옵션은 공식홈페이지에서 찾아볼 수 있다.

  • create 필드

    • 객체를 반환해야 하며 코드 스멜을 감지할 선택자나 이벤트명 등을 선언할 수 있다.

    • 위에서는 NewExpression 이라는 선택자를 선택해 new 생성자를 사용할 때 ESLint가 사용되게 했다. 그리고 해당 NewExpression을 찾았을 때 노드를 기준으로 생성자 검증을 진행한다.

    • 위에서는 callee.name이 Date이고 인수가 없는 경우인지를 검증한다.

    • 찾았다면 context.report 에서 리포트 후 문제가 생겼을 때 노출할 message를 선택한다.

    • fix 에서는 대체 시켜줄 함수를 작성해놓는다.

8.1.4 주의할 점

Prettier 와의 충돌

prettier 또한 eslint 와 같이 코드의 포맷팅을 도와주는 도구이다.
두 도구를 함께 사용하게 된다면 충돌이 일어날 수 있는데 해결 방법에는 두 가지가 있다.

  1. 서로 규칙이 충돌하지 않게 잘 설정하는 것이다. ( 너무 당연한 소리;; )

  2. 자바스크립트나 타입스크립트는 ESLint에 그 외의 파일은 Prettier에 맡기는 것이다.
    대신 추가적으로 자바스크립트에 Prettier를 사용해야할 경우 react-plugin-prettier를 사용한다.

예외처리

eslint-disable- 주석을 통해 eslint의 규칙을 벗어날 수 있다.

특정 줄만 제외 
//eslint-disable-line 

다음 줄 제외
//eslint-disable-next-line

특정 여러줄 제외
/*eslint-disable*/
~~~~~
~~~~~
/*eslint-enable*/

파일 전체에서 제외
/* eslint-disable */

하지만 필요하지 않은 규칙은 애초에 만들어지지 않았을 확률이 높다. 규칙을 끄거나 제외하기 전에 알맞고 정확한 코드를 작성한게 맞는지 한번 더 고민해보자.

8.2 리액트 팀이 권장하는 리액트 테스트 라이브러리

8.2.1 React Testing Library 란?

Dom Testing Library 를 기반으로 만들어진 테스팅 라이브러리로 리액트를 테스트하기 위해 만들어졌다.

  • 실제로 리액트 컴포넌트를 렌더링하지 않고도 리액트 컴포넌트가 원하는 대로 렌더링 되고 있는지 확인할 수 있다.

8.2.2 자바스크립트 테스트의 기초

테스트를 작성하는 방식에 대해 알아보자

  1. 테스트할 함수나 모듈을 선정한다.

  2. 함수나 모듈이 반환하길 기대하는 값을 적는다.

  3. 함수나 모듈의 실제 반환 값을 적는다.

  4. 3번의 개대에 따라 2번의 결과가 일치하는지 확인한다.

  5. 기대하는 결과를 반환한다면 테스트는 성공이며, 만약 기대와 다른 결과를 반환하면 에러를 던진다.

이를 위해 필요한 것은 5번, 기대와 다른 결과를 반환하면 에러를 던지고 성공하면 성공했다는 메세지를 출력하는 부분을 대신 해주는 라이브러리다.
Node.js 는 assert 라는 모듈을 통해 해당 기능을 제공해주고 있다.

const assert = require("assert");
const sum = (a, b) => {
  return a + b;
};
assert.equal(sum(1, 2), 3); // pass
assert.equal(sum(1, 2), 5); // error

위처럼 테스트 결과를 확인할 수 있도록 도와주는 라이브러리 == 어설션 라이브러리

좋은 테스트 코드란?

다양한 테스트 코드가 작성되고 통과하는 것 뿐만 아니라 어떤 테스트가 무엇을 테스트 하는지 일목요연하게 보여주는 코드 => 테스팅 프레임워크의 사용

테스팅라이브러리 종류

Jest, Mocha, Karma, Jasmine 등이 존재.

현재 리액트 진영에서는 Jest 가 강세이며 Jest는 자체적으로 제작한 expect라는 패키지를 사용해 어설션을 수행한다.

Jest

앞의 테스트 코드를 Jest로 완전히 새롭게 작성해보면 아래와 같다.

test("두 인수가 덧셈이 되어야 한다.", () => {
  expect(sum(2, 2)).toBe(3);
});

위와 같이 테스트 프레임워크를 사용하면 나오는 정보가 assert를 사용했을 때보다 훨씬 다양하다.

  • 무엇을 테스트 했는지

  • 소요된시간

  • 무엇이 성공하고 실패했는지

  • 전체 결과

8.2.3 리액트 컴포넌트 테스트 코드 작성하기

  1. 컴포넌트를 렌더링한다.

  2. 필요하다면 컴포넌트에서 특정 액션을 수행한다.

  3. 컴포넌트 렌더링과 2번의 액션을 통해 기대하는 결과와 실제 결과를 비교한다.

위의 코드를 해석해보면 다음과 같다.

  1. <App/>을 렌더링한다.

  2. 렌더링하는 컴포넌트 내에서 'learn react' 라는 문자열을 가진 DOM 요소를 찾는다.

  3. expect(linkElement).toBeInTheDocument() 어설션을 활용해 2번의 요소가 document 내부에 있는지 확인한다.

HTML 요소가 있는지 여부를 확인하기 위해서는 3가지 방법이 있다.

  1. getBy...

    1. 인수의 조건에 맞는 요소를 반환하며, 해당 요소가 없거나 두 개 이상이면 에러를 발생시킨다.

    2. 여러개를 한번에 찾고 싶다면 getAllBy... 를 사용하면 된다.

  2. findBy...

    1. getBy... 와 비슷하지만 큰 차이점은 Promise를 반환한다. 즉 비동기로 찾으며 기본으로 1000ms의 타임아웃을 갖고 있다.

    2. 해당 요소가 없거나 두 개 이상이면 에러를 발생시킨다.

    3. 여러개를 한번에 찾고 싶다면 findAllBy... 를 사용하면 된다.

  3. queryBy...

    1. 인수의 조건에 맞는 요소를 반환하는 대신 찾지 못하면 null을 반환한다.
      따라서 인수를 찾되 찾지못해도 에러를 발생시키고 싶지 않다면 queryBy... 을 사용하면 된다.

    2. 여러개를 한번에 찾고 싶다면 queryAllBy... 를 사용하면 된다.

컴포넌트를 테스트하는 파일은 테스트하고자 하는 파일과 같은 디렉터리상에 위치하는 것이 일반적이다.

정적 컴포넌트

beforeEach(()=>{
 render(<Login/>)
})

describe('버튼존재 유무 확인',()=>{
 it('로그인 버튼 확인',()=>{
  const loginButton = screen.getByTestId('loginButton')
  expect(loginButton).toBeVisible();
 })
 
 it('비밀번호 찾기 버튼 확인',()=>{
  ~~~~
 })

})
  • beforeEach

    • 각 테스트( it ) 수행 전 실행하는 함수다.
      테스트 실행 전 Static Component를 렌더링한다.

  • describe

    • 비슷한 속성을 가진 테스트를 하나의 그룹으로 묶는 역할을 한다.
      반드시 필요한 메서드는 아니지만 테스트 코드가 많아지고 관리가 어려워진다면 describe 내부에 describe를 또 사용할 수 있다.

  • it

    • test와 완전히 동일하며, test의 축약어다. it이라는 축약어를 제공하는 이유는 테스트 코드를 사람이 읽기 쉽게 하기 위해서다.
      한 강의에서는 it은 영어로, test는 한글로 작성하는 것이 좋다고 되어있었다.

  • testId

    • testId 는 리액트 테스팅 라이브러리의 예약어로, get 등의 선택자로 선택하기 어렵거나 곤란한 요소를 선택하기 위해 사용할 수 있다.

동적 컴포넌트

  describe("", () => {
    const setup = () => {
      const screen = render(<InputComponent />);
      const input = screen.getByLabelText("input") as HTMLInputElement;
      const button = screen.getByText(/제출하기/i) as HTMLButtonElement;
      return {
        input,
        button,
        ...screen,
      };
    };

    it("input의 초깃값은 빈 문자열이다.", () => {
      const { input } = setup();
      expect(input.value).toEqual("");
    });

     it('버튼 클릭시 해당 아이디로 표시된다.',()=>{
     const alertMock =      jest.spyOn(window,'alert').mockImplementation((_:string)=>undefined)
      const {button,input} = setup();
      const inputValue = 'helloWorld'
      userEvent.type(input,inputValue)
      fireEvent.click(button);
      expect(alertMock).toHaveBeenCalledTimes(1);
      expect(alertMock).toHaveBeenCalledWith(inputValue);
    })
  });
  • setup 함수

    • 내부에서 컴포넌트를 렌더링하고, 또 테스트에 필요한 button과 input을 반환한다.

  • userEvent.type

    • 사용자가 타이핑하는 것을 흉내내는 메서드다.

    • 기본적으로 userEvent는 fireEvent의 여러 이벤트를 순차적으로 실행해 좀 더 사용자가 작동하듯이 흉내내준다. userEvent.click 을 수행할 경우 아래 다섯개의 fireEvent가 실행된다.

      • fireEvent.mouseOver

      • fireEvent.mouseMove

      • fireEvent.mouseDown

      • fireEvent.mouseUp

      • fireEvent.click

    • 단 대부분의 이벤트를 테스트 할 경우 fireEvent 로 충분하다. 사용자를 특별히 흉내내고자 할 때만 사용하도록 하자.

  • jest.spyOn

    • 어떠한 특정 메서드를 오염시키지 않고 실행이 됐는지, 또 어떤 인수로 실행 됐는지 등 실행과 관련된 정보만 얻고 싶을 때 사용한다.

  • mockImplementation

    • 해당 메서드에 대한 모킹 구현을 도와준다.

비동기 이벤트가 발생하는 컴포넌트

직접 작성하기에는 코드가 너무 길어지는 이슈가 있기 때문에 라이브러리를 사용한다.

MSW(Mock Service Worker)

Node.js 나 브라우저에서 모두 사용할 수 있는 모킹 라이브러리이다.
브라우저에서 서비스 워커를 활용해 실제 네트워크 요청을 가로채는 방식으로 작동한다.
Node.js에서는 XMLHttpRequest의 요청을 가로채는 방식으로 작동한다.

  1. 서버 만들기

const server = setupServer(
 rest.get('/todos/:id',(req,res,ctx)=>{
  const todoId = req.params.id

  if(Nubmer(todoId)){
   return res(ctx.json({...MOCK_TODO_RESPONSE, id:Number(todoId)}))
  } else {
   return res(ctx.status(404))
   }
  }),
 }

setupServer 는 MSW에서 제공하는 메서드로 서버를 만드는 역할을 한다.
코드는 express와 비슷하게 작성하고 응답만 미리 준비해 둔 모킹 데이터를 반환하면 된다.

  1. 서버 동작 선언해주기

beforeAll(()=> server.listen())
afterEach(()=>server.resetHandlers())
afterAll(()=>server.close())

테스트 코드 시작 전 서버를 기동
서버를 기본 설정으로 되돌리는 역할 ( 테스트가 실패할 경우 계속해서 같은 코드를 테스트하지 않기 위해서 존재 )
테스트 코드 실행이 종료되면 서버를 종료

  1. 테스트 해주기

it('버튼 클릭시 데이터 불러오기',async ()=>{
 const button = screen.getByRole('button',{name:/1번/})
 fireEvent.click(button)

 const data = await screen.findByText(MOCK_TODO_RESPONSE.title)
 expect(data).toBeInTheDocument()
})

버튼을 클릭해 fetch를 발생시켜준다.
요소가 렌더링 될 때까지 일정시간 동안 기다리는 find 메서드를 사용해 요소를 검색한다.

8.2.4 사용자 정의 훅 테스트하기

react-hooks-testing-library를 사용하여 테스트 하도록 하자.

8.2.5 테스트를 작성하기에 앞서 고려해야 할 점

프론트 엔드 개발 부분에 있어 100% 커버리지 테스트 코드는 생각보다 매우 드물다.

따라서 우리는 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것을 우선으로 하고 관련 코드 중점적으로 테스트 코드를 작성하며 개발해 나가는 것이 좋을 것이다.

Share article

석우의 개발블로그