본문 바로가기
Front-End Study/모던 리액트 딥다이브 스터디

모던 리액트 딥다이브 - 13회차 [8-1, 8-2]

by 코딩기 2024. 7. 12.
728x90

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

좋은 코드를 작성하기 위한 ESLint와 리액트 테스트 라이브러리를 알아보자


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

✨ JS 생태계에서 가장 많이 사용되는 정적 코드 분석 도구 ESLint에 대해 알아봅세~


8.1.1 ESLint 살펴보기


🔖 ESLint 어떻게 이렇게 쌈@뽕하게 JS 코드를 분석하는 걸까?

  • JS코드를 문자열로 읽는다.

  • JS 코드를 분석 가능한 파서(parser)로 코드를 구조화한다.

  • 구조화한 AST(Abstract Syntax Tree)라 하고, 이를 각종 규칙과 대조한다.

  • 규칙 위반 코드를 알리거나 수정한다.

🖥️ 코드 변환 톺아보기

const handleCancelClick = () => {
  setIsEditing(false);
  setRenewalTime(!renewalTime);
  cancelModalOff();
  updateFormData();
};
{
  "type": "Program",
  "start": 0,
  "end": 322,                 // 코드 시작
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 181,
      "end": 322,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 187,                   // const 뺀 나머지
          "end": 321,
          "id": {
            "type": "Identifier",
            "start": 187,
            "end": 204,                     // 함수 이름
            "name": "handleCancelClick"
          },
          "init": {
            "type": "ArrowFunctionExpression",
            "start": 207,                     // 화살표 함수임을 의미
            "end": 321,
            "id": null,
            "expression": false,
            "generator": false,
            "async": false,
            "params": [],
            "body": {
              "type": "BlockStatement",
              "start": 213,
              "end": 321,                     // {} 블록 스테이트 시작
              "body": [
                {
                  "type": "ExpressionStatement",
                  "start": 219,
                  "end": 239,
                  "expression": {
                    "type": "CallExpression",
                    "start": 219,
                    "end": 238,
                    "callee": {
                      "type": "Identifier",
                      "start": 219,
                      "end": 231,
                      "name": "setIsEditing"    // setIsEditing 실행 부분
                    },
                    "arguments": [
                      {
                        "type": "Literal",
                        "start": 232,
                        "end": 237,
                        "value": false,
                        "raw": "false"
                      }
                    ],
                    "optional": false
                  }
                },
                {
                  "type": "ExpressionStatement",
                  "start": 244,
                  "end": 273,
                  "expression": {
                    "type": "CallExpression",
                    "start": 244,
                    "end": 272,
                    "callee": {
                      "type": "Identifier",
                      "start": 244,
                      "end": 258,
                      "name": "setRenewalTime"      // setRenewalTime 실행 부분
                    },
                    "arguments": [
                      {
                        "type": "UnaryExpression",
                        "start": 259,
                        "end": 271,
                        "operator": "!",
                        "prefix": true,
                        "argument": {
                          "type": "Identifier",
                          "start": 260,
                          "end": 271,
                          "name": "renewalTime"     // setRenewalTime에 들어간 인수
                        }
                      }
                    ],
                    "optional": false
                  }
                },
                {
                  "type": "ExpressionStatement",
                  "start": 278,
                  "end": 295,
                  "expression": {
                    "type": "CallExpression",
                    "start": 278,
                    "end": 294,
                    "callee": {
                      "type": "Identifier",
                      "start": 278,
                      "end": 292,
                      "name": "cancelModalOff" // cancelModalOff 실행 부분
                    },
                    "arguments": [],
                    "optional": false
                  }
                },
                {
                  "type": "ExpressionStatement",
                  "start": 300,
                  "end": 317,
                  "expression": {
                    "type": "CallExpression",
                    "start": 300,
                    "end": 316,
                    "callee": {
                      "type": "Identifier",
                      "start": 300,
                      "end": 314,
                      "name": "updateFormData"            // updateFormData 실행 부분
                    },
                    "arguments": [],
                    "optional": false
                  }
                }
              ]
            }
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

📚 espree 코드 분석 결과를 바탕으로 ESLint가 뭘 수정하고 변경할지 결정

module.export = {
  meta: {
    type: "problem",
    docs: {
      description: "Disallow the use of `debugger`",
      recommended: true,
    },
    fixable: null, // 코드 수정 불가
    schema: [], // 옵션 미설정
  },
  create(context) {
    // 실제 코드에서 문제점 확인
    return {
      DebuggerStatement(node) {
        context.report({
          // debuggerStatement 확인 후 리폿
          node,
          message: "Unexpected 'debugger' statement.",
          fix: function (fixer) {
            return fixer.remove(node);
          },
        });
      },
    };
  },
};

8.1.2 eslint-plugin과 eslint-config


  • eslint-plugin 은 언급했던 규칙들을 모아놓은 패키지이며, 대시(-)를 뒤에 붙여 다양한 상황에 해당하는 규칙들을 제공한다.
eslintrc
├─ conf
│  ├─ config-schema.js
│  └─ environments.js
├─ dist
│  ├─ eslintrc-universal.cjs
│  ├─ eslintrc-universal.cjs.map
│  ├─ eslintrc.cjs
│  └─ eslintrc.cjs.map
├─ lib
│  ├─ config-array
│  │  ├─ config-array.js
│  │  ├─ config-dependency.js
│  │  ├─ extracted-config.js
│  │  ├─ ignore-pattern.js
│  │  ├─ index.js
│  │  └─ override-tester.js
│  ├─ shared
│  │  ├─ ajv.js
│  │  ├─ config-ops.js
│  │  ├─ config-validator.js
│  │  ├─ deprecation-warnings.js
│  │  ├─ naming.js
│  │  ├─ relative-module-resolver.js
│  │  └─ types.js
│  ├─ cascading-config-array-factory.js
│  ├─ config-array-factory.js
│  ├─ flat-compat.js
│  ├─ index-universal.js
│  └─ index.js
├─ LICENSE
├─ package.json
├─ README.md
└─ universal.js
  • eslint-config 는 위의 eslint-plugin 의 묶음 판매 패키지라고 볼 수 있으며, 이는 여러 프로젝트에 걸쳐 동일하게 사용 가능한 ESLint 관련 설정을 제공한다.
{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "next/core-web-vitals",
    "next",
    "eslint:recommended",
    "plugin:import/recommended",
    "plugin:import/typescript",
    "plugin:@typescript-eslint/recommended",
    "plugin:react-hooks/recommended",
    "plugin:tailwindcss/recommended",
    "prettier"
  ],
  "plugins": ["import", "@typescript-eslint", "react-hooks", "tailwindcss", "prefer-arrow"],
  "rules": {
    "import/order": [
      "error",
      {
        "groups": [
          "external", // 외부 라이브러리
          "builtin", // Node.js 내장 모듈
          "internal", // 경로 별칭 (@)
          "parent", // 부모 경로 (..)
          "sibling", // 같은 경로 (./)
          "index", // 현재 디렉토리 (./)
          "object"
        ],
        "pathGroups": [
          {
            "pattern": "react*",
            "group": "external",
            "position": "before"
          }
        ],
        "pathGroupsExcludedImportTypes": ["react*"]
      }
    ],
    "arrow-body-style": ["error", "always"],
    "curly": ["error"],
    "max-depth": ["error", 4],
    "import/export": "warn",
    "react/react-in-jsx-scope": "off",
    "prefer-arrow/prefer-arrow-functions": [
      "error",
      {
        "disallowPrototype": true,
        "singleReturnOnly": false,
        "classPropertiesAllowed": false
      }
    ]
  },
  "settings": {
    "import/resolver": {
      "typescript": true
    }
  }
}

👍 원래 직접 만드는 것보다 남이 해준 요리가 더 맛있다.

  • 개발자가 직접 패키지를 짜는 경우는 거의 없고, 일부 IT 기업에서 만들어둔 템플릿들을 활용하는 느낌으로 많이 사용한다.

개추! (개발자 추천)

  • eslint-config-airbnb

    • 가장 대중적인 eslint-config 템플릿이다.
  • titicaca/tripole-config-kit

    • 유지보수가 활발한 편에 속하는 config 이며, airbnb의 config 를 기반으로 하지 않는데도 불구하고 왠만한 규칙을 제공하여 사용 시 큰 지장이 없다.

    • 또한 외부 제공 규칙에 대한 테스트 코드가 존재하여 각 환경에 맞게 사용할 수 있다.

  • eslint-config-next

    • Next.js 를 이용한 프로젝트는 그냥 이놈 쓰자.

8.1.3 나만의 ESLint 규칙 만들기


🔖 나만의 ESLint 규칙을 만들어 나에게 가장 익숙한 규칙으로 코드를 작성하자

  • 이미 존재하는 규칙을 커스터마이징하여 적용하는 것은 매우 유용하다.
// no improt react
module.exports = {
  rules: {
    'no-restricted-imports' : [
      'error',
      {
        path: [
          {
            name: 'react',
            importNames: ['default'] // import react만 제외하기 위함
            message: '이제부턴 import react는 안해도 돼!'
          }
        ]
      }
    ]
  }
}
  • 아예 새로운 나만의 규칙을 만들 수도 있다.
// new Date 금지하기
module.exports = {
  meta: {
    type: "problem",
    docs: {
      description: "Disallow use of `new Date()` without arguments",
      category: "Possible Errors",
      recommended: false,
    },
    fixable: "code", // 코드 수정 가능
    schema: [], // 옵션 미설정
  },
  create(context) {
    return {
      NewExpression(node) {
        if (node.callee.name === "Date" && node.arguments.length === 0) {
          // 코드 객체의 이름이 Date임과 동시에 인수의 길이가 0일 경우
          context.report({
            node,
            message: "Use of `new Date()` without arguments is not allowed.",
            fix: function (fixer) {
              // 자동 수정: `new Date()`를 `new Date(/* specify arguments */)`로 변경
              return fixer.replaceText(node, "new Date(/* specify arguments */)");
            },
          });
        }
      },
    };
  },
};

📂 만든 규칙들을 배포해보자!

  • 먼저 yogenerate-eslint 를 활용해 eslint-plugin 구성 환경을 만든다.

  • 구성된 디렉토리 및 파일에서 rules/no-new-date.js 파일을 열고 앞에서 작성한 규칙을 붙여넣는다.

  • npm publish 로 배포하고 원하는 프로젝트에 설치 및 사용한다.


8.1.4 주의할 점


🔖 잘못 설정하면 원치 않는 결과가 나온다.

  • 코드의 포메팅을 돕는 Prettier 과의 충돌이 일어날 수 있으므로 항상 주의해야 한다.

    • 서로 충돌하지 않게끔 규칙을 잘 설정한다.

    • JS, TS는 ESLint에 맡기고, 그 외는 Prettier 에 맡긴다.

  • 특정 규칙을 임시로 제외하고 싶은 경우 react-hooks/no-exhaustive-deps 주석을 사용한다.

    • 모든 규칙은 존재하는 이유가 있으니까 핑계대지 말고 잘 점검하고 사용하자
  • ESLint 버전이 충돌하여 문제가 생길 수 있다.

    • 설치하고자 하는 configplugin 및 프로젝트가 지원하는 버전을 잘 확인해야한다.

8.1.5 책 정리 + 주관적인 정리

🔖 책 정리

  • ESLint는 개발자의 숟가락과도 같다.

  • 잘 정리된 규칙을 사용하거나 직접 커스텀하는 등 자유도가 높지만 그만큼 알맞게 사용해야 한다.

  • 익숙해진다면 본인만의 eslint-config를 만들어보는 것도 큰 도움이 될 것이다.

🏷️ 주관적인 정리

  • ESLint는 항상 가까이 있으나 막상 보면 어색한 친구라고 생각한다.

  • ESLint의 규칙들은 모두 존재하는 이유가 있으므로, 항상 신경쓰며 잘 사용하자.


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

✨ 프론트엔드는 퍼블리싱만이 다가 아니므로, 각각의 개발 환경에서 항상 테스트해야 한다.


8.2.1 React Testing Library란?


🔖 DOM Testing Library를 기반으로 만들어진 HTML이 없는 환경에서 HTML 및 DOM을 사용하여 테스팅이 가능하도록 설계

const jsdom = require("jsdom");

const { JSDOM } = jsdom;

const dom = new JSDOM("<!DOCTYPE html><p>Hello world</p>");

console.log(dom.window.document.querySelector("p").textContext);
  • jsdom 을 사용해 HTML이 있는 것처럼 DOM을 불러와 조작 가능하다.

  • 실제로 리액트 컴포넌트를 렌더링하지 않아도 원하는대로 작동하는지 검사할 수 있다.


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


🔖 기본적인 테스트 코드 작성법을 알아보자

// 테스트할 함수
function sum(a, b) {
  return a + b;
}

// 테스트할 코드
let actual = sum(1, 2);
let expected = 3;

if (expected !== actual) {
  throw new Error(`${expectred}는 기대와 다르다!`);
}

actual = sum(2, 3);
expected = 4;

if (expected !== actual) {
  throw new Error(`${expectred}는 기대와 다르다!`);
}
  • 기본적으로 테스트할 함수의 리턴값이 예상하는 답과 같다면 이것에 대해 성공/실패 여부를 판단하도록 코드를 구성하는 것이 기본이다.
// Node.js의 assert
const assert = require("assert");

function sum(a, b) {
  return a + b;
}

assert.equal(sum(1, 2), 3);
assert.equal(sum(2, 2), 5); // AssertionError ...
  • Node.js 에서 기본적으로 assert 메서드를 제공한다.

  • 또한 equal 뿐만 아니라 다양한 결과값을 상황에 맞게 보여주는 키들이 존재한다.

❗하지만 테스트 코드는 단순히 값뿐만 아니라 테스트의 관한 실제 정보 또한 반환해야 한다.

  • 이는 테스팅 프레임워크를 통해 볼 수 있다.

    • Jest, Mocha, Karma, Jasmine 등의 다양한 프레임워크가 존재한다.

    • 그중에서도 Jestexpect 패키지를 알아보자

// add.js
function add(a, b) {
  return a + b + 1; // 의도적으로 오류를 넣음
}

module.exports = add;

const add = require("./add");

test("adds 1 + 2 to equal 3", () => {
  expect(add(1, 2)).toBe(3);
});
FAIL  ./add.test.js
✕ adds 1 + 2 to equal 3 (5ms)

● adds 1 + 2 to equal 3

  expect(received).toBe(expected) // Object.is equality

  Expected: 3
  Received: 4

  3 | test('adds 1 + 2 to equal 3', () => {
  4 |     expect(add(1, 2)).toBe(3);
  5 | });
    |                        ^

  at Object.<anonymous> (add.test.js:4:24)
  • Jest 가 제공하는 자체적인 '글로벌' 값들을 이용해서 테스트 관련 정보를 임포트 하지 않고 효율적으로 정보를 볼 수 있다.

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


🔖 컴포넌트를 렌더링하고 컴포넌트에서 필요 시 특정 액션을 수행하며, 두 과정을 통해 기대와 실제를 비교한다.

  • create-react-app 에는 이미 테스팅 라이브러리가 포함되어 있다.
npx create-react-app react-test --templete typescript
// src/App.tsx
function App() {
  return (
    <div className="App">
      <p>learn react</p>
    </div>
  );
}

// src/App.test.tsx
import React from "react";
import { render, screen } from "@testing-library/react";
import App from "./App";

test("renders learn react link", () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});
  • App 컴포넌트를 렌더링하고 'learn react' 라는 문자열을 가진 DOM 요소를 찾는다.

  • expect(linkElement).toBeInTheDocument() 어설션을 활용해 해당 요소가 document 내부 요소인지 확인한다.

    • getBy, findBy, queryBy 등 다양한 조건의 테스팅 어설션이 존재한다.

