1. 배경
1.1. 오픈소스에 대한 느낌
옛날부터 JavaScript를 이용한 개발을 해오면서, 오픈소스 개발 경험 한 개쯤은 가져보고 싶다는 생각이 들었다.
학교에서 배운 '오픈소스 정신'의 철학을 실천해보고 싶다는 이유도 있었고, JavaScript 개발 특성상 오픈소스와 많이 접할 수밖에 없는 환경에 놓였기 때문에 아무래도 오픈소스가 낯설지 않았다.
아래 사진을 보자.
각 언어별로 얼마나 많은 라이브러리 패키지를 소유하고 있는지를 나타낸 그래프이다.
JavaScript(Node.js)가 압도적인 수치를 보이고 있음을 확인할 수 있다.
즉 엄청난 지조와 절개를 지키며 生 vanilla JavaScript 코드로 하나부터 열까지 모든 코드를 직접 짜지 않는 이상, 우리는 필연적으로 오픈소스 코드와 접하고 있음을 볼 수 있다.
결론적으로 나는 colorpia라는 이름의 오픈소스를 제작하였고, npm에 배포하였다.
1.2. colorpia를 제작하게 된 계기
개인 프로젝트 코드를 리뉴얼하는 과정에서 아이디어를 떠올리게 되었다.
프론트엔드 개발을 진행하다 보면 CSS 및 이에 준하는 스타일 작업을 진행할 때, 반드시 요소에 색을 지정하는 과정을 거쳐야 한다.
CSS에서는 16진수로 표현되는 24비트 색상 값을 지정할 수 있으며 # 기호를 이용하여 값을 표현한다.
(예를 들어 red의 경우 #ff0000, cyan의 경우 #00ffff로 나타낸다)
프로젝트를 기획할 때 디자인 시스템도 함께 마련하기 때문에, 주로 사용하게 될 색상 정보를 미리 저장해 놓으면 나중에 작업하기 편한 이점이 있다.
예를 들어 styled-component나 emotion 라이브러리의 경우 전역적으로 theme을 지정할 수 있으므로(ThemeProvider), 이후 스타일 작업을 수행할 때 일일이 #으로 시작하는 색상정보를 하드코딩 할 일이 사라져 매우 유용하다.
나의 경우에는 개인 프로젝트에서 사용할 초록색으로 #00ff2b 값을 채택하였다.
모든 텍스트, 버튼, border 등등에서 이 색을 사용할 예정이었기 때문에 theme.ts 파일을 아래와 같이 작성하였다.
const colors = {
green: '#00ff2b',
black: '#000000',
black50: '#00000080',
};
export type Colors = keyof typeof colors;
export default { colors };
그리고 아래 코드와 같이 적용하였다.
일일이 색상 값을 하드코딩 할 필요가 없으므로, 오타에 의한 색상 불일치 문제도 해결할 수 있고, 무엇보다도 매우 간편하다는 것이 장점으로 느껴졌다.
const ButtonBase = styled.button`
color: ${({ theme }) => theme.colors.green};
`;
하지만 문제가 하나 발생했다.
phaser라는 게임 라이브러리는 화면에 띄울 요소의 색상을 지정할 때, 색상 값을 # 형식이 아닌 0x 형식으로 나타내야 하기 때문에, 미리 선언한 색상 정보를 사용할 수 없었다.
private initBall() {
this.ball = this.add
.rectangle(
this.scale.width / 2,
this.scale.height / 2,
BALL_SIZE,
BALL_SIZE,
0x00ff2b, // #00ff2b로 작성하면 에러 발생!
)
.setScale(window.devicePixelRatio);
}
변수를 하나 더 만들어 0x 형식으로 나타낸 색상 정보를 저장하는 방법으로 문제를 해결할 수 있겠으나, 한 가지 색상 정보를 여러 개의 변수로 표현하고 싶지 않았다.
이런 문제점에 부딪힌 과정에서, 색상 정보를 다양한 형태의 format으로 쉽게 변환해 줄 수 있는 라이브러리가 있으면 좋을 것 같다는 생각이 들었고, 이를 주제로 하여 만들게 되었다.
아무래도 데이터를 변환하는 라이브러리이기 때문에, 복잡한 로직을 필요로 하지도 않고 테스트 코드를 작성하기에도 용이하므로, 첫 라이브러리 개발의 주제로 삼기에 적절했다고 생각한다.
2. 배포하는 법
2.1. 준비 과정
참고: 좀 더 쉬운 이해를 위해, github에 업로드된 실제 코드를 참조하세요. (링크)
일단 npmjs의 계정이 있어야 한다. npmjs에 접속하여 signup 과정을 먼저 진행하자.
이후에는 로컬 PC의 터미널에서 npm 계정으로 로그인을 해야 한다.
터미널에 아래와 같이 입력하면, 브라우저를 통해 로그인을 하기 위해 enter를 누르라는 메시지가 뜬다.
enter를 눌러 브라우저를 띄운 뒤, 로그인 절차를 완료하자.
$ npm login
아래와 같은 창이 뜨면 로그인이 완료된 것이다.
다시 터미널을 확인해 보면, 로그인되었다는 문구가 출력될 것이다.
whoami 명령을 통해 현재 로그인된 사용자가 내가 맞는지 검증할 수 있다.
(로그인된 사용자의 id가 출력된다)
$ npm whoami
2.2. 라이브러리 정보를 package.json에 기입하기
package.json을 통해 라이브러리의 name, description, author, version, keywords, license, repository 등의 정보를 기입해야 한다.
해당 내용을 기반으로 라이브러리가 npm에 등록된다.
참고로 npm에 이미 등록된 이름과 중복되거나 유사할 경우 publish가 거부된다.
따라서 이름을 매우 잘 지어야 한다.
(유명하거나 자주 쓸 것 같은 이름을 미리 등록해 두어 알박기 하는 사람이 많다는 것을 이때 알게 되었다)
2.3. 폴더구조
폴더 구조는 아래와 같은 형태로 잡는 것이 편하다.
라이브러리_이름/
├── dist/
├── lib/
├── build.js
├── .gitignore
├── .npmignore
├── LICENSE
└── package.json
dist 폴더에는 빌드 결과물, lib 폴더에는 개발 코드가 들어가도록 하였다.
또한 build.js에는 빌드 옵션을 넣었다.
2.4. TypeScript 지원하기
2.4.1 타입 정의 파일의 필요성 알아보기
라이브러리를 배포하고자 할 경우, 웬만해서는 TypeScript 코드 내에서도 사용될 수 있도록 이를 지원해야 한다.
(즉 타입 정의 파일이 함께 제공되어야 한다)
만약 라이브러리 코드의 타입 정의 파일이 존재하지 않는다면 어떤 일이 발생할까?
npmjs에서 타입이 정의되지 않은 임의의 라이브러리(ramdom-joke)를 찾아, TypeScript 코드 내부에 import해보았다.
import { getRandomJoke } from "random-joke";
그 결과 아래 사진과 같이, 타입 선언 파일을 찾을 수 없다는 오류와 마주치게 되었다.
오류를 해결하기 위해서는, 라이브러리를 이용하는 모든 사람들이 (자기가 짠 코드가 아님에도 불구하고) 해당 라이브러리 코드에 들어가 타입을 일일이 지정해주거나, 타입 정의 파일(d.ts)을 만들어야 한다.
요즘에는 TypeScript를 이용하여 개발하는 경우가 많기 때문에, 라이브러리의 타입이 제공되지 않을 경우 사람들은 해당 라이브러리를 사용하지 않을 것이다.
(아무도 사용하지 않기 때문에, 원 개발자는 유지보수의 필요성을 못느끼게 될 것이며, 결과적으로 죽은 코드가 된다)
따라서, 라이브러리 배포를 위해서는 타입 정의 파일도 함께 제공해야 하다는 것이 사실상 필수가 되었다.
2.4.2. 타입 정의 파일(d.ts) 만들기
직접 d.ts 파일을 생성하여, 그 안에 일일이 타입을 지정하는 방법을 사용할 수 있으나, 더 쉬운 방법이 있다.
tsconfig.json 파일 내부에서 declaration 옵션을 true로 지정하면 된다.
이럴 경우 컴파일을 수행하였을 때, 컴파일된 코드와 함께 d.ts 파일도 함께 생성된다.
앞서 살펴본 폴더구조에서 개발 코드는 lib 폴더에, 빌드 결과물은 dist 폴더에 넣기로 결정하였기 때문에, 이를 반영한 tsconfig.json 코드는 아래와 같다. (나머지 옵션은 생략함)
한편 바로 뒤에서 살펴보겠지만, 타입스크립트 컴파일러는 단순히 타입 정의 파일을 만드는데에만 사용할 목적이기 때문에 emitDeclarationOnly 옵션을 true로 지정하여 d.ts 파일만 생성되도록 하였다.
{
"compilerOptions": {
"rootDir": "./lib",
"outDir": "./dist",
"declaration": true,
"emitDeclarationOnly": true,
}
}
2.5. 여러 모듈 타입 대응하기
폭넓은 사용성을 위해, 배포할 라이브러리가 여러 모듈 타입에서 사용될 수 있도록 작성해 주는 게 좋다.
쉽게 설명해서 commonJS 방식 및 ESM 방식에 관계없이 라이브러리를 불러올 수 있도록 작성해야 한다는 뜻이다.
// esm 방식으로 불러온 코드
import { Color } from 'colorpia';
// commonJS 방식으로 불러온 코드
const { Color } = require('colorpia');
두 개의 방식을 모두 지원하는 방법은 여러 개가 있을 수 있다.
(각각의 방식마다 서로 다른 코드를 작성하여 배포를 한다던지... 하지만 이럴 경우 작업을 두 배로 수행해야 된다는 단점이 있다)
그 중 채택한 것은 한 가지 스타일로 코드를 작성한 다음 트렌스파일링을 통해 다른 스타일로 변환하는 방법이다.
여기서 트랜스파일링 (및 경량화 작업)을 수행할 도구로는 esbuild를 선정하였다.
(매우 빠르다는 점, TypeScript 코드에 대한 트랜스파일링 기능을 무리없이 지원한다는 점을 근거로 채택하였다)
즉 ESM으로 작성한 TypeScript코드를 esbuild를 통해 (경량화된) ESM JavaScript코드 및 (경량화된) commonJS JavaScript코드로 변환하는 과정을 거치도록 하였다.
참고: esbuild는 트랜스파일링 기능만 수행할 뿐, ts코드에 대한 타입 정의 파일(d.ts) 생성까지는 해주지 않는다.
따라서 코드 변환 과정과 타입 정의 파일 생성 과정을 따로따로 진행해야 한다. (esbuild 문서 참고)
build.js 파일 내부에는 아래와 같이 작성하였다.
import esbuild from 'esbuild';
const baseConfig = {
entryPoints: ['lib/index.ts'],
outdir: 'dist',
bundle: true,
minify: true,
};
Promise.all([
esbuild.build({
...baseConfig,
format: 'cjs',
outExtension: { '.js': '.cjs' },
}),
esbuild.build({
...baseConfig,
format: 'esm',
}),
]).catch((error) => {
console.error(error);
process.exit(1);
});
빌드할 파일의 진입점이 되는 것은 (당연히) lib 폴더의 index.ts 파일이며, 빌드 결과물은 dist 폴더에 생성되도록 지정하였다.
또한 여러 개의 코드를 한 개로 압축시키기 위해 bundle 옵션을 주었고, 코드 경량화를 위해 minify 옵션도 주었다.
마지막으로 ESM과 CommonJS(=cjs) 두 개 방식의 빌드 결과물이 생성되도록 하였다.
모든 빌드 세팅을 마친 뒤 추후에 빌드 명령을 수행한다면, dist 폴더 내부에는 아래 구조도 처럼 구성되게 된다.
dist/
├── index.js
├── index.cjs
├── index.d.ts
└── (기타 파일 및 폴더들)/
2.6. package.json 세팅 마무리하기
이제 package.json의 script를 수정하는 작업과, exports 옵션을 수정하는 작업, 진입점 설정 작업만 끝내면 된다.
외부에서 우리가 작성한 라이브러리를 사용하기 위해서는, 라이브러리 코드의 위치와, 타입 선언 파일의 위치가 어디인지를 지정해주어야 한다.
즉, 실행될 파일은 빌드로 인해 생성된 index.js 파일과 index.d.ts 타입 정의 파일이므로 이를 지정하면 된다.
{
"main": "dist/index.js",
"types": "dist/index.d.ts",
}
또한 외부에서 라이브러리 코드를 불러올 때, ESM 방식과 CommonJS 방식을 모두 지원해야 하므로, 외부에서 불러온 형태(import, require)에 따라 각각 다른 파일(js, cjs)을 export해줄 필요가 있다.
이를 지정하는 코드는 아래와 같다.
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
마지막으로 script 명령은 다음과 같이 지정하였다.
(사실 이 부분은 선택사항이긴 하다. 사람마다 선호하는 script 명명법이 있기 마련이므로)
"scripts": {
"build": "rm -rf dist/* && npm run build:tsc && npm run build:js",
"build:tsc": "tsc",
"build:js": "node build.js",
},
2.7. .npmignore 작성하기
git ignore와 마찬가지로 npm에 publish 할 때 올리고 싶지 않은 파일을 지정할 수 있다.
웬만해서는 빌드 결과물, README.md, LICENSE 파일만 올리는 것이 보통이기 때문에 .npmignore 파일은 아래와 같이 작성하였다.
(package.json은 지정여부와 관계없이 업로드된다)
**/*
!/dist/**
!LICENSE
!README.md
2.8. publish 하기
이제 끝났다.
publish 명령만 입력하면 바로 업로드된다.
(주의할 점은 기존에 publish된 버전과 겹치면 안 된다. 버전은 package.json에서 수정하거나 명령을 통해 전환할 수 있다)
(버전과 관련된 내용은 'SemVer' 문서 참조)
$ npm publish
major 버전 올리기
$ npm version major
minor 버전 올리기
$ npm version minor
patch 버전 올리기
$ npm version patch
이제 npmjs 사이트에서 내가 publish한 라이브러리를 검색하면 뜰 것이다.
이때 어떤 파일이 업로드되었는지 확인하려면, Code 탭을 누르면 된다.
'JavaScript' 카테고리의 다른 글
클로저 (Closure) (0) | 2024.01.01 |
---|---|
실행 컨텍스트 (Execution Context) (0) | 2023.12.28 |
var, let, const, 호이스팅, TDZ (0) | 2023.12.20 |
프로토타입(Prototype) (0) | 2023.10.07 |
옵셔널 체이닝(Optional Chaining) (1) | 2023.10.04 |