직접 작성한 TypeScript 코드를 NPM 에 배포해보았다.
배포 과정에서 자연스럽게 Module System 에 대해 공부하게 됐는데,
이번 포스트에서는 그에 대한 내용과 NPM 패키지 배포 방법에 대해 이야기하려 한다.
color-family
본격적으로 시작하기에 앞서, 내가 기획한 패키지에 대해 간단하게 소개해보려 한다.
“NPM 에 간단한 모듈 하나 올려보자.”
가벼운 마음으로 시작한 파일럿 프로젝트이기 때문에, 굉장히 단순하다.
나는 한 프로젝트에서 런타임 중에, 배경색이 random(무작위)한 값으로
결정되는 컴포넌트를 구현했었다.
직접 함수를 만들어 해결했는데,
문득 "random 의 범위가 너무 넓은게 아닌가?" 걱정했던 게 기억난다.
예를 들어, random 한 색상 위에 텍스트를 띄우는 UI를 그린다고 하면,
검은 배경 위에 검은 텍스트가 올라가는 경우도 있을 것이다.![]()
위와 같은 이슈를 해결하기 위한, 솔루션을 이번 패키지에 구현했다.
나는 random 의 범위를 #000000
~ #FFFFFF
중 하나가 아니라,
특정 색상 계열 중 하나로 결정하는 방법을 생각했다.
여기서 내가 정의한 특정 색상 계열 이란
파스텔 색상, 비비드 색상 과 같이 유사한 색조와
분위기를 가진 색상들의 묶음 를 의미한다.ex1) pastel
![]()
ex2) vivid
![]()
"색상 계열" 을 강조하고자, 패키지 이름도 color-family
로 했다.
정리하자면, 무작위한 색상코드(혹은 사용자가 지정한 값)를
(파스텔 등의) 특정 계열로 변환해주는 패키지이다.
아직 정식 버전(1.0.0) 으로 배포하지 않아서 잡다한 파일도 함께 올라갔고
기능도 제대로 동작하지는 않지만, 설치해보면 사용할 수는 있다.
npm i color-family
조금 더 코드를 추가하고 다듬어서, 다음과 같이 사용되도록 만들어볼까 한다. 😄
import {ColorFamily} from "color-family" ... const cf = new ColorFamily() console.log(cf.code()) // ✅ 기본: #3300FF console.log(cf.pastel()) // ✅ pastel 색상으로 변환: #00A4EF console.log(cf.vivid()) // ✅ vivid 색상으로 변환: #98B8E9
이제 본격적인 내용으로 들어가겠다.
NPM 에 패키지를 배포할 때는
어떤 환경에서 Module 이 호출될 지를 생각해야 한다.
여기서 Module
이란 무엇일까?
추가로 Module System
에는 어떤 것들이 있고,
각각 어떻게 다르기에 사용환경을 고민해야 하는 걸까?
그에 대한 내용을 간단하게 정리해 보았다.
독립적인 코드 단위로 나눈 코드 블록을 말한다.
Module 로 코드를 관리할 경우
복잡도를 낮추고, 중복 코드을 줄일 수 있어
코드 가독성, 재사용성, 유지보수성 등을 개선할 수 있다.
함수 혹은 클래스를 라고 생각하면 이해가 쉽다.
물론 그것보다 훨씬 더 포괄적인 개념이다.
모듈 시스템(Module System)은 각기 다른 파일에서
모듈을 어떻게 정의하고 공유할 것인지를 약속하고 통일한 규정이다.
가장 먼저 등장한 모듈 시스템으로, 특히 Node.js 에서 자주 사용된다.
// math.js
module.exports.add = function(a, b) {
return a + b;
};
// app.js
const math = require('./math');
console.log(math.add(2, 3)); // ✅ 정상출력: 5
클라이언트 측에서, JavaScript에서 모듈을
비동기적으로 로드할 수 있는 Module System 이다.
// math.js
define([], function() {
return {
add: function(a, b) {
return a + b;
}
};
});
// app.js
require(['math'], function(math) {
console.log(math.add(2, 3)); // ✅ 정상출력: 5
});
CommonJS, AMD, 그리고 브라우저 전역 변수 패턴을
모두 지원하는 Module System 이다.
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['exports'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node, CommonJS-like
factory(module.exports);
} else {
// Browser globals (root is window)
factory(root.myModule = {});
}
}(typeof self !== 'undefined' ? self : this, function(exports) {
exports.add = function(a, b) {
return a + b;
};
}));
ECMAScript의 표준 모듈 시스템으로,
브라우저와 서버 환경 모두를 지원한다.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
console.log(add(2, 3)); // ✅ 정상출력: 5
<script type="module">
태그를 사용하여 비동기적으로 로드한다.다시 내가 올린 패키지에 대한 이야기다.
나는 ESM 과 CommonJS 환경에서 코드가 동작되기를 바랬다.
ESM 은 최신 표준 모듈 시스템으로,
주로 모던 브라우저와 프론트엔드 개발에서 널리 사용되고 있다.반면, CommonJS 는 주로 Node.js와 같은
서버사이드 환경에 적합한 모듈 시스템이다.나는 SSR 환경에서도 CSS 속성에 색상값을 할당하기를 원했기 때문에,
ESM과 CommonJS 환경 모두를 지원하도록 패키지를 설계했다.
ESM과 CommonJS 모두에 대응하기 위해서는
package.json
에 관련 설정을 추가해야 한다.
이제부터 이에 대한 설정에 대해 이야기해보겠다.
package.json 파일을 보면 main
항목이 있다.
이 항목은 패키지를 CommonJS 환경에서 사용할 때의 진입점(entry point)을 지정하는 역할을 한다.
예를 들어, jeffs_package 라는 패키지를 만들었다고 가정했을 때,
사용자가 require('jeffs_package') 를 호출하면
main에 지정된 파일이 불러와진다.
ESM 환경에서는 진입점을 module
이라는 항목으로 지정할 수 있다.
주로 Webpack, Rollup 이나 브라우저에서 모듈을 불러올 경우,
module
에 지정된 파일을 로드한다.
// package.json
{
"name": "jeffs_package",
...
"main": "dist/cjs/index.js", // CommonJS 진입점
"module": "dist/esm/index.js", // ESM 진입점
...
"type" : "module"
...
}
위의 설정대로 배포할 경우, type
항목으로 인해 문제가 발생할 수 있다.
type
의 값으로 module
이라고 명시했는데,
이와 같은 경우 “.js” 확장자를 가진 파일은 ESM 모듈로 인식하기 때문이다.
{
"type" : "commonjs" // ESM 패키지로 인식
"type" : "module" // CommonJS 패키지로 인식
}
type 항목 의 기본값은 "commonjs" 이기 때문에,
CommonJS 모듈로 인식된다.
(이번엔 ESM 환경에서 문제가 생길 수 있다.)
답은 파일의 확장자이다.
파일이 CommonJS 모듈인지, ESM 모듈인지 판단하는 기준은
type 필드보다 구체적인 확장자 가 더 우선이다.
“.cjs” 로 설정된 파일은 항상 CommonJS 로더를 통해 불러와진다.
또, “.mjs” 로 설정된 파일은 항상 ESM 모듈로 인식된다.
// package.json
{
"name": "jeffs_package",
...
"main": "dist/cjs/index.cjs", // CommonJS 진입점
"module": "dist/esm/index.mjs", // ESM 진입점
...
}
이제 같은 기능을 하는 코드를 CommonJS 형식, ESM 형식으로 각각 준비하여,
main
과 module
에 상대위치로 설정하면 된다.
단순한 패키지라면 main 과 module 필드만으로도 충분할 수 있다.
CommonJS와 ESM의 진입점이 구분되기 때문에, 이 방식이 쉽고 직관적입니다.
하지만 조금 더 복잡한 구조의 패키지라면,
exports 필드를 활용한 편이 좋다.
위의 예시에서 보았듯, 진입점 외의 다른 설정은 모듈에 상관없이 공유된다.
예를들어, 기본 모듈 이외에
jeffs_package/some_feature
라는 서브 모듈을
ESM, COMMONJS 로 각각 제공해야하는 경우라면main
과module
항목 만으로 어렵다.
exports
필드를 이용할 경우, 이러한 조건부 로드가 가능해진다.
{
"name": "jeffs_package",
...
"exports": {
"import": "./dist/esm/index.js", // ESM 환경에서의 진입점
"require": "./dist/cjs/index.js", // CommonJS 환경에서의 진입점
"./some_feature": {
"import": "./dist/esm/some_feature.js", // ESM 환경에서 feature1
"require": "./dist/cjs/some_feature.js" // CommonJS 환경에서 feature1
},
"default": "./dist/index.js" // 기본 환경 파일 (fallback)
}
...
}
분량이 길어져, 생성부터 배포하기까지의 과정에 대한 설명은 다음 게시물의 내용으로 하겠다.