상세 컨텐츠

본문 제목

⚡ 나도 라이브러리를 만들 수 있을까? with React

카테고리 없음

by maymj9798 2025. 3. 14. 16:13

본문

준비단계

설계

먼저 어떤 라이브러리를 만들 것인가에 대한 설계를 한다. 나는 간단한 모달창과 버튼을 제공하는 라이브러리를 만들어 보려고 한다.

기술선정

다음으로는 환경을 세팅한다.
환경에는 어떤 기술스택을 사용하고, JS를 통해 만들지, TS를 사용할 지 등을 결정 해야한다.
최근에는 타입의 안정성을 위해 타입스크립트를 통한 라이브러리가 많아지고 있다고 해서,
나도 타입스크립트와 리액트를 사용해서 만들어 보겠다.

패키징

다음으로 패키징을 설정한다
패키징에는 주로 Rollup, Vite, Webpack을 활용하여 빌드 환경을 세팅한다

Rollup

  • 라이브러리 번들링에 최적화됨
  • 사용하지 않는 코드를 제거하는 성능이 뛰어남
  • 번들의 크기를 최소화하고, 리액트 라이브러리를 만들 때 주로 사용한다

Vite

  • 빠른 개발 서버를 제공
  • Rollup을 내부적으로 사용하여 번들링
  • HMR(Hot Module Replacement) : 개발 중 빠르게 코드 변경 확인 가능
  • 개발 속도가 중요한 애플리케이션 개발에 많이 사용된다
  • vite build를 이용해서 간단한 라이브러리 번들링 가능

Webpack

  • 강력한 설정 - Babel, TypeScript, SCSS 등이 가능
  • 코드 스플릿 지원
  • 대규모 프로젝트에서 커스터마이징이 필요할 때 주로 사용
  • 대규모 웹 애플리케이션에 적합

🎯 이번에는 좀 더 익숙하면서도 설정이 간단하고, Rollup이 가진 장점인 트리쉐이킹과 코드 수정 시 바로 반영되는 점 등을 활용하기 위해서
Vite를 사용해 보기로 한다

배포

npm 계정을 만들고, 로그인 한 다음 배포를 진행한다.
이 때, README, Storybook 등을 활용하여 해당 라이브러리를 설치하는 사람들이 곧바로 사용할 수 있게 끔 친절히 작성한다.

개발 단계 0.0.1

이제부터 실제로 라이브러리를 만들어보자
먼저 진짜 내가 만든게 배포가 되는 지를 확인하기 위해
빈 컴포넌트를 만들고 배포해보자

1. 초기 세팅

1-1 초기화

먼저 나만의 프로젝트명으로 새로운 폴더를 만들어주고 이동해준다

mkdir momodal-library
cd momodal-library

다음으로, npm을 초기화 해주면서 package.json 파일을 생성한다
package.json은 프로젝스트의 기본 설정을 관리하는 파일이다.
내용으로는 프로젝트 이름, 버전, 의존성 패키지 등을 관리한다

// 기본 세팅
{
  "name": "momodal-library",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {},
  "dependencies": {},
  "devDependencies": {},
  "license": "MIT"
}

1-2 리액트, 타입스크립트 설치

리액트 라이브러리를 만드니까 reactreact-dom을 설치해준다

npm install react react-dom

해당 과정을 거치면 패키지에 의존성이 자동으로 추가된다

"dependencies": {
  "react": "^18.0.0",
  "react-dom": "^18.0.0"
}

다음으로, 타입스크립트를 설치해준다

npm install -D typescript @types/react @types/react-dom

이때 -D 는 개발용 패키지로 설치하는 걸 의미한다
설치를 마치면 마찬가지로 의존성이 자동으로 추가된다

"devDependencies": {
  "typescript": "^5.0.0",
  "@types/react": "^18.0.0",
  "@types/react-dom": "^18.0.0"
}

1-3 타입스크립트 설정

이제 타입스크립트를 설정하는 파일을 생성한다

npx tsc --init

