home
🧨

[메디스트림 개발팀] Vue.js npm 패키지 탄생비화

안녕하세요. 메디스트림의 프론트엔드 개발자 서보현입니다.
3개월의 인턴 기간을 거쳐 얼마 전 프론트 멤버로 합류하게 됐고, 인턴 기간중 실제 서비스에서 사용할 컴포넌트를 오픈소스 패키지화하는 프로젝트를 진행했습니다.
좋은 경험이지만 시간과 노력이 많이 들기 때문에 실제 서비스에서 패키지화를 거치는 방식은 잘 택하지 않을텐데요. 인턴이었기에 감사한 기회가 주어졌습니다. ( 이 자리를 빌어서 또 좋은 프로젝트를 선정해주신데에 감사.. )
사내에서 소소한 기술 세미나를 시작하며 개발 과정 중 가치있는 경험을 정리해 공유해보자는 기획 아래 이번 글을 작성했습니다.

Modal 컴포넌트 구현

전체 글은 패키지화에 초점을 맞췄지만, 구현할 컴포넌트에 관해서도 간략히 설명을 하고 넘어가겠습니다.
패키지화할 컴포넌트는 Modal 컴포넌트인데 일반적인 Modal의 구현 방식과는 차이점을 가져가기로 했습니다.
App, Viewport, Modal을 옆에서 본다고 가정해보자
< 일반적인 Modal 방식 >
위처럼 App, Viewport, Modal이 존재하고 옆에서 본 모습이라고 했을 때 일반적으로 Modal은 아래와 같이 Viewport에 상대적인 위치에 존재하게 됩니다.
App을 스크롤한다고 가정하면 Modal은 Viewport의 상대적 위치에 그대로 고정돼있을겁니다.
그런데 만약 Modal 내부의 컨텐츠가 많으면 어떻게 할까요?
그럴 때는 Modal을 Viewport과 같거나 작도록 만들고 Modal 내부에 스크롤을 가능하게 해서 해결합니다.
< 구현할 Modal 방식 : vue-fullpage-modal >
구현할 Modal 컴포넌트는 조금 다른식으로 Modal을 띄우도록 했습니다.
App을 잠시 배경처럼 고정시킨 후 Modal이 그 자리를 대신하게 만드는 방식으로 Modal을 띄우도록 했습니다.
이런 식으로 Modal을 구현하면 Modal 내부에 컨텐츠가 많든 적든 스크롤이 Modal을 기준으로 일어나기 때문에 스크롤에 관련한 이슈를 걱정할 필요가 없습니다. 이를 내부적으로는 fullpage방식 Modal이라고 부르기로 했습니다. ( 그래서 컴포넌트 이름이 vue-fullpage-modal이 됐습니다 )

Best Practices 리서치

컴포넌트를 구현한 이후에는 본격적으로 npm 패키지를 배포하기 위한 작업입니다.
패키지화의 필수요소는 소스코드와 package.json 입니다.
막막했기 때문에 일단 best practice를 검색해보고, 다른 패키지들의 directory structure와 package.json도 살펴봤습니다.

directory

# . ├── build/ ├── libs/ ├── src/ ├── tests/ ├── vendor/ └── package.json
Plain Text
복사
vue
vue-final-modal

package.json

// { "name": "package-name", "version": "1.0.0", "description": "A description to show on NPM", "keywords": ["search tags"], "author": "Your Name", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "repository": { "type": "git", "url": "https://github.com/..." }, "scripts": {}, "devDependencies": {}, "dependencies": {} }
JSON
복사
vue-final-modal
vue

Best Practice들을 기반으로 예상

리서치를 해보니 여러 패키지들에서 비슷한 directory structure와 package.json 속성들이 반복됐습니다. 이를 기반으로 완성된 Modal 패키지의 directory와 package.json을 예상해봤습니다.
directory 이름과 package.json 속성의 이름으로 역할 추측이 가능했기 때문에 예상해보는 것이 어렵지 않았습니다.

directory

. ├── (dist/) ├── docs/ ├── src/ ├── .gitignore ├── LICENSE ├── README.md ├── package.json └── webpack.config.js
Markdown
복사
dist 디렉토리에 소괄호가 있는 이유는 빌드 및 배포 과정에서만 생성될 디렉토리이기 때문입니다.

package.json

