들어가며
기업 과제에서 필수 요구 사항이었던 테스트에 어려움을 겪고, 글또에서 진행하는 TDD 스터디에 참여하면서 프로젝트 환경에 따른 Jest 설정에 대해 공부하였습니다. 이 과정에서 그동안 대략적으로만 알고 사용했던 Vite, TypeSciprt, ESlint의 환경 설정과 새롭게 알게 된 Jest, React Testing Library(이하 RTL) 관련 설정을 전체적으로 살펴 보고 이해하는 시간을 가졌습니다. 이를 통해 ESM과 CJS의 차이를 이해하고, 설치하는 패키지 간 의존성과 버전 호환의 중요성을 깨닫게 되었습니다.
해당 포스팅에서는 [1편]에서 먼저 각 패키지를 설정할 때 알아두면 유용한 점들과 주의할 점들을 살펴 보고, [2편]에서 패키지 간 의존성과 패키지 버전 및 설정 파일 문제로 발생했던 에러 및 해결책을 공유하고자 합니다.
사전 지식: ESM과 CJS 이해하기
다양한 패키지의 설정을 들여다보니 기존에 사용하던 CommonJS 모듈(CJS)과 최신 ECMAScript Module 모듈(ESM)을 어떻게 호환하여 잘 사용할 것인지에 대한 내용들을 계속해서 마주했습니다. 따라서 아래 내용을 훑어보고 가면 이해에 도움이 될 것입니다.
CJS와 ESM은 자바스크립트에서 사용되는 두 가지 모듈 시스템입니다. 둘은 크게 1. 문법과 2. 사용 환경에서 차이를 보입니다.
CJS
- 주로 Node.js와 같은 서버 환경에서 사용되고, require/module.exports로 모듈을 불러오고 내보냅니다.
// import
const myModule = require('./myModule');
// export
module.exports = { someFunction };
ESM
- 브라우저와 서버 환경 모두에서 사용되며, import/export 로 모듈을 불러오고 내보냅니다.
// import
import myModule from './myModule';
// export
export const someFunction = () => { /* ... */ };
조금 더 자세한 차이는 아래와 같습니다.
CommonJS | ESModules | |
문법 | 모듈 가져오기: require 모듈 내보내기: module.exports |
모듈 가져오기: import 모듈 내보내기: export |
로딩 | 동기적으로 모듈 로드. 작성된 순서대로 실행 |
동기/비동기 로드 모두 지원. import()를 통한 비동기 로딩 지원 |
범위 | - 모듈은 자신만의 스코프를 가진다. - 모듈 내부의 변수와 함수는 명시적으로 내보내지 않은 이상 해당 모듈에서만 사용 가능 |
- CJS의 모듈보다 정적이고 명시적인 스코프를 가진다. - 명시적으로 내보내고 가져오지 않으면 변수와 함수들이 자동으로 공유되지 않는다. |
default exports | 가져오기: require(module_path) 내보내기: module.exports = module_name |
가져오기: import A from module_path 내보내기: export default A |
다양성 | 서버 환경을 위해 디자인되어 동기적 특성을 가지고, 서버 어플리케이션 사용에 적합 | 유연하게 디자인되어 서버와 클라이언트 어플리케이션 모두에 사용 가능(비동기, 동기 모두 가능) |
브라우저 지원 | - 브라우저 지원 X - Webpack, Browserify 같은 번들러 사용하면 브라우저 지원 가능 |
- 모던 브라우저에서 사용 가능 - 기본 ESM에는 번들러가 필요없지만, 다른 기능을 위해 사용 |
사용 환경 | 주로 서버 사이드 환경에서 사용됨 ex) Node.js | 서버와 클라이언트 사이드 개발의 표준이 되어가는 중 |
Vite 시작하기
Vite는 모든 최신 JavaScript 및 CSS 기능을 지원하는 브라우저를 사용하고 있다고 가정하고 기본적으로 TypeScript 트랜스파일링의 target으로 ESNext를 사용합니다. 이는 TypeScript 컴파일러에게 최신 ECMAScript 문법과 기능을 사용해 Javacript 코드를 생성하라고 알려주는 것인데, 이러한 Vite의 기본 설정은 tsconfig.json의 target 값을 무시합니다. 명시적으로 다른 target을 설정하려면 vite.config.js에서 esbulid.target 또는 build.target을 설정해야 합니다.
이는 모던 웹 개발 관행에 초점을 맞추어 모던 ECMAScript와 브라우저의 기능을 이용하고자 하는 Vite의 디자인 철학에 바탕을 두고 있습니다. 브라우저가 최신 버전이 아닌 ECMAScript도 지원하고 있는데 굳이 최신 버전의 ECMAScript를 사용하려는 이유에 대한 의문이 들 수 있습니다. 이는 최신 버전의 ECMAScript를 통해 개발자들이 모던 JavaScript 엔진에 최적화된 코드를 작성하고, 옵셔널 체이닝이나 nullish 연산자 등을 사용해 보다 효율적이고 유지 보수하기 좋은 코드를 작성할 수 있기 때문입니다.
tsconfig.json과 tsconfig.node.json의 차이점?
React + TypeSciprt 기반의 Vite를 설치하면 기본적으로 프로젝트 내에 .eslinltrc.cjs, tsconfig.json, tsconfig.node.json, vite.config.ts 설정 파일들이 생성됩니다. 이 가운데 tsconfig.json과 tsconfig.node.json의 차이를 살펴보겠습니다.
- tsconfig.json
- 주요 TypeScript 설정 파일로, 클라이언트 코드 개발 환경(브라우저)에 사용
- 브라우저와 호환될 ECMAScript 타겟 버전, 모듈 시스템과 같은 컴파일러 옵션을 명시
- React나 Vue를 사용할 경우 JSX 처리 방법을 명시
- tsconfig.node.json
- 서버 환경(Node.js)에 사용
- Node.js 환경의 ECMAScript 버전과 모듈 시스템 명시
- 주로 메인 파일인 tsconfig.json에서 reference에 명시하는데, 이를 통해 기본 세팅을 확장하여 override 가능
Vite에서 tsconfig.json은 브라우저에서 실행될 앱에 적용되고, tsconfig.node.json은 Node에서 실행될 vite 자체에 적용되는 TypeScript 설정입니다.
TypeScript 설정 이해하기
tsconfig는 TypeScript를 JavaScript로 트랜스파일할 때 적용할 설정을 정의하는 파일입니다. 이때 변환할 파일의 폴더 내에서 tsconfig를 찾고, 없으면 상위로 올라가면서 설정 파일을 찾습니다. 기본적으로 node_modules는 제외하지만, third-party 라이브러리의 타입을 정의하는 @types 폴더는 컴파일에 포합됩니다.
└─ node_modules
├─ @types => 컴파일에 포함
├─ lodash => 컴파일에서 제외
tsconfig 파일은 아래와 같습니다.
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"types": ["@testing-library/jest-dom"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"esModuleInterop": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["frontend/src", "frontend/test"],
"references": [{ "path": "./tsconfig.node.json" }],
"files": ["frontend/jest.setup.ts"]
}
주요 속성들에 대해 알아보겠습니다.
- target: 컴파일 할 JavaScript 버전
- ES3, ES5, ES6, ESNext 등
- 이 값에 따라 자동으로 필요한 lib 및 module이 지정됨
- lib: 컴파일 시 포함될 빌트인 타입 그룹 리스트. TypeScript 컴파일러에 이러한 문법을 사용할 것이라고 명시하는 것
- es2015: Promise 객체 인식
- dom: DOM API 사용하는 경우 필요
- module: 자바스크립트 파일 간 import 할 때 사용할 문법
- CommonJS(기본값) : require 문법. target이 ES3, ES5인 경우 기본값
- ES6, ES2015, ES2020, ESNext: import 문법. target이 ES6 이상인 경우 기본값
- 이외에도 AMD, UMD, System 등이 있다.
- moduleResolution: import한 모듈이 무엇을 참조하는지 확인하는 과정. module에 따라 moduleResolution 프로퍼티의 기본값이 지정됨
- Node: module이 CommonJS인 경우 기본값
- Classic: module이 CommonJS가 아닌 경우 기본값
- bundler: TypeScript 5.0이상부터 사용 가능하며, TypeScript에게 코드가 다른 툴에 의해 번들링 될 것이라고 알려주고, 이에 맞게 규칙을 완화해주는 역할. vite는 bundler를 기본 옵션으로 사용.
- resolveJsonModule: *.json 확장자 모듈의 import를 허용하는 설정. JSON 모듈을 가져와서 사용하려면 true로 설정
- noEmit: 타입스크립트를 컴파일하면 JavaScript 변환 파일을 만들어내지 않도록 하는 설정.
- true인 경우, TypeScript를 에디터 기능 제공 용도 또는 소스 코드 타입을 확인하는 용도로 사용한다는 의미.
- Vite, Babel과 같은 다른 도구가 컴파일을 하는 경우에 해당.
- jsx: tsx를 어떻게 컴파일할지 결정
- react: .js 파일로 컴파일(React.createElement() 호출)
- react-jsx: .js 파일로 컴파일(_jsx() 함수 호출)
- preserve: .jsx 파일로 컴파일(jsx 코드 그대로 유지)
- esModuleInterop: CommonJS 모듈을 ES6 모듈 코드로 import 할 때 사용
- allowSyntheticDefaultImports: default import가 없는 모듈로부터 default import를 허용하는 설정. 타입체킹만 하고 code emit에는 영향을 미치지 않음. esModuleInterop을 true인 경우 true.
- include: 변환할 폴더 지정
esModuleInterop에 대하여
기본적으로 TypeScript는 CommonJS/AMD/UMD 모듈을 ES6 모듈로 취급합니다. 이때 CommonJS 모듈을 형식인 const moment = require("moment")와 ES6 모듈 형식인 import * as moment from "moment" 또는 import moment from "moment"가 같지 않기 때문에 CommonJS/AMD/UMD 모듈을 TypeScript 프로젝트로 가져올 때 문제가 발생합니다.
esModuleInterop을 true로 설정하면, 컴파일러의 동작을 변경하고, 2가지의 헬퍼 함수를 사용해 이 문제를 해결합니다. 헬퍼 함수 중 하나인 __importDefault 함수를 살펴보면, ES6 모듈은 그대로 가져오고, CommonJS 모듈은 default 키로 감싸서 CommonJS 모듈도 default import할 수 있도록 합니다.
var __importDefault = (this && this.__importDefault) || function(mod) {
return (mod && mode.__esModule)? mod : { "default" : mod };
};
즉, CommonJS 모듈을 ES6 모듈 스펙에서 사용할 수 있게 됩니다.
// CommonJS 모듈 불러오기
import moment from 'moment'
moment();
// esModuleInterop이 true인 경우 모듈을 불러오면 아래처럼 변환되어 사용 가능
const moment = __importDefault(require('moment'));
moment.default();
ESlint 설정 이해하기
ESlint는 일관적인 코드 스타일을 유지하기 위해 사용하며, tsconfig와 마찬가지로 현재 lint 대상의 파일이 위치한 폴더 안에 설정 파일이 있는지 확인하고, 없으면 상위 폴더로 거슬러 올라가면서 찾습니다.
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
},
};
- root
- true: 더 이상 상위 폴더로 찾으러 올라가지 않도록 설정.
- 하나의 프로젝트 내에 다른 설정 파일을 적용하려면 각 어플리케이션(또는 폴더)의 설정 파일은 fale로, 최상위 경로에는 true로 설정하여, 공통 설정과 프로젝트별 설정을 분리해서 관리할 수 있다.
- plugins : 기본 제공 규칙 이외 추가적으로 사용하는 규칙
- 먼저 해당 플러그인을 설치하고 extends 또는 rules 옵션에 추가 설정 필요
- eslint-plugin-**
// 설치 $ npm i -D eslint-plugin-import eslint-plugin-react // 설정 적용 { "plugins": ["import", "react"] }
- extends: 기업이 공개해놓은 설정 또는 ESlint plugin의 추천 설정을 base로 설정
- eslint-config-**
- rules: 규칙을 세세하게 제어, 일반적으로 extends 옵션을 통해 설정된 규칙을 덮어쓸 때 유용하게 사용
- extends 규칙보다 우선
- env : 접근 가능한 모든 전역 객체를 등록
- ESlint는 기본적으로 미리 선언하지 않은 변수에 대해 오류를 내기 때문에 실행 환경(런타임)에서 기본적으로 제공되는 전역 객체에 대해 설정을 통해 알려주어야 합니다.
- 전역 객체를 등록할 환경 명시 : browser, node
- parser, parserOptions
- ESlint 는 기본적으로 순수 js만 이해할 수 있기 때문에 JavaScript 최신문법이나 TypeScript를 lint하려면 해당하는 parser를 설정해주어야 합니다.
// ts
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 11,
}
}
// babel
{
"parser": "@babel/eslint-parser"
}
Jest 설정 이해하기
Jest에서 TypeScript를 사용하기 위한 2가지 방법
- @babel/preset-typescript
npm install --save-dev @babel/preset-typescript
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};
- Babel의 역할은 TypeScript를 JavaScript로 트랜스파일링하는 것이기 때문에 tsc처럼 완전한 타입체크를 하지 않습니다. 완전한 타입 체킹을 하기 위해서는 ts-jest를 사용하거나 TypeScript 컴파일러 tsc를 따로 실행해야 합니다.
- ts-jest
npm install --save-dev ts-jest
- ts-jest는 Jest 소스맵을 가진 TypeScript 전처리기로, 이를 통해 JavaScript를 지원하는 Jest가 TypeScript를 이해하도록 컴파일합니다.
- 사용하는 Jest와 호환되는 버전을 설치해야 합니다.
Jest global API를 타이핑하는 방법 2가지
1. @jest/global 설치하고, 필요한 api import 해서 사용
npm install --save-dev @jest/globals
import {describe, expect, test} from '@jest/globals';
import {sum} from './sum';
describe('sum module', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
});
2. @types/jest 패키지 사용
npm install --save-dev @types/jest
- @types/jest는 DefinitelyTyped에서 관리되고 있는 third-party 라이브러리로, 최신 Jest 기능이나 버전들이 반영되지 않았을 수 있어 Jest 버전을 해당 패키지와 최대한 유사하게 맞춰야 합니다.
- 예를 들어, 만약 Jest 27.4.0을 사용하고 있으면, @types/jest 27.4.x를 사용하는 것이 좋습니다.
RTL(React Testing Libraray) 설정 이해하기
React Testing Library는 Behavior Driven Test(BDT)를 기반으로 실제 사용자 경험과 유사한 방식의 테스트를 작성하는 라이브러리입니다. React 테스트 사용 시 RTL과 Jest를 함께 사용합니다. RTL은 브라우저 없이 테스트하기 위한 가상 DOM을 제공하고, Jest는 테스트를 찾고, 실행하고, 테스트의 실패 또는 성공 여부를 결정하는 test runner 역할을 합니다.
Jest 버전에 따라 RTL로 테스트 작성 시 jsdom을 사용하기 위해 다른 설정이 필요합니다.
npm install --save-dev @testing-library/react
Jest 28 이상
- jest-environment-jsdom 설치 필수
npm install --save-dev jest-environment-jsdom
// jest.config.js module.exports = { + testEnvironment: 'jsdom', // ... other options ... }
- jsdom이 더 이상 기본 환경이 아니기 때문에 jest.config.js를 수정해 jsdom을 전역적으로 사용할 수 있습니다.
Jest 27
module.exports = {
+ testEnvironment: 'jest-environment-jsdom',
// ... other options ...
}
- 마찬가지로 jsdom이 더 이상 기본 환경이 아니기 때문에 jest.config.js를 수정해 jsdom을 전역적으로 사용할 수 있습니다.
Jest 24 이하 버전
npm install --save-dev jest-environment-jsdom-fifteen
module.exports = {
+ testEnvironment: 'jest-environment-jsdom-fifteen',
// ... other options ...
}
- jest-environment-jsdom-fifteen 패키지 사용 추천
정리
이번 포스팅에서는 Vite, TypeScript, ESlint, Jest, React Testing Library의 각 환경 설정에 필요한 내용들을 알아보았습니다. 프로젝트에서는 여러 가지 패키지를 설치해 사용하므로 종속성 버전 간 충돌에 유의하여 사용해야겠습니다. 다음 포스팅에서는 이러한 내용을 바탕으로 의존성 간 버전 충돌과 설정 파일 문제로 발생했던 에러에 대한 해결책을 공유하고자 합니다.
REF
https://testing-library.com/docs/react-testing-library/setup
https://tecoble.techcourse.co.kr/post/2021-10-22-react-testing-library/
https://jestjs.io/docs/getting-started#using-vite
https://jestjs.io/docs/tutorial-react
https://www.daleseo.com/eslint-config/
'FrontEnd > Test' 카테고리의 다른 글
프론트엔드 테스트 어떤 순서로, 무엇을 테스트하면 좋을까? (23) | 2024.03.31 |
---|---|
프론트엔드 Vite 프로젝트에 Vitest 적용하기 (1) | 2024.01.20 |
Vite, TypeScript, React Testing Library, Jest 설정하기 - (2) React 테스트 환경 설정 및 Jest 에러 해결 (32) | 2024.01.07 |