이러면 타입스크립트 설정파일인 tsconfig.json 이 생성된다

{
  "compilerOptions": {
    "target": "ESNext",
      // 최신 JavaScript 문법 사용(ES6)
    "module": "ESNext",
      // 최신 ECMAScript 모듈 시스템 사용 (ESM)
    "jsx": "react-jsx",
      // React-JSX 문법을 사용할 수 있도록 설정
    "declaration": true,
      // .d.ts 파일 자동 생성 
    "outDir": "./dist",
      // 빌드 결과물이 저장될 폴더 지정
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
  // src 폴더 내부만 컴파일
}

📌 declaration: true 를 설정하면 자동으로 타입 정의 파일인 .d.ts파일이 생성된다
해당 파일은 라이브러리에서 아주 중요하게 사용되는데
이게 없다면 타입 지정이 안된다.
만약 Modal.tsx파일을 만들었다면 해당 타입은
Modal.d.ts파일에서 정의되고 타입체크와 자동완성을 제공한다

📌 outDir: "./dist" 를 설정하면
컴파일할 때 파일들이 dist폴더에 저장된다
Modal.tsx파일은
dist폴더에서 Modal.js 파일과 Modal.d.ts 파일로 저장된다
이러면 기본 JS파일과 타입정의 파일을 같이 다운 받은 사람에게 제공할 수 있게 된다

1-4 폴더 구조 및 코드 작성

기본적으로 배포하고 싶은 컴포넌트는 src 폴더 하위에 작성하고
프로젝트 전체적인 설정은 package.json,
타입 설정은 tsconfig.json파일에서 이루어진다

momodal-library/
│
├── src/
│   └── Empty.tsx
├── package.json
└── tsconfig.json

배포가 되는지만 확인하면 되니까 빈 컴포넌트 하나를 src 폴더 하위에 만든다

// src/Empty.tsx
import React from 'react';

const EmptyDiv = () => {
  return <div></div>;
};

export default EmptyDiv;

1-5 엔트리 포인트(entry point) 작성

src 폴더 하위에 index.ts파일을 생성한다
index.ts 파일은 라이브러리의 진입점을 의미한다
따라서 해당 파일에서 작성한 컴포넌트를 내보내기 해준다

export { default as Empty } from "./Empty";

이후 추가되는 파일도 각각 내보내기(export) 해주면 된다

해당 파일이 없다면
라이브러리 설치자가 만들어놓은 Empty.tsx파일을 가져다 쓰려면
직접 import를 해야한다

import EmptyDiv from 'my-empty-react-library/EmptyDiv';

하지만 라이브러리 개발 시에 index.ts 파일로 export를 설정해두면
사용자가 간단하게 import하게 해준다

import { EmptyDiv } from 'my-empty-react-library';

1-6 빌드 설정하기

초기에 설정했던 vite를 빌드도구로 사용하여 빌드를 설정한다
먼저 설치를 해준다

npm install -D vite @vitejs/plugin-react

vite가 번들러를 설치하는 것이고
@vitejs/plugin-react가 vite를 리액트에서 사용하게 해주는 플러그인이다

다음으로, 프로젝트 루트에 vite.config.ts 파일을 생성하고
기본적인 설정을 해준다

plugins: [react()]
// plugin: 플러그인 설정을 의미하며 보통 리액트 플러그인을 적용

build: {}
// 빌드의 상세한 방식을 설정

lib: {
   entry: "src/index.ts",
   name: "momodal-library",
   formats: ["es", "cjs"],
   fileName: (format) => `momodal-library.${format}.js`,
}
// 리액트 컴포넌트 사용환경을 설정

- entry: 라이브러리의 시작파일인 진입점을 설정한다. 
     설정을 하면 vite에게 번들링의 시작 파일을 알려준다. 
         그래서 진입점을 보통 index.ts파일로 설정하여 
         다양한 export파일을 함께 가져온다 
- name: 빌드 시 글로벌 변수로 사용할 이름을 지정한다 
- formats: 빌드 할 모듈의 형식을 설정한다. 
   "es" 옵션은 최신 자바스크립트인 ES 모듈형식이고,
           "cjs" 옵션은 Node.js에서 사용하는 모듈 형식이다
- fileName: 빌드 할 때 저장되는 파일의 이름을 지정한다.
파일명이 일치하지 않아서 
            빌드 시에 오류가 지속적으로 발생하여 고생한적이 있다
            fileName: (format) => `momodal-library.${format}.js`
            이렇게  작성하면 파일은 
            `momodal-library.cjs.js`, `momodal-library.es.js`
            두개의 파일이 `dist` 폴더에 자동 생성된다

1-7 빌드하기

이제 빌드를 할 차례이다
가장 먼저 빌드 명령어를 설정해줘야한다
해당 명령어는 공식 명령어라서 다른 명령어를 작성하면 오류가 발생한다.
굳이 다른 명령어를 쓰고 싶다면 custom: "" 옵션을 쓴다

// package.json
"scripts": {
  "build": "vite build"
}

명령어 설정이 완료되면 빌드 명령어를 실행해준다

npm run build

빌드가 성공하면 dist폴더가 루트에 생성된다

// 빌드 전
src/
├── EmptyDiv.tsx    # React 컴포넌트
└── index.ts        # 라이브러리의 엔트리 포인트

// 빌드 후
dist/
├── empty-div.es.js     # ES Modules (ESM) 형식
├── empty-div.cjs.js    # CommonJS (CJS) 형식
├── EmptyDiv.d.ts       # 타입 정의 파일 (TypeScript)
└── index.d.ts          # index.ts의 타입 정의 파일

빌드 후를 보면 두개의 파일이 추가적으로 생성된 걸 볼 수 있다
먼저
📌.es.js 파일은 최신 자바스크립트 모듈 시스템으로 일반적으로 많이 사용하는 방식이다
익숙한 방식인 import를 사용하여 파일을 가져오게 해준다

import { EmptyDiv } from 'momodal-library'; // ESM 방식

📌.cjs.js 파일은 노드 환경에서 사용되는 모듈 시스템이다
require을 사용하여 모듈을 불러온다

const { EmptyDiv } = require('momodal-library'); // CJS 방식

1-8 마지막 설정하기

마지막 설정으로 package.json 을 알맞게 수정해준다
해당 내용은 npm에 등록되는 정보를 입력한다

{
  "name": "momodal-library", 
  // NPM에 배포될 라이브러리 이름 설정(중복시 에러)
  "version": "0.0.1" 
  // 현재 라이브러리 버젼 (업데이트 시 올려줘야한다)
  // major 1.0.0 -> 2.0.0  대규모 변경
  // minor 1.0.0 -> 1.1.0  새로운 기능 추가
  // patch 1.0.0 -> 1.0.1  버그수정
}

{
  "main": "./dist/momodal-library.cjs.js",
    // CJS 모듈 진입점
  "module": "./dist/momodal-library.es.js",
    // ESM 모듈 진입점
  "types": "./dist/index.d.ts",
    // 타입스크립트 자동 완성 파일
  "files": ["dist"]
    // 배포할 파일 -> 여기서는 dist폴더만 배포
}

{
  "scripts": {
  "build": "vite build" 
    // 빌드 스크립트 : npm run build로 번들링
}


{
  "peerDependencies": {  
    // 사용자가 필수로 설치하는 패키지
  "react": ">=16",
    // 리액트 16 이상 설치 필요
  "react-dom": ">=16"
    // 리액트 돔 16 이상 설치 필요
}
💡 이때 사용자가 리액트 15버전에서 라이브러리를 설치한다면,
  터미널에 경고메시지가 나타난다. 하지만 라이브러리는 강제설치가 된다.
  하지만 실제 사용할 때는 에러가 발생할 수 있다
  17버전이라면 문제없다. 해당 옵션은 `최소` 기준이기 때문이다

{
  "devDependencies": {
    // 사용자가 아닌 라이브러리를 개발하는 사람이 필요한
       `개발 중`에만 필요한 패키지 
       즉 배포시에는 포함 되지 않음 (사용자 설치 x)
  "@vitejs/plugin-react": "^4.2.1",
  "typescript": "^5.0.0",
  "vite": "^6.2.1",
  "vite-plugin-dts": "^3.6.3"
}
}



1-9 npm 배포

npm login을 하면 새로운 창이 열리면서 npm사이트 로그인창으로 이동한다.
로그인 완료 후 npm publish 명령어를 입력하면 배포가 완료된다

배포가 완료된 라이브러리는 https://www.npmjs.com/ 에서 확인 가능하다

🎨 내가 만든 라이브러리!

버전 업데이트 1.0.0 정식 배포

위에서 간단한 빈 컴포넌트를 임시로 올려보고 배포에 성공했다
이제 기존에 설계 했던 모달창을 만들어서 버젼을 업데이트 해본다

2-1 컴포넌트 만들기

src 폴더 하위에 컴포넌트를 만들고 코드를 작성한다
이때 타입스크립트 베이스로 만들었으니까
미리 타입지정을 다해준다

// src/Modal.tsx

// src/Modal.tsx
import React, { ReactNode } from 'react';

interface ModalProps {
  isOpen: boolean;             // 모달창 열림 여부
  children: ReactNode;         // 모달창 안의 내용
  onClose: () => void;         // 모달창 닫기 함수
}

const Modal = ({ isOpen, children, onClose }: ModalProps) => {
  if (!isOpen) return null;

  return (
    <div style={styles.overlay}>
      <div style={styles.modal}>
        {children}
        <button onClick={onClose} style={styles.closeButton}>
          닫기
        </button>
      </div>
    </div>
  );
};

const styles = {
  overlay: {
    position: 'fixed' as const,
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba(0,0,0,0.5)',
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
  },
  modal: {
    background: '#fff',
    padding: '20px',
    borderRadius: '8px',
    width: '300px',
    textAlign: 'center' as const,
  },
  closeButton: {
    marginTop: '15px',
  },
};

export default Modal;

위 코드에서는 style로 css를 작성했다
그 이유에는 라이브러리 배포 시에 사용자가 별도의 css를 다운,설정을 해줘야하는 반면, 직접 인라인으로 스타일을 작성하면
별도의 설정 없이 즉시 사용이 가능하다는 장점이 있다. 또한 다른 프로젝트의 css명과 충돌할 걱정이 없다는 점도 간편한다
하지만 컴포넌트가 커질수록 가독성과 재사용성이 힘들다는 단점이 있다.
게다가 hover나 반응형도 불가능하다

물론 프로젝트가 커지는 실무에서는 styled-components, CSS module, 테일윈드 등을 사용한다

2-2 엔트리파일에 추가하기

라이브러리의 진입점인 index.ts 파일에 새로 생성한 Modal.tsx파일을 가져와준다

// src/index.ts
export { default as Empty } from './Empty';
export { default as Modal } from './Modal';

2-3 빌드 후 다시 배포하기

npm run build 로 다시 빌드를 진행하여
새로운 파일 Modal.d.ts을 생성한다

다음으로 package.json 의 버젼을 알맞게 올려준다
(major 1.0.0 -> 2.0.0 대규모 변경 / minor 1.0.0 -> 1.1.0 새로운 기능 추가 / patch 1.0.0 -> 1.0.1 버그수정)

마지막으로
npm publish를 통해 변경된 사항을
npm에 다시 배포해준다

2-4 배포 확인 후 사용해보기

npmjs.com 사이트에서 라이브러리 확인이 가능하다

 


이미지를 보면 정상적으로 등록도 됐고
정식 1버전이 올라가있다
초기에 다운로드는 일반적으로 문제가 없는지 확인하기 위해
npm에서 다운을 한다고 한다 그래서 50

100회는 기본으로 설정이 된다고 한다. ~

실제 사람이 받은건 아니겠지?~~


이제 사용을 해보자
다른 프로젝트에서
npm install momodal-library 실행하면 내가 올린 라이브러리가 바로 다운된다
npm update momodal-library 를 통해서 버전 업데이트가 가능하다

import { useState } from "react";
import { Modal } from "momodal-library"; // 👈 배포된 라이브러리에서 가져오기

function App() {
  const [isOpen, setOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setOpen(true)}>모달 열기</button>

      <Modal isOpen={isOpen} onClose={() => setOpen(false)}>
        <h2>모달창 테스트!</h2>
        <p>라이브러리 모달창 성공 🎉</p>
        <button onClick={() => setOpen(false)}>닫기</button>
      </Modal>
    </div>
  );
}

export default App;

📌 직접 사용을 해보니 두가지 문제점이 보인다

  1. 중앙정렬이 안된다
  2. 닫기 버튼의 커스텀이 안된다

다음 버전에서 두가지 문제를 해결해보자

업데이트 1.1.0

위에서 발견한 문제를 해결해보자

모달창 css 수정

기본 width와 height를 수정하고
컨텐츠 내용에 맞게 높이가 증가하는 식으로 수정하였다
그리고 모든 children요소를 중앙정렬하였다

최대 높이를 80vh로 설정하였다

chilredn width 해결 1.2.0

스크롤 생성 시 컨텐츠 바로 옆에 생기는 문제가 생겨
width 100%를 추가하여 다시 배포하였다

 

Button 업데이트 1.3.0

모달창에 쓰일 버튼을 만들자
버튼에는
onClick, label, color 등 커스텀 옵션이 필요하다
해당 부분을 prop으로 넘겨준다
색상의 경우 기본적인 색상(활성화), 경고색상, 비활성화 색상으로 3가지를 제공한다

/** @format */

interface ButtonProps {
  label: string;
  onClick: () => void;
  color?: "primary" | "secondary" | "danger";
}

const Button = ({
  label,
  onClick,
  color = "primary",
}: ButtonProps) => {
  return (
    <button
      onClick={onClick}
      style={{
        ...styles.button,
        ...styles[color],
      }}
    >
      {label}
    </button>
  );
};

이제 가장 고민이 많이 됐던 사이즈 부분이다
먼저 가장 쉽게 size prop을 제공하고
타입스크립트 자동완성으로 기본 사이즈 4가지를 제공한다

interface ButtonProps {
  size?: "sm" | "md" | "lg" | "xl";
}

  sm: {
    padding: "4px 8px",
    fontSize: "0.875rem",
  },
  md: {
    padding: "8px 16px",
    fontSize: "1rem",
  },
  lg: {
    padding: "12px 24px",
    fontSize: "1.125rem",
  },
  xl: {
    padding: "16px 32px",
    fontSize: "1.4rem",
  },

기본으로 md사이즈를 제공하고
width와 height를 사용자가 직접 입력하여
버튼의 크기를 원하는 대로 사용도 가능하게 하였다
크기는 숫자를 받아서 px로 사용이 가능하고 (width={200})
문자를 받아서 %로 사용도 가능하게 설계하였다 (width="100%")

interface ButtonProps {
  size?: "sm" | "md" | "lg" | "xl";
  width?: number | string;
  height?: number | string;
}


<button
  onClick={onClick}
  style={{
     ...styles.button,
     ...styles[color],
     ...styles[size],
     width: typeof width === "number" ? `${width}px` : width,
     height: typeof height === "number" ? `${height}px` : height,
 }}> 
   {label} 
</button>
📌 사용예제
<Modal isOpen={isOpen} onClose={handleClose}>
  <div className="modal-actions">
    <ModalButton label="확인" onClick={handleConfirm} color="primary" size="lg" width="100%" height={50} />
    <ModalButton label="취소" onClick={handleClose} color="secondary" size="md" width={200} height="40px" />
    <ModalButton label="삭제" onClick={handleDelete} color="danger" size="sm" width="50%" height="auto" />
  </div>
</Modal>