[React] - TDD 실습환경구성

Lee Jeong Min·2021년 11월 26일
3
post-thumbnail

실습 환경 구성

package.json 설정

  "private": true,
  "type": "module",
  "name": "preparing-for-react",
  "description": "React 학습 전에 알아야 할 개발 환경 구성 공부",
  "version": "0.0.1",
  "scripts": {
    "start": "cross-env OPEN='/client/public' npm run dev",
    "dev": "cross-env PORT=8080 node server/index.mjs",
    "test": ""
  },

패키지 json을 다음과 같이 설정함

  • private: npm에 앱/패널이 실수로 게시되는 것을 방지한다.

  • type: 기본은 commonjs설정이지만 module을 붙이게 되면 esm 모듈을 사용한다는 뜻

참조: https://nodejs.dev/learn/the-package-json-guide


cross-env 설치

MacOS, Windows, Linux등 다양한 OS 마다 환경변수를 설정하는 방법이 다르기 때문에 모든 운영체제에서 환경변수를 사용할 수 있도록 하는 패키지

참고: https://www.npmjs.com/package/cross-env

npm i -D cross-env

이후 터미널에

npm start

로 실행시키게 되면 다음과 같은 에러가 발생

type 설정을 module로 하였기 떄문에 require가 인식되지 않는 문제! --> import로 바꾸어줌

index.js

import liveServer from 'live-server';

그 이후 다시 실행시켜 보면...

또 다른 에러가 발생하게 되는데, 이는 type module로 설정을 하게 되면, mjs파일을 모듈로 가져오기 때문에 index.js를 index.mjs로 바꾸지 않아서 생기는 문제!

null 병합

const params = {
  host: 'localhost',
  port: PORT || 3000,
  open: OPEN || false
};

env같은 것들을 사용하다보면 ??(null 병합 연산자)를 사용해야하는 경우가 많은데, 이를 사용하는 경우는 null 또는 undefined이외의 값을 기본값으로 설정을 하고 싶은 경우에 이를 이용하여 '', false등을 기본값으로 설정한다.
|| 는 falsy한 값들이면 모두 우항 값을 설정한다.


env 설정

참고자료: https://nodejs.dev/learn/how-to-read-environment-variables-from-nodejs

위 사이트를 참고하면 env를 다음과 같이 사용할 수 있다.

따라서 script에 다음과 같이 설정을 해주면 PORT, OPEN각각 환경변수로 사용할 수 있다.

"scripts" {
  "start": "cross-env OPEN='/client/public' npm run dev",
  "dev": "cross-env PORT=8080 node server/index.mjs",
  ...
}

HTML파일 설정 소개

<meta http-equiv="X-UA-Compatible" content="IE=edge" />

IE를 위한 코드(렌더링 할 때 IE를 기준)
edges --> 가장 마지막 버전(11버전)

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

각 디바이스에 맞는 너비를 가짐
예전에는 content='user-scalable=no'이런 속성도 있었는데 의도적으로 사용자가 확대하는 것을 막음(접근성 측면에서 안좋음)

server측 파일 수정 시 자동 변경

nodemon도 있고 node-dev도 있는데 nodemon을 많이 써보았기 때문에 node-dev로 수업시간에 진행!


TDD

TDD 유틸리티 직접 구현

기대 값을 검토하는 유틸리티

tests.js

export function expect(received) {
  // 전달값과 비교할 수 있는 유틸리티 모음 객체 반환
  return {
    toBe(expected) {
      // 전달값과 기대값을 비교해서 같지 않으면 오류 발생
      if (received !== expected) {
        throwError(`${received}${expected} 값은 동일하지 않습니다.`);
      }
    },
    toBeTruthy() {
      if (received !== true) {
        throwError(`${received}의 값은 true가 아닙니다.`);
      }
    },
    toBeFalsy() {
      if (received !== false) {
        throwError(`${received}의 값은 false가 아닙니다.`);
      }
    },
    toBeInTheDocument() {
      if (!document.body.contains(received)) {
        throwError(`${received}는 문서에 포함되지 않습니다.`);
      }
    },
    not: {
      toBe(expected) {
        if (received === expected) {
          throwError(`${received}${expected} 값이 동일합니다.`);
        }
      },
    },
    toHaveClass(expected) {
      if (!received.classList.contains(expected)) {
        throwError(
          `${received} 요소는 ${expected} 클래스를 포함하고 있지 않습니다.`
        );
      }
    },
  };
}

expect라는 함수를 클로저 형태로 만들어 received를 참조하는 함수를 반환시킴

테스트 유틸리티

export function test(description, callback) {
  // 오류 발생 여부 감지
  try {
    callback();
    console.log(`✅ 테스트 성공: ${description}`);
  } catch (error) {
    console.groupCollapsed(`❌ 테스트 실패: ${description}`);
    console.error(error.message);
    console.groupEnd();
  }
}