🏷️ 정적 컴포넌트를 테스트하는 방법은 상태가 존재하지 않아 항상 결과가 같으므로 어렵지 않다.

const InteractiveComponent: React.FC = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>정적 컴포넌트</h1>
      <p>현재 값: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
};

import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import InteractiveComponent from "./InteractiveComponent";

describe("InteractiveComponent", () => {
  beforeEach(() => {
    render(<InteractiveComponent />);
  });

  it("모든 요소가 올바르게 존재합니다.", () => {
    const headingElement = screen.getByText(/정적 컴포넌트/i);
    const paragraphElement = screen.getByText(/현재 값: 0/i);
    const buttonElement = screen.getByText(/증가/i);

    expect(headingElement).toBeInTheDocument();
    expect(paragraphElement).toBeInTheDocument();
    expect(buttonElement).toBeInTheDocument();
  });

  it("버튼을 클릭하면 count가 증가합니다.", () => {
    const buttonElement = screen.getByText(/증가/i);
    fireEvent.click(buttonElement);

    const updatedParagraphElement = screen.getByText(/현재 값: 1/i);
    expect(updatedParagraphElement).toBeInTheDocument();
  });
});
  • 여기서도 beforeEach, describe, it 등 다양한 메서드를 상황에 맞게 사용 가능하다.