{ "name": "my-package", "version": "0.1.1", "description": "good package", "main": "dist/myPackage.js", "scripts": { "build": "webpack" // ... }, "files": [ "src", "dist/*.js" ], // files는 추가로 설명할 속성 "repository": { "type": "git", "url": "https://github.com/medistream-team/vue-fullpage-modal" }, "keywords": [ "vuejs", "component", "vue" ], "author": "my-id", "license": "MIT", "dependencies": { "vue": "^2.5.2" }, "devDependencies": { "@babel/core": "^7.13.16", "webpack": "^4.45.0" // ... } }
JSON
복사
files 는 추가로 설명할 속성입니다.
패키지 출시 후 돌아보니 최소한의 구조에서 살을 하나하나 붙이는 이미지를 떠올려도 괜찮았을 것 같았습니다.

build & publish

소스코드와 package.json이 준비됐다면 바로 publish가 가능합니다.
그렇지만 보통 build 과정을 통해 패키지의 용량을 줄이고, 여러 모듈 시스템에서도 사용할 수 있게 하는 등 추가적인 보완 작업을 해줍니다.
모듈 시스템은 CommonJS, ESM, AMD 등을 말합니다. 각 모듈 시스템에 대한 내용은 뒤에서 더 살펴보도록 하겠습니다.
build
빌드 툴로는 rollup, parcel, webpack, vite 등 여러가지가 있습니다. 이번 프로젝트에서 빠른 빌드 속도로 주목을 받고있는 vite를 사용할까 고려했습니다. 그런데 vue2를 공식적으로는 지원하지 않는 등의 상황이 있고, 빌드 속도가 결정적인 고려 요소가 아니었기 때문에 정통적인 webpack을 사용하기로 했습니다.
패키지에 맞게 webpack을 설정해준 뒤 빌드를 합니다.
npm run build Hash: 3gfsdc123... Version: webpack 4.46.0 Time: 1654ms Built at: 07/07/2021 12:35:23 PM Asset Size Chunks Chunk Names myPackage.js 27.3 KiB 0 [emitted] main
Shell
복사
빌드 결과물 저장 디렉토리로 dist, 파일이름을 myPackage.js로 정했기 때문에 dist/myPackage.js 파일이 생성됩니다.
package가 읽어들일 파일 지정
import 'my-package'를 할 때 읽어들이는 파일은 package.json의 main 속성에 지정된 파일입니다. 기본 main 속성은 패키지 root 폴더의 index.js로 지정되기 때문에 명시적으로 빌드된 파일로 지정해줍니다.
# package.json { ... "main": "dist/myPackage.js" ... }
Shell
복사
publish
빌드가 됐고 package.json 설정이 완료됐으면 publish를 할 준비가 됐습니다!
> npm run publish npm notice npm notice 📦 my-package@0.1.1 npm notice === Tarball Contents === npm notice 27.3kB dist/myPackage.js npm notice 42.2kB src/components/MyComponent.vue npm notice 1.2kB src/index.js npm notice 1.0kB package.json npm notice 1.9kB README.md npm notice === Tarball Details === npm notice name: my-package npm notice version: 0.1.1 npm notice package size: 12.1 kB npm notice unpacked size: 45.5 kB npm notice shasum: 2asdfasdfsdfsdfsa9b0857858dd295cd8801449 npm notice integrity: sha512-NFHSDASDFAa5T[...]Kx1kpZyiJ5KZA== npm notice total files: 5 npm notice + my-package@0.1.1
Shell
복사
files
하지만 포함된 파일들을 살펴보면 조금 이상한 점이 있습니다.
실제로 패키지에 포함하고 싶은건 dist/myPackage.js 파일만인데, index.jsMyComponent.vue 파일도 같이 포함돼있습니다.
이는 npm publish할 때 기본설정 때문인데요. 기본적으로 패키지 root 디렉토리 안의 파일 및 디렉토리를 모두 포함하도록 돼있습니다. 원하는 파일만 포함시켜주기 위해서 package.json에 files 속성을 사용합니다.
{ ... "files": "dist" // 혹은 "files": "dist/myPackage.js" ... }
JSON
복사
files 속성은 .npmignore 파일과 비교해서 뒤에 추가 설명을 하겠습니다.
* 실제로는 vue plugin을 패키지화한 것
Component를 만들었지만 Vue에서 해당 컴포넌트를 사용하려면 플러그인을 만들고, 플러그인을 통해서 컴포넌트를 등록하는 과정이 필요합니다. 그래서 패키지의 main 파일은 사실 plugin을 가리킵니다.
import MyComponent from 'my-component' Vue.use(MyComponent)
JavaScript
복사
// 내부적으로 벌어지는 일 Vue.component('my-modal', MyModal) Vue.$myFunction = function(){ ... } 등등..
JavaScript
복사
이렇게 npm package를 publish 했습니다.
패키지 publish까지의 과정을 간략하게 살펴보았고 이제 좀 더 깊은 내용으로 넘어가볼까 합니다.

모듈 시스템

JavaScript에는 CommonJS, ES Module, AMD 등 여러 모듈 시스템이 있습니다.
아주 간략히 비교해보겠습니다. 자세히 다룬 글들을 끝에 추가해놨으니 읽어보시기를 추천드립니다.
CommonJS
동기적으로 작동하고, 심플한 사용방식을 가지고 있습니다. Node.js에서 처음 도입된만큼 서버사이드에서 사용하기에 적합합니다.
AMD
비동기를 지원하는 특징이 있습니다. Frontend에서 사용하기 위해서 도입된 모듈 시스템입니다.
ESM
자바스크립트 공식 모듈 시스템으로 비동기적인 특징과 CommonJS의 심플한 사용방식을 모두 가지고 있습니다.
UMD
하지만 여전히 CommonJS 방식과 ESM, 저는 잘 접해보지 못했지만 AMD 방식도 혼용해서 사용되고 있는 상태라고 합니다. 이런 혼용으로 인한 복잡함을 해결하기 위해 대용으로 쓰는 방식이 UMD인데요. 패키지를 사용하는 곳의 모듈 시스템에 따라 CommonJS, ES Module, AMD에서 패키지를 모두 사용할 수 있도록 해줍니다. 정확히는 디자인 패턴에 가깝다고 합니다.
UMD 예제 코드
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['exports', 'b'], factory); } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { // CommonJS factory(exports, require('b')); } else { // Browser globals factory((root.commonJsStrict = {}), root.b); } }(this, function (exports, b) { //use b in some fashion. // attach properties to the exports object to define // the exported module properties. exports.action = function () {}; })); // https://riptutorial.com/javascript/example/16339/universal-module-definition--umd-
JavaScript
복사
webpack을 통한 모듈 시스템 선택
webpack에서는 이미 모듈 시스템을 선택할 수 있는 옵션을 제공하고 있습니다.
// webpack.config.js { ... "output": { "libraryTarget": "umd", ... } ... }
JavaScript
복사

package.json

package.json에서도 여러 속성들 중 더 살펴볼만한 속성들이 있습니다.

files

패키지에 포함할 파일들을 지정해주는 속성입니다.
패키지에 포함할 파일을 설정하는 2가지 방식이 있습니다.
blacklist방식인 .npmignore 와 whitelist 방식인 files 속성입니다.
blacklist 방식에서는 패키지에서 제외하고 싶은 파일들을 선택합니다.
whitelist 방식에서는 패키지에 포함하고 싶은 파일들을 선택합니다.
여러 라이브러리 git repository를 돌아다니다보면 .npmignore 를 사용하는 곳이 거의 없다는 사실을 발견할 수 있습니다. .npmignore가 보편적인 방식인줄 알았기에 이 사실에 꽤나 놀랐었는데, 여기에는 이유가 있었습니다.
.npmignore사용 시 주의할 점
.gitignore.npmignore가 동시에 존재할 때 npm에서는 .npmignore만 참조합니다.
.gitignore만 있으면 npm에서 .gitignore를 참조해서 패키지에 포함할 파일이나 폴더를 결정하는데, 둘 다 존재하면 .gitignore의 내용은 무시하고 .npmignore의 내용만 참조하기 때문에 헷갈릴 수 있는 여지가 있습니다.
또한 blacklist 방식을 사용하면 다른 파일이나 폴더가 추가될때마다 이를 blacklist에 추가해줘야합니다.
whitelist 방식에서도 패키지 구성이 변경되면 whitelist에 추가해야해서 조삼모사가 아닌가 싶은데, AWS Crendential 관련 파일 등 패키지에 '포함되지 않아야할 파일이 포함되는 것'이 상당한 risk를 가지기 때문에 blacklist 방식보다 whitelist 방식이 더 안전하다고 할 수 있습니다.

version

x.y.z의 형태의 버저닝을 semantic versioning. 줄여서 semver라고 부릅니다. npm의 여러 패키지들을 사용하면서 눈으로는 익숙하지만 실제로 어떤 의미를 담고있는지는 몰랐기 때문에 이번 기회에 versioning에 대해서도 찾아봤습니다.
주요 내용은 아래와 같습니다.
기존 버전과 호환되지 않게 API가 바뀌면 x를 올리고 (major 버전)
기존 버전과 호환이 되면서 새로운 기능이 추가되면 y를 올리고 (minor 버전)
기존 버전과 호환이 되면서 버그가 수정된 것이라면 z를 올립니다. (patch 버전)
major 버전 0(0.y.z)은 초기 개발을 위해 씁니다. 아무 때나 마음대로 바꿀 수 있습니다. 안정판으로 보지 않는 것이 좋습니다.
patch버전 바로 뒤에 붙임표(-)를 붙이고 마침표(.)로 구분된 식별자를 더해서 정식 배포를 앞둔 (pre-release) 버전을 표기할 수 있습니다. 식별자는 반드시 아스키(ASCII) 문자, 숫자, 붙임표로만 구성합니다[0-9A-Za-z-]. 정식배포 전 버전은 아직 불안정하며 연관된 일반 버전에 대해 호환성 요구사항이 충족되지 않을 수도 있습니다. 예: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92.
~를 붙이는 Tilde Ranges와 ^를 붙이는 Caret Ranges 는 내용이 조금 길어 아래의 좋은 링크로 대체 합니다.

문서화

네.. 패키지를 출시해서 끝이난줄 알았는데 문서화가 남았습니다..
다른 사람이 사용할 수 있게 하려면 문서화가 아주 중요합니다. 대체할 수 있는 다른 패키지들이 있기 때문에 제가 개발한 내용의 사용법을 가독성 있게 알려줄 수 있는 문서화가 아주 중요했습니다.
README.md
이게 전부인 README.md
저장소를 방문하면 첫 눈에 예시부터, 설치와 사용까지 간단하게 눈에 들어오게하고 싶었습니다.
simple is the best!!
Documentation
문서도 볼 수 있고 조작해볼 수 있는 예제 위주로 구성하려고 노력했습니다.
출시 후 약 한달
출시 후 약 한달이 지난 시점에 5명의 전혀 모르는 사용자가 사용하고 있었습니다 . 어떠한 홍보도 하지 않았는데 어떻게 컴포넌트를 찾았는지 참 신기했습니다. 예시 gif 이미지를 봤을 때 모바일 Navigation drawer 같아 보여서 도입하지 않았을까 합니다.

후기

한달 동안 작업을 했지만 1.0.0 버전을 출시할 정도까지 되지 못했습니다. 해야할 다른 일들이 있었기 때문에 더 미루지 못하고 0.x 버전으로 실제 서비스에서 필요한정도로만 패키지로 완성하고 이후 필요에 따라 발전시키는 것으로 결정했습니다.
컴포넌트 구현부터 npm 패키지 출시까지 일련의 과정을 겪어보니 느끼는 점들이 있었습니다.
패키지의 당위성, 필요성이 중요한 것 같다.
'패키지가 왜 존재해야하는지, 정말 유용한지'가 결국 중요한 것 같았습니다. 패키지화를 하려고 시작하기보다 필요한 것이 있어서 만들고 이후에 많은 사람들이 사용할 수 있게 하기 위해서 패키지화를 하는 것이 순서에 맞는 것 같았습니다.
그래서 예시를 모바일위주로 구성했습니다. vue-fullpage-modal 이 pc, mobile에서 모두 사용이 가능하지만 Viewport가 작아도 유용한 Modal을 만들기 위해 출발했기때문에 그걸 명확히 나타내기로 했습니다. ( 문서에 pc 예시도 넣어야 하는데 .. 항상 노오력이 부족한듯 합니다 )
문서화에도 자원을 많이 할당하자
실제 컴포넌트를 구현하는데 절반의 시간 그리고 문서화에 나머지 절반이 소요됐습니다. 그래도 여전히 발전시키고 추가해야하는 내용이 많이 남아 있습니다. 문서화를 하지 않으면 패키지를 사용할 개발자가 어떻게 사용해야하는지 알기가 너무 어렵고, 너무 복잡하거나 가독성이 떨어지는 문서는 없는 것과 똑같기 때문에 좋은 문서를 위해 많은 시간을 사용해야 했습니다.
그래서 계획을 짤 때도 구현 뿐만이 아니라 문서화에 많은 관심을 쏟아야합니다.
오픈소스를 지향한다면 필요한 것들이 더 많다.
오픈소스 패키지라고 하기에는 아직 더 필요한 것들이 많습니다. 단순히 사용하는 것이 아니라 여러 사람들이 소스코드를 발전시키고 도와주는 것이 중요하기에 아래와 같은 기반이 추가적으로 필요했습니다.
기여가이드 ( CONTRIBUTING )
행동 강령 ( Code of Conduct )
테스팅 & 자동화
이슈 template
등등 ..
이렇게 npm 패키지화의 경험을 글로 정리해봤습니다. 처음부터 끝까지 해본 경험은 개인적인 career path 형성에 좋은 발자취로 남을 것 같습니다. 글을 읽고 약간의 간접경험이라도 된다면 좋겠습니다. 긴 글 읽어주셔서 감사합니다. 이상 메디스트림 프론트엔드 개발자 서보현이었습니다.

참고하면 좋은 링크들

모듈 시스템
files
semver
채용 페이지 바로가기 메디스트림에서는 훌륭한 인재를 적극 채용중입니다. 한의계의 새로운 흐름에 합류하세요.