npm 배포를 위한 보일러플레이트

김동균·2022년 5월 12일
5
post-thumbnail

typescript, react, rollup, jest, npm link, prettier, eslint, husky를 사용하는 npm 라이브러리 배포를 위한 보일러플레이트

💾보일러 플레이트

만들고 싶은 라이브러리가 생겨서, 라이브러리를 제작하였다.
만들기에 앞서 설정하고, 편의성을 추가하는 과정이 복잡하고, 어려웠다.
그래서 나중에 먼 미래에 다른 라이브러리를 만들 나를 위해, 또는 라이브러리를 만들고 싶은 누군가를 위해 보일러플레이트와 간단한 설명을 기록해두고자 한다.

  • 라이브러리는 react components를 제작할 예정이다.
  • 번들링은 rollup을 사용할 예정이다. rollupwebpack번들러와 비교해보면, 장점은 다양하게 있지만 그 중 가장 큰 장점은 코드의 양을 줄여 모듈의 크기를 줄일 수 있다는 것이다.
  • typescript를 사용할 예정이고, 간단한 테스트를 위해 jest를 추가할 것이다.
  • 제작된 라이브러리를 확인하기 위해서 storybook을 사용했었는데 배포 후 확인해 본 결과와 다른 경우가 있어 npm link를 이용하여 배포 전 완성도 테스트를 할 것이다.
  • 여러 사람과 제작하기 좋게 prettier, eslint를 추가할 것이다.
  • 마지막으로 husky를 추가할 것이다.

1. package.json init

먼저 터미널에 다음 명령어를 입력하여 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를 해준다.

2. rollup 설정

// 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를 간단히 설명하면

  • index.ts파일을 불러와서 빌드하고 dist/index.cjs.js, dist/index.esm.js, dist/index.d.ts로 추출한다.
  • rollup-plugin-peer-deps-external: peerDependencies 사용
  • @rollup/plugin-node-resolve: 외부 모듈 사용 시 다양한 설정을 할 수 있다. 우리는 기본 설정을 사용할 것이다.
  • @rollup/plugin-commonjs: node_module폴더에 있는 패키지 중 대부분이 CommonJs를 사용하므로 추가한다.
  • @rollup/plugin-typescript: typescript 사용
  • @rollup/plugin-alias: d.ts 파일에서 절대 경로 설정해준다.
    📌 적용이 안 돼서 라이브러리 제작 시 절대 경로 다 뺐어요... 혹시 적용시키는 방법 아시면 댓글로 남겨주세요!
  • rollup-plugin-postcss: postcss 사용
  • rollup-plugin-dts: d.ts 파일 생성

📌 rollup-plugin-postcss

설정 시 아래와 같이 extract 설정 추가 없으면 해당 라이브러리 사용하여 다른 프로젝트 빌드 시 에러 날 수 있다.

postcss({
  extract: true,
  extract: 'index.css',
}),

하지만 해당 설정을 추가하였다면 antd 라이브러리를 사용할 때처럼 라이브러리의 css 적용을 위해서 다음과 같이 import를 추가해줘야 한다.

import 'npm-library-name/dist/index.css'

3. ts 설정

위에서 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"
  ]
}

4. 예시 코드 작성

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 폴더에 생성되는 것을 볼 수 있다.

💻npm link를 이용하여 라이브러리 확인

간단하게 vitecra로 프로젝트를 만들고

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 가 겹쳐서 에러 날 수 있다.

5. jest 설정

먼저 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다. 커버리지의 문턱 값을 설정할 수 있다. 여기서 커버리지란, 전체 코드 중 어떤 부분이 테스트 되고 어떤 부분이 테스트 되지 않았는지를 비율을 알려주는 기능이다.

🎨test 코드 작성

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를 사용해보면 테스트 결과와 함께 표가 하나 출력된다.

표의 열을 보면 각각

  • File (파일과 폴더 트리)
  • % Stmts (구문 비율)
  • % Branch (if문 등의 분기점 비율)
  • % Func (함수 비율)
  • % Lines (코드 줄 수 비율)
  • Uncovered Line #s (커버되지 않은 줄 위치)
    로 이우러져 있다. 퍼센티지가 높을 수록 많은 코드가 테스트되었다는 뜻이다.

jest.config.js에서 설정한 coverageThreshold 값에 도달하지 못한 것을 볼 수 있다.

여기선 명시적으로 테스트하고 require한 코드만 커버리지 분석이 된다는 점을 주의해야 한다.

출처: https://inpa.tistory.com/entry/JEST-📚-테스트-커버리지-Test-Coverage [👨‍💻 Dev Scroll]

6. prettier 설정

라이브러리를 설치한다.

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",
},

7. eslint 설정

마찬가지로 라이브러리를 설치한다.

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 추가 설정

좀 더 엄격하게 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',
      },
    ],
  }
}

8. husky 설정

husky 라이브러리를 설치하고, commit 전에 npm test를 자동으로 실행해주게 하자.

npm i -D husky
npx husky install
npx husky add .husky/pre-commit "npm test"

npx lint-staged도 추가해주자. lint-stagedgit 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"

😥에러 1


📌.git 폴더가 있어야 한다.

😥에러 2


📌 node 버전이 옛날 버전일 경우 발생할 수 있다. node 버전을 업데이트 하거나 아니면 다음과 같이 명령어를 사용하면 된다.

node node_modules/husky/lib/bin.js add .husky/pre-commit "npm test"

9. 마무리

모든 설정이 완료되었다. 담고 싶은 것이 많다보니 글이 길어졌다.
직접 하나씩 따라해보면서 해봐도 좋고, 아래 git 주소로 boilerplateclone해서 사용해도 좋다.
라이브러리 동작 확인은 💻npm link를 이용하여 라이브러리 확인 내용을 따라 하거나 npm publish 하여 확인하면 된다.
마지막으로 이 글을 작성하게 만든 react-cropper-custom을 사용해 봐주세요..

npm-publish-rollup-boilerplate

react-cropper-custom 새로운 라이브러리

profile
초보 개발자

0개의 댓글