본 글은 이 링크에 있는 글을 참고해서 작성되었으며, 부족한 부분은 참고자료에 명시한 자료들로 보충하였음을 알려드립니다.
리액트 컴포넌트를 만들고 난 뒤 해당 컴포넌트를 다른 프로젝트에서도 사용하려고 했던 경험이 있다. 복사 붙여넣기도 한 방법이었지만 관련 코드들이 추가되면서 프로젝트가 더욱 복잡해지는 것이 싫었고, 추후에 또다른 프로젝트에서도 재사용할 수 있게 만들고 싶었다.
이를 위한 멋진 방법은 바로 npm에 패키지로 배포하는 것이다. 아래의 과정을 마치면 npm i
로 손쉽게 내가 만든 컴포넌트를 설치해서 사용할 수 있다.
배포 과정에서 babel, typescript, 패키지 의존성 등 내가 몰랐던 부분들 때문에 많이 헤맸기에, 그 과정을 차근차근 기록으로 남겨보았다.
본 글은 타입스크립트로 작성된 리액트 컴포넌트를 npm 패키지로 배포하는 방법에 관한 글이다. 타입스크립트가 아닌 자바스크립트를 사용했다면 타입스크립트와 관련된 과정은 넘어가고 진행하면 된다.
아래와 같이 CRA로 리액트를 설치해준다.
npx create-react-app [프로젝트 이름] --template typescript
설치하고 나면 다음과 같이 폴더가 형성되어 있을 것이다.
프로젝트 이름/
├── node_modules/
├── public/
├── src/
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
여기서
src
폴더 안에 lib
폴더를 만든다.lib
안에 작성한다.lib
폴더 내에 index.ts
파일과 components
폴더를 만든다.components
폴더 내에 작성한다.index.ts
에는 작성한 모든 컴포넌트들을 불러오고(import
), 이를 내보낼(export
) 것이다. 추후에 index.ts
가 (정확히는 이를 바벨로 컴파일한 index.js
파일이) 패키지의 메인 entry point가 될 것이다.dist
폴더를 만든다.lib
내에 있는 코드를 바벨로 컴파일 한 결과를 dist
폴더로 내보낼 것이다.dist
폴더 내부의 코드가 npm에 배포된다.폴더 구조에는 정답이 없지만 내가 참고한 여러 글들에서 위와 유사한 구조를 따르고 있어서 비슷한 구조로 만드는 것을 추천한다.
여기까지 완료했다면 다음과 같은 폴더 구조가 만들어졌을 것이다.
tsconfig.json 설정은 해당 링크를 참고했다.
{
"compilerOptions": {
// Target latest version of ECMAScript.
"target": "esnext",
// Search under node_modules for non-relative imports.
"moduleResolution": "node",
// Process & infer types from .js files.
"allowJs": true,
"declaration": true,
"emitDeclarationOnly": true,
// Enable strictest settings like strictNullChecks & noImplicitAny.
"strict": true,
// Disallow features that require cross-file information for emit.
"isolatedModules": true,
// Import non-ES modules as default imports.
"esModuleInterop": true,
"jsx": "preserve",
"outDir": "dist"
},
"include": ["src/lib"]
}
참고한 링크에 나와있는 tsconfig.json에서 jsx
, outDir
, include
부분만 바꿔주었다.
여기서 .d.ts
파일 생성과 관련되어 있는 옵션이 포함되어 있는데, 이를 다루기에 앞서 .d.ts
파일이 무엇인지 살펴보자.
.d.ts
파일은 무엇이고 왜 필요한가?
.d.ts
파일은 타입을 정의하기 위해 존재하는 파일이다. 우리가 열심히 타입스크립트로 작성한 코드는 결국엔 자바스크립트로 트랜스파일된다. 따라서 트랜스파일된 파일만 배포해서 사용할 경우 타입스크립트를 사용하고 있는 환경에서는 해당 라이브러리의 타입체킹을 할 수가 없다. 따라서 .d.ts
파일을 생성해서 넣어주지 않는다면 아래와 같이 ts(7016) 에러가 뜬다.
따라서 타입스크립트 환경에서 내가 배포한 라이브러리를 설치해서 사용할 수 있도록 .d.ts
파일을 생성해줘야 한다.
아래와 같은 옵션을 추가해주면 tsc 명령어를 통해 컴파일 했을 때 자동으로 .d.ts
파일이 생성되게끔 할 수 있다.
"compilerOptions": {
// tsc를 사용해 .js 파일이 아닌 .d.ts 파일이 생성되었는지 확인합니다.
"declaration": true,
"emitDeclarationOnly": true,
// Babel이 TypeScript 프로젝트의 파일을 안전하게 트랜스파일할 수 있는지 확인합니다.
"isolatedModules": true
}
.d.ts
파일을 만들어준다..d.ts
파일만 만들어준다.나같은 경우에는 FixedSizeList
라는 컴포넌트만 배포할 예정이었기 때문에 index.ts
의 파일 내용은 다음과 같았다.
import FixedSizeList from "./components/FixedSizeList";
export { FixedSizeList };
참고용으로 나의 src 폴더의 구조를 공유한다. components에는 .tsx
확장자를 가진 컴포넌트만 넣었고, 그 외 커스텀 훅 등 기타 코드들은 lib
폴더 안에 넣어주었다.
배포할 모든 코드를 작성하였다면 이제 트랜스파일링을 위해서 바벨을 설치할 차례이다.
트랜스파일링이 왜 필요한가?
우리가 작성한 코드는 타입스크립트로 작성된, 리액트 문법을 사용한 코드이다. 이를 자바스크립트로 변환해주는 작업이 필요하다. 또한 ES6 문법을 지원하지 않는 브라우저도 있기에 모든 브라우저에서 코드가 원활히 실행되도록 구 버전 코드로 바꿔줘야 한다.
바벨을 설치할 때 함께 설치하는 plugin, 그리고 plugin들을 모아둔 preset이 실제 변환을 수행해주는 역할을 한다. 이러한 preset, plugin은 컴파일 타임에 코드를 트랜스파일링 할 때(.tsx
, .ts
→ js
파일로 변환할 때) 작용한다.
→ 따라서 devDependencies
로 설치하면 된다.
폴리필
ES6 문법 중 Promise, Array.from처럼 ES5 버전의 문법에 존재하지 않는 기능은 트랜스파일링을 통해 변환되지 않는다. 하지만 변환할 수 없는 부분을 폴리필을 통해 구현할 수는 있다.
폴리필을 하기 위한 방법에는 몇 가지가 있는데 그 중 하나는 @babel/preset-env
에 useBuiltIns 및 corejs 옵션을 설정해주는 것이다. 하지만 이 방법을 사용하면 전역 스코프가 오염되는 이슈가 발생할 수 있다고 하여, @babel/plugin-transform-runtime
을 사용하기로 하였다.
이는 런타임에 등록되지 않은 메서드나 기능을 주입해주는 역할을 한다.
→ dependencies
로 설치해야 한다. (@babel/runtime
, @babel/runtime-corejs3
)
@babel/plugin-transform-runtime
을 사용할 시 주의할 점은 axios와 같이 ES6 문법을 사용하는 npm module을 프로젝트에서 사용하고 있다면 해당 모듈 (e.g. node_modules/axios)까지 트랜스파일 범위에 포함되도록 해야 한다는 것이다.
아래 명령어로 바벨을 설치해준다.
npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-transform-runtime
npm install --save @babel/runtime @babel/runtime-corejs3
bable.config.json 파일을 생성하고, 아래와 같이 작성해준다.
{
"presets": [
["@babel/preset-env"],
[
"@babel/preset-react",
{
"runtime": "automatic"
}
],
[
"@babel/preset-typescript",
{
"isTSX": true,
"allExtensions": true
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
옵션을 하나씩 살펴보자.
여기까지 완료되었다면 이제 package.json을 설정해보자.
다음과 같이 package.json의 scripts에 명령어를 추가해준다.
"scripts": {
...
"build": "babel src/lib --out-dir dist --copy-files --extensions \".ts,.tsx\"",
"type": "tsc",
}
src/lib
디렉토리에 있는 .ts
, .tsx
확장자를 가진 파일들을 트랜스파일링 하도록 한다. 결과물은 dist
디렉토리에 저장된다..d.ts
파일을 생성하고, 타입 체킹이 이루어지도록 한다.다음으로 의존성을 설정해줄 것이다.
현재는 react
, react-dom
등이 dependencies로 들어가있는 상황일 것이다.
아래와 같이 @babel/runtime
과 @babel/runtime-corejs3
를 제외한 모든 것들을 devDependencies로 옮겨주었다.
"dependencies": {
"@babel/runtime": "^7.21.0",
"@babel/runtime-corejs3": "^7.21.0",
},
react
와 react-dom
은 정확히는 peerDependencies에 있어야 한다. peerDependencies는 실제로 패키지에서 import 하지는 않지만 특정 라이브러리나 툴에 호환성을 필요로 할 경우에 사용한다.
name
: 배포할 패키지의 이름. 다른 패키지 이름과 중복되면 안 된다.version
: SemVer로 버전 설정. 같은 버전을 중복해서 배포할 수는 없다.description
: 프로젝트의 설명을 지정한다.author
: 저자 명시private
: false로 지정할 경우 공개 "main": "dist/index.js",
"types": "dist/index.d.ts",
"module": "dist/index.js",
"files": [
"dist"
],
"respository": {
"type": "git",
"url": "git+https://github.com/dahyeon405/windowing"
},
main
: 프로젝트의 기본 entry point를 지정한다.types
: .d.ts
파일의 entry point를 지정한다.files
: 패키지가 의존성으로 설치될 때 같이 포함될 파일들의 배열repository
: 코드가 존재하는 장소를 지정한다.npm run type
명령어로 .d.ts
파일을 생성해준다.npm run build
명령어로 트랜스파일링을 수행한다.npm login
을 입력하여 로그인을 수행한다.npm publish
명령어로 배포한다!!여기까지 하면 끝이다. npm 사이트에 들어가서 내가 배포한 패키지를 확인할 수 있다.
위 과정을 거쳐 아래 컴포넌트를 npm에 배포하였다.
🔹 windowing이 적용된 리액트 컴포넌트
https://www.npmjs.com/package/react-window-dhy
npm i 패키지 이름
을 입력하여 프로젝트에 패키지를 설치하고 사용할 수 있게 되었다.배포 과정에서 생각보다 우여곡절이 많았다. 바벨의 폴리필을 제대로 이해하지 못해 배포한 패키지를 설치하자 프로젝트에서 오류가 발생하기도 하고, .d.ts
파일을 작성해주지 않아 코드에 빨간줄이 떠있기도 했다. 관련 부분들을 공부하고 패키지를 다시 배포하고 설치하는 과정을 반복하면서 결국엔 해결할 수 있었다. 이 과정에서 바벨, 타입스크립트, package.json에 대한 이해가 훨씬 높아진 것 같다.
본문에 오류가 있으면 댓글로 알려주시면 감사하겠습니다 :)
참고자료
Publish React components as an npm package
TSConfig Reference - Docs on every TSConfig option
FE개발자의 성장 스토리 02 : Babel7과 corejs3 설정으로 전역 오염 없는 폴리필 사용하기