🏷️ 동적 컴포넌트는 기본적으로 신경쓸 부분이 많아져 훨씬 어렵다.

  • 정적 컴포넌트와 같은 순수한 '무상태' 컴포넌트는 상황에 따라 달라지는 여러 값들을 신경쓸 필요가 없지만, 동적은 다르다.
// src/DynamicComponent.tsx
const DynamicComponent: React.FC = () => {
  const [text, setText] = useState("");

  const handleClick = () => {
    alert(`Button clicked with text: ${text}`);
  };

  return (
    <div>
      <h1>Dynamic Component</h1>
      <input type="text" placeholder="Type something..." value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={handleClick}>Click Me</button>
      <p>You typed: {text}</p>
    </div>
  );
};

// src/DynamicComponent.test.tsx
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import DynamicComponent from "./DynamicComponent";

const setup = () => {
  const utils = render(<DynamicComponent />);
  const input = screen.getByPlaceholderText(/Type something.../i);
  const button = screen.getByText(/Click Me/i);
  return {
    input,
    button,
    ...utils,
  };
};

describe("DynamicComponent", () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it("renders the initial state correctly", () => {
    const { input, button } = setup(); // 컴포넌트 렌더링 및 필요 요소 반환
    const headingElement = screen.getByText(/Dynamic Component/i);
    const paragraphElement = screen.getByText(/You typed:/i);

    expect(headingElement).toBeInTheDocument();
    expect(input).toBeInTheDocument();
    expect(button).toBeInTheDocument();
    expect(paragraphElement).toHaveTextContent("You typed:");
  });

  it("updates the text as the user types", async () => {
    const { input } = setup();
    await userEvent.type(input, "Hello, world!"); // 유저가 타이핑하는 것을 흉내

    const updatedParagraphElement = screen.getByText(/You typed: Hello, world!/i);
    expect(updatedParagraphElement).toBeInTheDocument();
  });

  it("calls handleClick when button is clicked", () => {
    const { button } = setup();
    const handleClickSpy = jest.spyOn(window, "alert").mockImplementation(() => {}); // alert 함수를 모킹하여 실제로 동작하진 않도록 구현

    fireEvent.click(button); // 버튼 클릭 이벤트 시뮬레이션
    expect(handleClickSpy).toHaveBeenCalledWith("Button clicked with text: ");
  });

  it("calls handleClick with the correct text", async () => {
    const { input, button } = setup();
    await userEvent.type(input, "Hello, world!");
    const handleClickSpy = jest.spyOn(window, "alert").mockImplementation(() => {});

    fireEvent.click(button);
    expect(handleClickSpy).toHaveBeenCalledWith("Button clicked with text: Hello, world!");
  });
});
  • 여기서도 마찬가지로 userEvent.type, jest.spyOn(window, 'alert').mockImplementation() 등 상황에 맞는 다양한 메서드를 사용 가능하다.

