npm install create-react-app
npm install react-router-dom@6
npm install swiper ...
웹 프론트엔드 개발자라면 모를 수 없는 npm
항상 써오기만 했던 npm을 내가 직접 npm에 배포에 다른 개발자들이 사용할 수 있게 한다면?
재미있을 거 같아서 바로 가보자고..🤤
사전에 필요한것🧐
- 어떤 라이브러리를 만들 것인지에 대한 아이디어
나는 실제로 react 컴포넌트를 npm 라이브러리로 배포하면서 컴포넌트화와 재사용성에 대해 많이 배우고 얻었다. (이 전에 스터디했던 내용이 더 바탕이 많이 되어주었지만.. 이에 대한 내용은 다음 글로 남기겠다)
1. npm 회원가입 및 로그인
사이트에서 회원가입 후 아래 vscode나 사용하는 IDLE 터미널에서 명령어로 로그인해준다.
npm login
2. 프로젝트 생성 및 설정
react+typescript를 사용하기 때문에 cra를 통해 환경을 구축하였다
npx create-react-app 프로젝트 이름 --template typescript
이렇게 프로젝트를 생성하면 tsconfig.json도 함께 생성되는데 이 파일설정을 수정해 줘야 한다.
tsconfing.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
//여기서부터 추가
"noEmit": false, //출력을 내보내지 않는다는 설정인데 배포를 위해선 출력해야하므로 False로 지정
"declaration": true, //.d.ts 파일을 생성할 것인지에 대한 설정, 타입스크립트를 지원하는 라이브러리이기 때문에 true로 해준다
"outDir": "./dist" //tsc 명령어로 컴파일된 파일들이 위치할 장소 설정, /dist에 위치하게 할 것임
},
"include": ["./src/lib/**/*.tsx", "./src/lib/**/*.ts"] //tsc 명령어로 컴파일 할 파일들이 어디에 위치하는지 알려주는 설정 우리는 src/lib 폴더를 생성해 그 안에서 개발을 해야한다
}
그리고 내가 만들 라이브러리에 대한 정보를 package.json을 수정해 입력해 줘야 한다
package.json
{
"name": "roulette-img", // 내가 만들 라이브러리 이름 (미리 npm웹페이지에서 검색해보고 없는걸로 만들자)
"version": "0.3.1", // 라이브러리 버전 시작은 0.0.0, 파일을 수정할 때마다 버전을 바꿔 배포해 줘야 한다 아니면 에러가 나요
"private": false, // npm에 배포를 해줄것이기 때문에 false로 지정
"main": "dist/index.js", // 라이브러리의 시작 파일을 명시해준다
"types": "dist/index.d.ts", // 타입 추론을 도와주는 시작 파일을 명시해준다
"browser": "./browser/specific/main.js", // 라이브러리가 브라우저에서 구동되어야 하기 때문에 설정 해줘야한다
"description": "🎯내가 원하는 이미지로 생성하는 룰렛 컴포넌트 라이브러리", //프로젝트 설명
"author": { //프로젝트 작성자 정보
"name": "SU-VIN",
"email": ""
"url": ""
},
"repository": { //프로젝트 소스 코드를 저장한 저장소 정보
"type": "git",
"url": "https://github.com/SU-VIN/roulette-img"
},
"keywords": [ //프로젝트 검색시 참조되는 키워드
"react",
"typescript",
"component",
"roulette"
],
"dependencies": {
"@emotion/css": "^11.11.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.71",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"prepare": "rm -rf dist && mkdir dist && tsc" // 배포전 dist 폴더를 지웠다가 다시 생성 후 컴파일된 파일들을 넣는 과정이다
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
.npmignore
dist 폴더 외 다른 폴더들은 라이브러리에 포함할 이유가 없기 때문에. npmignore 파일을 생성해 아래 목록을 적어주자
.gitignore처럼 생각하면 된다!
node_modules/
src/
public/
tsconfig.json
3. 라이브러리 개발
프로젝트 src/ 폴더 구조이다.
내가 개발할 라이브러리 컴포넌트는 src/lib 폴더를 만들어 거기서 개발을 하면 되고 이 컴포넌트를 테스트 및 보고 싶을 땐 App.tsx에서 라이브러리 파일을 임포트 해서 보면 된다!
.
├── App.css
├── App.tsx
├── index.css
├── index.tsx
└── lib
├── Roulette
│ ├── index.tsx
│ └── styled.tsx
├── index.tsx
├── types
│ └── roulette.ts
import { RouletteStyle } from "./styled";
import { Roulette as RouletteProps } from "../types/roulette";
import { Arrow } from "../types/roulette";
import { useState, useEffect, useRef } from "react";
const Roulette = ({
imgUrl = "/assets/bg_circle-",
arrowImgUrl = "/assets/arrow.png",
chunkRange = { start: 2, end: 6 },
chunk = chunkRange.start,
arrowPosition = "up",
winNumber,
buttonText = "start",
buttonShape = "round",
buttonStyle,
onWin,
}: RouletteProps) => {
const [rouletteImg, setRouletteImg] = useState("");
const [arrowRotate, setArrowRotate] = useState(0);
const [isDeactive, setIsDeactive] = useState(false);
const rouletteRef = useRef<HTMLImageElement>(null);
useEffect(() => {
createImgUrl();
setArrowPosition();
}, []);
//룰렛 이미지 세팅
const createImgUrl = () => {
setRouletteImg(`${imgUrl}${chunk}.png`);
};
//룰렛 핀 위치 세팅
const setArrowPosition = () => {
const arrowPositionMap: { [key in Arrow]: number } = {
up: 0,
down: 180,
left: 270,
right: 90,
};
const rotate = arrowPositionMap[arrowPosition];
setArrowRotate(rotate);
};
const startonClickHandler = () => {
setWinNumber();
setStopRoulettePosition();
};
//당첨번호 선택
const setWinNumber = () => {
if (winNumber == null) {
winNumber = Math.floor(Math.random() * (chunk - 1 + 1)) + 1;
}
console.log(winNumber);
onWin?.(winNumber);
};
//룰렛 정지 위치 지정
const setStopRoulettePosition = () => {
const min = (360 / chunk) * (winNumber! - 1) - 360 / chunk / 2;
const max = (360 / chunk) * (winNumber! - 1) + 360 / chunk / 2;
const deg = Math.floor(Math.random() * (max - min + 1)) + min + 3240;
console.log(deg - 3240);
spinRoulette(deg);
};
//룰렛 돌리기
const spinRoulette = (deg: number) => {
setIsDeactive(true);
const onAnimationEnd = () => {
if (rouletteRef.current) {
alert(`축하합니다! ${winNumber}번 칸에 당첨되었습니다.`);
rouletteRef.current.style.transition = "";
rouletteRef.current.style.transform = "";
setIsDeactive(false);
}
};
if (rouletteRef.current) {
rouletteRef.current.addEventListener("transitionend", onAnimationEnd, {
once: true,
});
rouletteRef.current.style.transition = "transform 4s ease-in-out";
rouletteRef.current.style.transform = `rotate(${-deg}deg)`;
}
};
return (
<div className={RouletteStyle(buttonShape, arrowRotate)}>
<div className="roulette-wrapper">
<img ref={rouletteRef} className="roulette" src={rouletteImg} />
<img className="arrow" src={arrowImgUrl} />
</div>
{buttonStyle ? (
<div onClick={startonClickHandler}>{buttonStyle}</div>
) : (
<button
className={"start-button"}
onClick={startonClickHandler}
disabled={isDeactive}
>
{buttonText}
</button>
)}
</div>
);
};
export default Roulette;
코드에 대한 설명은 제외하겠다! 이 컴포넌트를 export 하기 위해선 lib/index.tsx 에
export { default as Roulette } from "./Roulette";
이렇게 적어주고 App.tsx에서 테스트해보겠다!
import React, { useEffect, useState } from "react";
import "./App.css";
import { Roulette } from "./lib";
function App() {
const [winNumber, setWinNumber] = useState(0);
const getWinNumber = (number: number) => {
setWinNumber(number);
};
return (
<div className="App">
<Roulette
imgUrl="/assets/bg_circle-"
arrowImgUrl="/assets/arrow.png"
chunkRange={{ start: 2, end: 6 }}
></Roulette>
<Roulette
imgUrl="/assets/bg_circle-"
arrowImgUrl="/assets/arrow.png"
chunkRange={{ start: 2, end: 6 }}
chunk={4}
arrowPosition="left"
buttonShape="squre"
onWin={getWinNumber}
></Roulette>
</div>
);
}
export default App;
라이브러리 불러쓰듯 임포트 하여 테스트해 보자
결과룰렛 라이브러리를 2개 임포트 했을 때 각자의 옵션에 맞게 독립적으로 잘 작동한다 ㅎㅎ
옵션에 대해 사용할 개발자들에게 어디까지 편의를 봐줄 것인가에 대한 고민을 많이 하게 되었다 버튼 같은 옵션들은 최대한 개발자들이 커스터마이징 가능하게 react.node를 타입으로 해서 직접 태그들을 render 하게 하는 방식으로 구현을 하였고 필수 값들에 대해선 자유도를 높이기보단 타이트하게 규칙을 지키는 형식으로 갔다 예로 imgUrl를 string으로만 받는 게 아닌 링크나 태그도 가능하게 해 주면 어떨까라는 의견을 받았지만 이미 인기 많은 라이브러리들을 참고해 본 결과 많은 라이브러리들이 img는 string url로만 받는 걸 선호한다는 결과를 얻어 나도 기존 그대로 반영하기로 했다.
아 추가로
type ChunkRange = {
start: number;
end: number;
};
export type ButtonShape = "round" | "squre";
export type Arrow = "up" | "down" | "left" | "right";
export interface Roulette {
imgUrl: string;
arrowImgUrl: string;
chunkRange: ChunkRange;
chunk?: number;
arrowPosition?: Arrow;
winNumber?: number;
buttonText?: string;
buttonShape?: ButtonShape;
buttonStyle?: React.ReactNode;
onWin?: (winNumber: number) => void;
}
라이브러리 사용 시 받아야 할 필수 값과 선택 값을 옵셔널을 사용해 구분해 주었다
또
사용법 같은 건 개발자들이 알기 쉽도록 README.md에 잘 정리하도록 하자
이 라이브러리는 아래 명령어로 다운로드할 수 있다!
npm i roulette-img
내가 npm 라이브러리를 만들었다니..! 신기하고 재미있는 경험이었다
배포한 지 이틀이 지났는데요 나 지금 완전 지현우
'개발새발🐶🐾🐥🐾 > React' 카테고리의 다른 글
[React + SpringBoot] 카카오 소셜 로그인 구현 (+ REST API) (3) | 2023.03.14 |
---|---|
[React] useRef로 해당 DOM으로 스크롤 이동하기 (+ scrollIntoView) (0) | 2023.02.04 |
[React Project: 포트폴리오 웹페이지(1)] 페이지 제작 - HOME (0) | 2023.01.30 |
[React Project: 포트폴리오 웹페이지(0)] 디자인 - Figma (0) | 2023.01.26 |
[React] 리액트 Open Graph - 링크의 썸네일, 미리보기제목, 설명 (0) | 2023.01.16 |