typescript
,react
,rollup
,jest
,npm link
,prettier
,eslint
,husky
를 사용하는 npm 라이브러리 배포를 위한 보일러플레이트
만들고 싶은 라이브러리가 생겨서, 라이브러리를 제작하였다.
만들기에 앞서 설정하고, 편의성을 추가하는 과정이 복잡하고, 어려웠다.
그래서 나중에 먼 미래에 다른 라이브러리를 만들 나를 위해, 또는 라이브러리를 만들고 싶은 누군가를 위해 보일러플레이트와 간단한 설명을 기록해두고자 한다.
react components
를 제작할 예정이다. rollup
을 사용할 예정이다. rollup
을 webpack
번들러와 비교해보면, 장점은 다양하게 있지만 그 중 가장 큰 장점은 코드의 양을 줄여 모듈의 크기를 줄일 수 있다는 것이다.typescript
를 사용할 예정이고, 간단한 테스트를 위해 jest
를 추가할 것이다.npm link
를 이용하여 배포 전 완성도 테스트를 할 것이다.prettier
, eslint
를 추가할 것이다.husky
를 추가할 것이다.먼저 터미널에 다음 명령어를 입력하여 package.json 파일을 만들어준다.
npm init
여러 질문들에 대답을 하고, 마지막에 yes
하면 된다.
그리고 다음 모듈들을 설치해준다.
// rollup
npm i -D rollup rollup-plugin-peer-deps-external @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript @rollup/plugin-alias rollup-plugin-postcss rollup-plugin-dts
// typescript
npm i -D typescript @types/react @types/react-dom tslib
// postcss
npm i -D postcss
그리고 packjson 파일에 다음 내용들을 추가해준다.
"main": "./dist/index.cjs.js",
"module": "./dist/index.esm.js",
"types": "./dist/index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rollup -c",
"watch": "rollup -cw"
},
"peerDependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
peerDependencies
로 설정한 라이브러리를 받기 위해 npm i
를 해준다.
// rollup.config.js
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import alias from '@rollup/plugin-alias';
import postcss from 'rollup-plugin-postcss';
import dts from 'rollup-plugin-dts';
import path from 'path';
const packageJson = require('./package.json');
export default [
{
input: 'src/index.ts',
output: [
{
file: packageJson.main,
format: 'cjs',
sourcemap: true,
},
{
file: packageJson.module,
format: 'esm',
sourcemap: true,
},
],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
postcss({
extract: true,
extract: 'index.css',
}),
typescript()
],
},
{
input: 'src/index.ts',
output: [{ file: 'dist/index.d.ts', format: 'cjs' }],
plugins: [
dts(),
alias({
entries: [{ find: '@', replacement: path.resolve(__dirname, 'src') }],
}),
postcss({
extract: true,
extract: 'index.css',
}),
],
},
];
위의 config를 간단히 설명하면
dist/index.cjs.js
, dist/index.esm.js
, dist/index.d.ts
로 추출한다.설정 시 아래와 같이 extract 설정 추가 없으면 해당 라이브러리 사용하여 다른 프로젝트 빌드 시 에러 날 수 있다.
postcss({
extract: true,
extract: 'index.css',
}),
하지만 해당 설정을 추가하였다면 antd
라이브러리를 사용할 때처럼 라이브러리의 css
적용을 위해서 다음과 같이 import
를 추가해줘야 한다.
import 'npm-library-name/dist/index.css'
위에서 ts에 필요한 모듈은 모두 설치했기 때문에 tsconfig.json 파일만 추가해주면 된다.
// tsconfig.json
{
"compilerOptions": {
"typeRoots": [
"./node_modules/@types",
"./src/types"
],
"target": "es5",
"lib": [
"dom",
"esnext"
],
"jsx": "react",
"module": "es6",
"moduleResolution": "node",
"baseUrl": "./",
"strict": true,
"esModuleInterop": true,
"outDir": "dist",
"paths": {
"@components/*": ["src/components/*"],
"@style/*": ["src/style/*"],
"@hooks/*": ["src/hooks/*"],
"@utils/*": ["src/utils/*"],
"@src/*": ["src/*"]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
src
폴더를 만들고, 내부에 index.ts
, Hello.ts
파일을 만든다.
// src/index.ts
export * from './Hello';
//src/Hello.ts
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
export const Hello = () => {
const [state, setState] = useState<string>("");
useEffect(() => {
setState("Hello");
}, []);
return <div>{state} World!!</div>;
};
다음 명령어에
npm run build
여러 개의 파일이 dist
폴더에 생성되는 것을 볼 수 있다.
간단하게 vite
나 cra
로 프로젝트를 만들고
npm create vite@latest
npm i
npm i file:../npm-library-name
npm link
npm link npm-library-name
npm link로 제작한 라이브러리와 연결하여 node_module에서 라이브러리를 확인해보면 다음과 같이 심볼 표시가 붙으며 연결된 것을 볼 수 있다.
아래와 같이 하여 코드를 짠 후, npm run dev
해보면 잘 실행되는 것을 볼 수 있다.
// src/App.tsx
import "./App.css";
import { Hello } from "npm-publish-rollup-boilerplate";
function App() {
return (
<div className="App">
<Hello />
</div>
);
}
export default App;
라이브러리와 vite
프로젝트가 연결되어 있기 때문에 라이브러리에서
npm run watch
로 실행하여 라이브러리를 수정하면 실시간으로 vite
프로젝트에 반영되는 것을 볼 수 있다.
📌 프로젝트에 link한 라이브러리 포함하여 빌드 시 @types/react 가 겹쳐서 에러 날 수 있다.
먼저 jest에 필요한 라이브러리를 설치한다.
npm i -D jest ts-jest enzyme @types/enzyme @testing-library/jest-dom @wojtekmaj/enzyme-adapter-react-17 react react-dom
jest
에서 react
, react-dom
을 사용할 예정이라 같이 추가해줘야 한다.
// jest.config.js
const SRC_PATH = '<rootDir>/src';
module.exports = {
preset: 'ts-jest',
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: -10,
},
},
coverageDirectory: 'coverage',
moduleDirectories: ['node_modules', 'src'],
setupFilesAfterEnv: ['<rootDir>/src/test/setupTests.ts'],
modulePaths: ['<rootDir>'],
roots: [SRC_PATH],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
moduleNameMapper: {
'^@src(.*)$': '<rootDir>/src$1',
'^@components(.*)$': '<rootDir>/src/components$1',
'^@style(.*)$': '<rootDir>/src/style$1',
'^@hooks(.*)$': '<rootDir>/src/hooks$1',
'^@utils(.*)$': '<rootDir>/src/utils$1',
'\\.(css|scss)$': '<rootDir>/mocks/styleMock.js',
},
testEnvironment: 'jsdom'
};
// pakage.json
"scripts": {
"test": "jest",
"corverage": "jest --coverage",
...
},
// mocks/styleMock.js
module.exports = {};
설정에서 눈여겨 볼 것은 coverageThreshold
다. 커버리지의 문턱 값을 설정할 수 있다. 여기서 커버리지란, 전체 코드 중 어떤 부분이 테스트 되고 어떤 부분이 테스트 되지 않았는지를 비율을 알려주는 기능이다.
components 렌더링 시 에러 확인을 위한 코드를 작성해보자.
// src/test/setupTests.ts
import '@testing-library/jest-dom';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import { configure } from 'enzyme';
import React from 'react';
React.useLayoutEffect = React.useEffect;
configure({ adapter: new Adapter() });
// src/test/TestComponents.test.tsx
import React from 'react';
import * as ReactDOM from 'react-dom';
import { Hello } from 'src/Hello';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<Hello />, div);
});
npm run coverage
를 사용해보면 테스트 결과와 함께 표가 하나 출력된다.
표의 열을 보면 각각
jest.config.js에서 설정한 coverageThreshold
값에 도달하지 못한 것을 볼 수 있다.
여기선 명시적으로 테스트하고 require한 코드만 커버리지 분석이 된다는 점을 주의해야 한다.
출처: https://inpa.tistory.com/entry/JEST-📚-테스트-커버리지-Test-Coverage [👨💻 Dev Scroll]
라이브러리를 설치한다.
npm i -D prettier
포맷을 체크한다.
npx prettier --check ./src
포맷팅🙂
npx prettier --write ./src
앞에서 살펴본 check 명령어와 write 명령어를 점 더 쉽게 사용하기 위해 package.json 파일을 열고 다음과 같이 수정한다.
"scripts": {
...
"format": "prettier --check ./src",
"format:fix": "prettier --write ./src",
},
마찬가지로 라이브러리를 설치한다.
npm i -D eslint eslint-plugin-functional
.eslintrc.js
파일을 만들고, 다음을 추가한다.
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
project: './tsconfig.json',
},
plugins: ['react', '@typescript-eslint', 'functional'],
settings: {
react: {
version: 'detect',
},
},
rules: {
// General
'no-console': 'error',
// TypeScript
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/indent': 'off',
'@typescript-eslint/member-delimiter-style': 'off',
'@typescript-eslint/no-confusing-void-expression': [
'error',
{ ignoreArrowShorthand: true, ignoreVoidOperator: true },
],
'no-duplicate-imports': 'off',
'@typescript-eslint/no-duplicate-imports': 'error',
'@typescript-eslint/no-implicit-any-catch': 'error',
'no-invalid-this': 'off',
'@typescript-eslint/no-invalid-this': 'error',
'@typescript-eslint/no-invalid-void-type': 'error',
'no-loop-func': 'off',
'@typescript-eslint/no-loop-func': 'error',
'no-loss-of-precision': 'off',
'@typescript-eslint/no-loss-of-precision': 'error',
'@typescript-eslint/no-parameter-properties': 'off',
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'error',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
'no-throw-literal': 'off',
'@typescript-eslint/no-throw-literal': 'error',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
'@typescript-eslint/no-unnecessary-condition': 'error',
'@typescript-eslint/no-unnecessary-type-arguments': 'error',
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': 'error',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-use-before-define': ['error', { variables: false }],
'@typescript-eslint/prefer-enum-initializers': 'error',
'@typescript-eslint/prefer-for-of': 'error',
'@typescript-eslint/prefer-includes': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/prefer-reduce-type-parameter': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
'@typescript-eslint/prefer-ts-expect-error': 'error',
'@typescript-eslint/promise-function-async': 'error',
'no-return-await': 'off',
'@typescript-eslint/return-await': 'error',
'@typescript-eslint/strict-boolean-expressions': 'error',
'@typescript-eslint/switch-exhaustiveness-check': 'error',
// React
'react/jsx-boolean-value': 'warn',
'react/jsx-curly-brace-presence': 'warn',
'react/jsx-fragments': 'warn',
'react/jsx-no-useless-fragment': 'warn',
'react/jsx-uses-react': 'off',
'react/prefer-stateless-function': 'warn',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
// Functional
'functional/prefer-readonly-type': [
'warn',
{
allowLocalMutation: true,
allowMutableReturnType: true,
ignoreClass: true,
},
],
},
};
// package.json
{
...
"eslintIgnore": [
"jest.config.js",
".eslintrc.js",
"rollup.config.js"
],
}
좀 더 엄격하게 eslint를 적용하고 싶으면, 라이브러리를 더 설치하고, .eslintrc.js
파일을 다음과 수정하면 된다.
npm i -D eslint-config-airbnb eslint-config-prettier eslint-plugin-prettier eslint-plugin-import eslint-plugin-react-hooks eslint-plugin-jsx-a11y eslint-import-resolver-typescript
// .eslintrc.js
module.exports = {
...
env: {
browser: true,
es2021: true,
jest: true,
},
extends: [
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'airbnb',
'airbnb/hooks',
'prettier',
],
plugins: ['react', '@typescript-eslint', 'prettier', 'import', 'functional'],
settings: {
react: {
version: 'detect',
},
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
typescript: {},
},
},
rules: {
...
// import
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
'import/prefer-default-export': 'off',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
}
}
husky 라이브러리를 설치하고, commit
전에 npm test
를 자동으로 실행해주게 하자.
npm i -D husky
npx husky install
npx husky add .husky/pre-commit "npm test"
npx lint-staged
도 추가해주자. lint-staged
는 git add
명령어로 커밋 대상이 된 파일에 대해 우리가 설정해둔 명령어를 실행하는 것을 말한다.
// package.json
{
...
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"prettier --write",
"eslint --fix"
]
},
}
npm i -D lint-staged
npx husky add .husky/pre-commit "npx lint-staged"
📌.git
폴더가 있어야 한다.
📌 node
버전이 옛날 버전일 경우 발생할 수 있다. node
버전을 업데이트 하거나 아니면 다음과 같이 명령어를 사용하면 된다.
node node_modules/husky/lib/bin.js add .husky/pre-commit "npm test"
모든 설정이 완료되었다. 담고 싶은 것이 많다보니 글이 길어졌다.
직접 하나씩 따라해보면서 해봐도 좋고, 아래 git
주소로 boilerplate
를 clone
해서 사용해도 좋다.
라이브러리 동작 확인은 💻npm link를 이용하여 라이브러리 확인 내용을 따라 하거나 npm publish
하여 확인하면 된다.
마지막으로 이 글을 작성하게 만든 react-cropper-custom
을 사용해 봐주세요..