🏷️ 비동기 이벤트 발생 컴포넌트도 테스트 해보자

  • fetch 가 추가됨으로서 신경써야할 사항이 더욱 많아졌다...
// src/mocks/handlers.ts
import { rest } from "msw";

export const handlers = [
  rest.get("/api/data", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ message: "Hello from the mocked API!" }));
  }),
];
// src/mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

export const server = setupServer(...handlers);

// src/FetchComponent.test.tsx
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import { server } from "./mocks/server";
import { rest } from "msw";
import FetchComponent from "./FetchComponent";

// MSW 서버 시작 및 종료 설정
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers()); // 서버를 기본 설정으로 되돌림
afterAll(() => server.close());

test("renders fetched data correctly", async () => {
  render(<FetchComponent />);

  // 로딩 텍스트가 표시되는지 확인
  expect(screen.getByText("Loading...")).toBeInTheDocument();

  // 데이터가 표시될 때까지 기다림
  await waitFor(() => {
    expect(screen.getByText("Message: Hello from the mocked API!")).toBeInTheDocument();
  });
});

test("handles server error", async () => {
  // API 요청을 실패하도록 모킹
  server.use(
    rest.get("/api/data", (req, res, ctx) => {
      return res(ctx.status(500), ctx.json({ error: "Internal Server Error" }));
    })
  );

  render(<FetchComponent />);

  // 로딩 텍스트가 표시되는지 확인
  expect(screen.getByText("Loading...")).toBeInTheDocument();

  // 오류 메시지가 표시될 때까지 기다림
  await waitFor(() => {
    expect(screen.getByText("Error: Internal Server Error")).toBeInTheDocument();
  });
});
  • MSW는 서비스 워커를 활용해 실제 네트워크 요청을 가로채는 방식으로 모킹을 구현하여 간편하고, 다양한 환경에서 수월하게 동작한다.

  • 데이터 모킹을 활용해 외부에서 데이터를 받아와야 하는 로직 특성 상 의존해야 하는 데이터에 대한 분리를 진행하고 코드 자체 테스트에 더욱 집중할 수 있도록 한다.