groupCollapsed란 콘솔을 그룹핑 해주되, 펼쳐지지 않고 접혀있게 만들어주는 것 (아래 스크린샷 참조)
groupCollapsed와 groupEnd사이에 결과를 확인할 수 있음

기술 유틸리티

export function describe(testLabel, callback) {
  console.group(testLabel);
  callback();
  console.groupEnd();
}

group의 경우 펼쳐진 상태로 보여줌

transformText 기능 테스트

transformText.js

export function snakeCase(data) {
  // 데이터가 무엇이 들어올지 모르니까 string으로 우선 변환!
  return data.toString().replace(/\s/g, '_');
}

export function kebabCase(data) {
  return data.toString().replace(/\s/g, '-');
}

export function camelCase(data) {
  return data
    .toString()
    .replace(/\s\w/g, (match) => match.toUpperCase().trim());
}

export function titleCase(data) {
  return data
    .toString()
    .replace(/(^\w|\s\w)/g, (match) => match.toUpperCase().trim());
}

transformText.test.js

import { test, expect, describe } from './tests.js';
import { snakeCase, kebabCase, camelCase, titleCase } from './transformText.js';

const str = 'simple is best';

describe('텍스트 트랜스폼 유틸리티 테스트', () => {
  test('snakeCase(simple is best) --> simple_is_best', () => {
    expect(snakeCase(str)).toBe('simple_is_best');
  });
  test('kebabCase(simple is best) --> simple-is-best', () => {
    expect(kebabCase(str)).toBe('simple-is-best');
  });
  test('camelCase(simple is best) --> simpleIsBest', () => {
    expect(camelCase(str)).toBe('simpleIsBest');
  });
  test('titleCase(simple is best) --> SimpleIsBest', () => {
    expect(titleCase(str)).toBe('SimpleIsBest');
  });
});

DOM 요소 테스트

domTest/index.js

import { describe, test, expect } from '../utils/index.js';

const appNode = document.getElementById('app');

describe('DOM 테스트', () => {
  // 문서의 제목이 "React 앱 개발환경 구성" 인가?
  test('문서의 제목이 "React 앱 개발환경 구성" 인가?', () => {
    expect(document.title).toBe('React 앱 개발환경 구성');
  });
  // 문서에 #app 요소가 존재하는가?
  test('문서에 #app 요소가 존재하는가?', () => {
    expect(appNode).toBeInTheDocument();
  });
  // #app 요소 안에 제목 요소가 포함되어 있나?
  test('#app 요소 안에 제목 요소가 포함되어 있나?', () => {
    const hasHeadlineElement = !!appNode.querySelector('h1,h2,h3,h4,h5,h6');
    expect(hasHeadlineElement).toBeTruthy(); // toBeFalsy()
  });
  // 제목의 내용은 "React 앱 개발"
  test('제목의 내용은 "React 앱 개발"', () => {
    const headlineNode = appNode.querySelector('h1,h2,h3,h4,h5,h6');
    expect(headlineNode.textContent).toBe('React 앱 개발');
  });
  // 제목 요소는 headline 클래스를 포함하는가?
  test('제목 요소는 headline 클래스를 포함하는가?', () => {
    const headlineNode = appNode.querySelector('h1,h2,h3,h4,h5,h6');
    expect(headlineNode).toHaveClass('headline');
  });
});

public/index.html

...
    <div id="app">
      <h1 class="a b c d e f headline">React 앱 개발</h1>
    </div>
...

코드 포맷팅

eslint 개별 설정

설치 명령어

npx eslint --init

npx eslint --help

전역에 설정 안되어있는 경우 npx eslint로 호출하면 불러올 수 있음

eslint package.json 설정

"lint": "eslint ./client --ignore-path .gitignore",
"watch:lint": "esw ./client --watch --color --ignore-path .gitignore",

./client안에 있는 파일들을 모두 eslint로 검사하고, .gitignore파일들을 무시하기 위해 위와 같이 설정함!
watch옵션을 주어서 eslint가 지속적으로 검사할 수 있도록 설정!

eslintrc.cjs

module.exports = {
    "env": {
        "browser": true,
        "es2021": true,
        "node": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended"
    ],
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 13,
        "sourceType": "module"
    },
    "settings": {
      "react": {
        "version": "detect"
      }
    },
    "plugins": [
        "react"
    ],
    "rules": {
      "no-unused-vars": "warn"
    }
};

prettier 개별 설정

설치 명령어

npm i -D prettier

실시간으로 프리티어가 검사하여 다시 작성해주는 npm --> Onchange 패키지

npm i -D onchange

script코드

