FE 개발자를 위한 안드로이드 후려치기 #2 자바스크립트 인터페이스 만들기
리액트 컴포넌트 라이브러리 만들기 (Typescript + Rollup + React)
2023-05-07
Explanation
엄청청 오랜만의 포스팅이네요.. 요즘 회사일이 너무 바빠서.. (는 거짓말)
작년 말에 내심 내년에는 한달에 하나씩은 꼭 블로그에 포스팅 해야지 라고 생각했는데, 벌써 5월인 것을 보니 생각만 하고 어디에 적지 않아서 다행이..
최근에 리액트 라이브러리를 만들고 있는데요, 초반에 리액트 컴포넌트 라이브러리를 만들다가 애먹었던 부분들을 조금 정리해 보려합니다!
역시나 모든 코드는 깃허브에 공개되어 있기 때문에, 바로 그냥 코드를 보고 참고하실 분은 아래 링크를 참고해주세요.
https://github.com/falsy/blog-post-example/tree/master/react-component-library
https://github.com/falsy/blog-post-example/tree/master/react-component-library-test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// packages.json { "name": "react-component-library", ... "devDependencies": { "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^11.0.0", "@types/react": "^18.0.0", "@types/rollup-plugin-peer-deps-external": "^2.2.1", "node-sass": "^8.0.0", "react": "^18.0.0", "rollup": "^3.20.2", "rollup-plugin-dts": "^5.3.0", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "tslib": "^2.5.0", "typescript": "^4.6.2" }, "peerDependencies": { "react": ">=18.0.0" }, "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "dist/index.d.ts", "files": [ "dist" ] ... } |
대략적으로 역시 딱봐도 메인은 rollup 이네요.
그리고 react가 중복해서 선언되지 않도록 peerDependencies 로 설정했어요.
그리고 typescript와, 스타일로 scss를 사용하기 위해서 node-sass를 설치했답니다.
그리고 main, module 에 각각 commonjs 형태와 ES모듈 형태의 번들파일의 경로를 설정해주었습니다.
가장 먼저 저는 라이브러리를 만들기 위해 번들러를 알아보다가 rollup을 알게 되었는데요.
(오래전부터 있던 번들러인데 저는 엄청청 늦게 알았네요…)
그냥 많이 사용하던 Webpack을 사용해도 되지만, 뭔가 라이브러리에서는 Webpack 보단 Rollup을 더 많이 사용하는 거 같더라고요?
애플리케이션 개발엔 Parcel, Webpack 라이브러리 개발엔 Rollup을 사용하는게 중론인듯?
가장 큰 차이는 아마도 Rollup은 ES 모듈 형태로 빌드 결과물을 출력할 수 있다는 점 인거 같아요.
(Webpack 에서도 지원하고 있지만 아직 완전히 지원되고 있지는 않은 것 같아요.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// rollup.config.js import resolve from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' import typescript from '@rollup/plugin-typescript' import peerDepsExternal from 'rollup-plugin-peer-deps-external' import postcss from 'rollup-plugin-postcss' import dts from 'rollup-plugin-dts' const packageJson = require('./package.json') export default [ { input: 'src/index.tsx', // 진입점? output: [ { file: packageJson.main, // commonjs 형태로 번들링 format: 'cjs', sourcemap: true, }, { file: packageJson.module, // ES모듈 형태로 번들링 format: 'esm', sourcemap: true, }, ], plugins: [ peerDepsExternal(), // peerDependencises에 사용된 라이브러리를 번들에서 제외합니다. resolve(), // 외부 노드 모듈을 사용할 수 있게 해줍니다. commonjs(), // commonjs 형태 모듈도 해석할 수 있게 해줍니다. typescript({ tsconfig: './tsconfig.json' }), // 타입스크립트를 사용할 수 있게 해줍니다. postcss({ // sass를 사용할 수 있게 해줍니다. modules: true, use: [ 'sass' ] }) ], }, { input: 'dist/esm/types/index.d.ts', output: [ { file: 'dist/index.d.ts', format: 'esm' } ], plugins: [ dts.default() // 타입스크립트 타입 정의를 번들링 해줍니다. ], }, ] |
저도 인터넷 돌아다니면서 주어들은 정보로 대충 이해한 내용이라.. 대략적으로 봐주세요..
잘못된 부분이 있다면 알려주시면 감사할 거 같아요. 꾸벅.
라이브러리로 사용할 컴포넌트는 최대한 간단하게 구성했어요.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// src/index.tsx import React, { useEffect } from 'react' import styles from './style.scss' const TestApp = () => { useEffect(() => { console.log('didmount') }, []) return ( <p className={styles['test']}>hello world</p> ) } export default TestApp |
대충 이렇게 스타일을 설정할 수 있고, 뜬금없는 useEffect는… 다음에 이야기할 오류에서 필요해서 넣어 놓았어요.
https://github.com/falsy/blog-post-example/tree/master/react-component-library-test
간단하게, 저는 typescript, webpack, react 구성으로 환경을 만들었어요.
배포하기 전에 우선 로컬에서 위 라이브러리가 잘 동작하는지 테스트를 해봐야 겠죠? 우선은 처음에 만든 ‘react-component-library’ 라는 패키지 디렉토리로 가서 터미널을 실행 후
1 |
$ npm link |
심볼릭 링크를 만들고,
위에 webpack으로 구성한 테스트 환경의 패키지 디렉토리로 가서 터미널을 실행 후
1 |
$ npm link -S react-component-library |
심볼릭 링크를 등록해 줍니다.
packages.json에 아래와 같이 등록된 것 을 확인하실 수 있을 거에요.
1 2 3 4 5 6 |
// packages.json ... "dependencies": { "react-component-library": "file:../react-component-library" } ... |
저는 아주아주 간단하게 그냥 가져와서 출력하게 했어요.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/index.tsx import React from 'react' import ReactDOM from 'react-dom/client' import Test from 'react-component-library' const App = () => ( <div> <Test /> </div> ) const container = document.getElementById('wrap') as HTMLElement const root = ReactDOM.createRoot(container) root.render(<App />) |
위와 같이 설정을 하고 webpack-dev-server를 실행하면 아래와 같은 오류 발생한답니다!
Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
좀 민망한 이야기지만, 여기서 몇시간을 헤맷는지 모릅니다..
결론적으로, 해결이라고 말하기도 애매하지만.. 딱히 라이브러리에 문제가 있는 건 아니였답니다.
그냥 npm publish 로 npm에 배포하고 npm install 을 통해 설치했으면 문제 없이 동작합니다!
로컬에서 위와 같이 오류가 나는 이유는 오류 메시지 그대로 React가 하나 이상 사용되어서 그런 건대요.
아까 라이브러리 코드에 특별한 이유 없이 useEffect를 사용했는데요, 위 오류가 Hook을 사용하지 않으면 발생하지 않아서 일부러 추가했던 거예요.
심볼릭 링크으로 등록된 라이브러리리는 ‘peerDependencies’가 동작하지 않아서 React가 중복되어 발생한 문제인 거 같아요. 그래서 로컬에서 심볼릭 링크를 통해서 테스트를 하실때는 webpack 설정을 아래와 같이 수정해주어야 해요.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// webpack.config.js const path = require("path") module.exports = { ... resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js'], alias: { react: path.resolve('./node_modules/react'), } }, ... } |
react 라는 별칭을 만들어서 react가 라이브러리에서 호출되지 않고 서비스의 모듈에서 호출되도록 하면, 위 오류를 해결할 수 있답니다.
안 그래도 잘 못쓰는 글쓰기가… 오랜만에 하니까 더 정신없게 작성된 거 같네요..
민망하지만.. 오랜만에 다시 포스팅을 시작한다는 점에 큰 의미를 두고..
오늘은 여기까지..