모킹에 대해 알아보자


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


🔖 사용자 정의 훅을 테스트하기 위해 react-hooks-testing-library 를 활용해보자

  • useEffectDebugger 훅을 활용해 어떤 props의 변경으로 리렌더링 됐는지 확인한다.
// src/hooks/useEffectDebugger.ts
import { useEffect, useRef } from 'react';

const useEffectDebugger = (effect: React.EffectCallback, dependencies: any[], dependencyNames: string[]) => {
  const previousDepsRef = useRef<any[]>();

  useEffect(() => {
    const changes: { [key: string]: { from: any; to: any } } = {};
    const previousDeps = previousDepsRef.current;

    dependencies.forEach((dep, i) => {
      if (previousDeps && previousDeps[i] !== dep) {
        changes[dependencyNames[i]] = {
          from: previousDeps[i],
          to: dep,
        };
      }
    }); // 뭐 때문에 리렌더링 된건지 비교

    if (Object.keys(changes).length) {
      console.log('[useEffectDebugger] Dependency changes:', changes);
    } // 변경 사항이 있다면 체크

    previousDepsRef.current = dependencies;
    effect();
  }, dependencies);
};

export default useEffectDebugger;

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


🔖 테스트 커버리지는 소프트웨어가 얼마나 잘 테스트 됐는지를 나타내는 지표지만, 맹신해선 안된다.

  • 테스트 코드 작성 전 우선과제는 앱에서 가장 취약하거나 중요한 부분을 파악하는 것이다.

  • 앱의 가장 핵심적인 부분부터 하나하나 테스트코드를 작성하고, 항상 소프트웨어 품질에 대한 확신을 얻기 위해 노력하자