"format": "prettier --write ./client --ignore-path .gitignore",
"watch:format": "onchange ./client -- npm run format {{changed}}",

.prettierrc.js 생성

module.exports = {
    // 화살표 함수 식 매개변수 () 생략 여부 (ex: (a) => a)
    arrowParens: 'always',
    // 닫는 괄호(>) 위치 설정
    // ex: <div
    //       id="unique-id"
    //       class="contaienr"
    //     >
    bracketSameLine: false,
    // 객체 표기 괄호 사이 공백 추가 여부 (ex: { foo: bar })
    bracketSpacing: true,
    // 행폭 설정 (줄 길이가 설정 값보다 길어지면 자동 개행)
    printWidth: 80,
    // 산문 래핑 설정
    proseWrap: 'preserve',
    // 객체 속성 key 값에 인용 부호 사용 여부 (ex: { 'key': 'xkieo-xxxx' })
    quoteProps: 'as-needed',
    // 세미콜론(;) 사용 여부
    semi: true,
    // 싱글 인용 부호(') 사용 여부
    singleQuote: true,
    // 탭 너비 설정
    tabWidth: 2,
    // 객체 마지막 속성 선언 뒷 부분에 콤마 추가 여부
    trailingComma: 'es5',
    // 탭 사용 여부
    useTabs: false,
  };

그러나 다음과 같은 문제가 발생

아까 package.json에서 type module을 설정해주었기 때문에 그냥 js라고 적으면 esm은 require를 지원하지 않음
prettierrc 파일을 js 파일이 아니라 cjs 파일로 설정해주면 문제가 해결됨

Jest

위에 직접 만든 TDD 유틸리티를 사용하는 것보다 Jest라는 자바스크립트 테스팅 프레임워크가 존재한다. JEST를 설치하고 한번 사용해보자.

jest 설치

npm i -D jest

jest configuration file 생성

npx jest --init

babel을 이용한 자동 자바스크립트 컴파일 패키지

npm i -D babel-jest @babel/core @babel/preset-env

eslint와 jest 같이 사용

npm i -D eslint-plugin-jest

.eslintrc.cjs

module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
    'globals/jest': true,
  },
  extends: [
    'eslint:recommended',
    // "plugin:react/recommended"
    'plugin:jest/recommended',
  ],
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 13,
    sourceType: 'module',
  },
  settings: {
    react: {
      version: 'detect',
    },
    jest: {
      version: require('.jest/package.json').version,
    },
  },
  plugins: ['react', 'jest'],
  rules: {
    'no-unused-vars': 'off',
    'no-console': 'off',
  },
};

위와 같이 설정

jest type 패키지 설치

npm i -D @types/jest

Jest 사용 예시 코드

getRandom.test.js

import { getRandom, getRandomCount } from './getRandom.js';

test('getRandom(10) 실행 결과는 10보다 작다', () => {
  let targetCount = getRandom(10);
  expect(targetCount).toBeLessThan(10);
});
test('getRandom(5, 7) 실행 결과는 5 이상 7 이하이다.', () => {
  const min = 5;
  const max = 7;
  const minmaxValue = getRandomCount(min, max);
  expect(minmaxValue).toBeGreaterThanOrEqual(min);
  expect(minmaxValue).toBeLessThanOrEqual(max);
});

domTest/index.js

import { describe, test, expect } from '../utils/index.js';

const appNode = document.getElementById('app');

describe('DOM 테스트', () => {
  // 문서의 제목이 "React 앱 개발환경 구성" 인가?
  test('문서의 제목이 "React 앱 개발환경 구성" 인가?', () => {
    expect(document.title).toBe('React 앱 개발환경 구성');
  });
  // 문서에 #app 요소가 존재하는가?
  test('문서에 #app 요소가 존재하는가?', () => {
    expect(appNode).toBeInTheDocument();
  });
  // #app 요소 안에 제목 요소가 포함되어 있나?
  test('#app 요소 안에 제목 요소가 포함되어 있나?', () => {
    const hasHeadlineElement = !!appNode.querySelector('h1,h2,h3,h4,h5,h6');
    expect(hasHeadlineElement).toBeTruthy(); // toBeFalsy()
  });
  // 제목의 내용은 "React 앱 개발"
  test('제목의 내용은 "React 앱 개발"', () => {
    const headlineNode = appNode.querySelector('h1,h2,h3,h4,h5,h6');
    expect(headlineNode.textContent).toBe('React 앱 개발');
  });
  // 제목 요소는 headline 클래스를 포함하는가?
  test('제목 요소는 headline 클래스를 포함하는가?', () => {
    const headlineNode = appNode.querySelector('h1,h2,h3,h4,h5,h6');
    expect(headlineNode).toHaveClass('headline');
  });
});
profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글