8.2.6 그 밖에 해볼 만한 여러 가지 테스트


  • 유닛 테스트 : 각각의 코드나 컴포넌트가 독립적으로 분리된 경우 의도대로 작동하는지 검증

  • 통합 테스트 : 유닛 테스트를 통과한 여러 컴포넌트가 하나의 기능으로 정상 동작하는지 검증

  • 엔드 투 엔드 : E2E 테스트라 지칭하며 실제 사용자와 같은 로봇을 활용해 앱 기능을 전체적으로 테스트

👍 E2E까지 가는건 어렵지만, 그만큼 자신감이 생길 것이다.


8.2.7 책 정리 + 주관적인 정리


🔖 책 정리

  • 테스트는 결과물이 정해져있는 앱과 다르게 다양하게 코드를 시도할 수 있다.

  • 다양한 방법들이 궁극적으로 추구하는 목표는 결국 앱이 비즈니스 요구사항을 충족하는지 확인하는 것이다.

  • 따라서 테스트 코드의 대한 작성과 이해, 확신은 결국 소프트웨어의 품질과 직결된다.

🏷️ 주관적인 정리

  • 리액트 및 브라우저 개발자 도구만으로도 벅차다고 생각했는데 정말로 쉽지 않다.

  • 테스트 코드 및 라이브러리의 활용은 먼 미래겠지만, 지금부터 조금씩 익혀놓는다면 분명 나중에 큰 도움이 될 것이라는 생각이 들었다.