Vue 패키지 개발하기

작년 말부터 하고 싶었지만 시간이 안나서 못했던 일 - 바로 공식 npm 저장소에 내 라이브러리를 올릴 일이었다. 작년 하반기에는 vue.js + Typescript 로 웹 페이지를 개발하고 있었는데, 그 프로젝트에서 견적서의 금액을 한글로 바꾸는 부분을 꼭 npm에 올리고 싶었다.

그래서 결국 만들어 올렸다. 한 3~4시간 걸린듯하다. 이 패키지가 할 수 있는 일은 숫자를 한글로 변환해준다. 예를 들어 100,1000원은 ‘일백만일천원’ 이런식으로 견적서에서 사용할 수 있는 한글로 변환한다.

이 글은 해당 패키지를 만들고 퍼블리싱까지 한 일을 정리해서 올렸다. 해당 패키지는 vue-cli와 타입스크립트를 써서 만들었다. 워낙 간단한 라이브러리라 타입스크립트는 안써도 된다.

1
2
npm install -g @vue/cli
vue create my-lib

@vue/cli는 create-react-app 과 같이 vue 앱의 스켈레톤을 만들어주는 패키지다. 만들어진 디렉토리로 들어가서 앱을 실행해본다.

1
2
cd my-lib
npm run serve

뷰 기본 앱을 볼 수 있다.

모듈(컴포넌트, 필터) 패키징화

/[project-root]/src 에 filters 디렉토리를 만든다. 그 후 index.ts에 아래의 코드를 작성했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import Vue from 'vue';

/**
* 숫자를 한글로 변환한다.
* @param value Filter 값으로 넘어온 숫자
* @param priceMode true면 변환된 한글에 '일'이 붙는다. 예를 들어 '일십이만삼천원'과 같이 주로 금액을 표시할때 사용한다.
* false라면 '일'이 생략된다. 다만 억과 같이 단독으로 사용했을때 어색한 숫자는 앞에 '일'을 붙인다.
* @returns 한글로 변환된 숫자를 리턴한다. 만약 숫자가 아니거나, 범위를 초과하거나 기타 문제가 발생했을 경우 빈 문자열을 리턴한다.
*/
export function numToKr(value: number, priceMode = true): string {
if (value === 0) {
return '영';
} else if (value === 1) {
return '일';
}

const han1 = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구'];
const han2 = ['', '만', '억', '조', '경', '해'];
const han3 = ['', '십', '백', '천'];

if (!priceMode) {
// 1만 들어올 경우 문제가 생긴다.
if (value !== 1) {
han1[1] = '';
}
}

const numStr = value.toString();
const size = numStr.length;

if (size > 4 * han2.length) {
return '';
}
let kor = '';
let tmp: string[] = [];

for (let i = 1; i < numStr.length + 1; i++) {
const v1 = Number.parseInt(numStr.charAt(i - 1), 10);
if (isNaN(v1)) {
break;
}
if (v1 !== 0) {
tmp.push(han1[v1]);
tmp.push(han3[(size - i) % 4]);
}

if ((size - i) % 4 === 0 && tmp.length !== 0) {
// 일억과 같이 일을 생략하면 어색한 숫자는 '일'을 붙여준다.
kor += tmp.join('');
if (tmp.length === 2 && v1 === 1 && !priceMode) {
kor += '일';
}

tmp = [];
kor += han2[(size - i) / 4];
}
}
return kor;
}

/**
* 숫자로 된 금액을 한글로 변환한다.
* 예) 994950 -> 구십구만사천구백오십원
*/
function install() {
Vue.filter('numberToKor', numToKr);
}

export default install;

해당 모듈은 단순하게 main.ts 혹은 main.js에서 아래와 같이 사용하면 된다.

1
2
3
import NumberToKorFilter from 'vue-number-to-kor';

Vue.use(NumberToKorFilter);

1. 라이브러리 빌드 셋업

먼저 해당 모듈을 타겟으로 잡고 빌드한다. vue-cli를 설치하면 내부적으로 vue-cli-service 를 사용하는데, 이 패키지를 이용하여 해당 모듈을 빌드한다. 기존 package.json은 아래와 같은 형태일 것이다.

1
2
3
4
5
6
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test": "vue-cli-service test:unit"
}

여기에 번들링할 명령어 - build:bundle을 추가한다.

1
2
3
4
5
6
7
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"build:bundle": "vue-cli-service build --target lib --name vue-number-to-kor ./src/filters/index.ts",
"lint": "vue-cli-service lint",
"test": "vue-cli-service test:unit"
},

2. npm run build:bundle

위 명렁어 실행을 마치면 dist 디렉토리에 umd와 commonjs 용 자바스크립트 라이브러리가 번들링되어 있을 것이다.

3. package.json에 name설정 및 main과 files 추가

이 부분이 중요한데, 반드시 package.json에 main 프로퍼티를 추가해야 한다. umd와 commonjs 중 하나를 선택하면 된다.

1
2
3
{
"main": "./dist/vue-number-to-kor.common.js"
}

그리고 어떤 파일을 패키지에 넣을 것인지 npm에 알려줘야 한다. 이를 package.json에 추가한다.

1
2
3
4
5
6
7
8
9
{
"files": [
"dist/*",
"src/*",
"public/*",
"*.json",
"*.js"
],
}

package.json에 name 속성이 있다. 이게 npm 공식 저장소의 패키지 이름이 되니까 신중하게 결정하도록 한다. 물론 겹치지 않는 걸로 한다.

1
2
3
{
"name": "패키지 이름",
}

4. npm 로그인

npm 공식 저장소에 패키지를 퍼블리싱 하려면 물론 npm에 회원 가입이 되어 있어야 한다. 계정이 없으면 먼저 회원가입을 하자. 그리고 npm login 명령어를 통해서 해당 계정으로 로그인한다. 로그인 상태 확인은 npm whoami를 통해서 할 수 있다.

5. 빌드 및 퍼블리싱

퍼블리싱 전 중간에 코드를 바꾸지 않았으면 다시 빌드 하지 않아도 되지만, 얼마 걸리지 않으니까 그냥 확실하게 빌드 다시 하자.

1
npm run build:bundle

그 다음 npm 공식 저장소에 퍼블리싱을 한다.

1
npm publish --access public

다음 npm공식 저장소에서 해당 패키지가 잘 퍼블리싱 되었는지 확인한다. 이걸로 끝이다.

기타

package.json에는 상당히 많은 정보를 담아야 한다. 기왕 패키지를 배포하는거 다른 개발자가 많이 쓰면 기분이 좋지 않은가.

반드시 기입하기를 권장하는 정보는 version, license, homepage 이며, 넣으면 좋은 정보는 author, keywords, bugs가 있다.

아래는 예시 package.json이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"version": "0.1.1",
"license": "MIT",
"keywords": [
"vue",
"vuejs",
"vue-number-to-kor",
"한글",
"숫자 변환",
"filter"
],
"author": {
"name": "Jongha Kim",
"email": "kim.jongha@gmail.com"
},
"bugs": {
"url": "https://github.com/wisedog/vue-number-to-kor/issues"
},
"homepage": "https://github.com/wisedog/vue-number-to-kor",
}

ECMAScript use strict 사용하기

“use strict” 은 자바스크립트 코드를 좀 더 엄격한(strict) 모드에서 실행하도록 인터프리터에게 명령하는 자바스크립트의 지시자(directive)다.

좋은 점 3가지

  1. 코딩 실수 대신 에러를 낸다. 따라서 프로그래머는 자신의 실수를 쉽게 알아차릴 수 있다.
  2. 보통 엄격 모드에서 실행속도가 빠르다라고 알려져 있다. 직접 테스트를 안해봐서 알 수는 없지만, 엄격 모드에서는 인터프리터의 최적화 작업을 어렵게 만드는 문법을 금지시키기 때문에 빠르다.
  3. ECMAScript의 차기 버전들에서 정의 될 문법을 사용하지 못한다.

‘엄격 모드’를 사용하면 생기는 제한

이 ‘엄격 모드’로 자바스크립트 코드를 실행하고자 하면 은은하게 많은 제한이 생긴다. 주석은 크롬 콘솔에서 실행했을 때 발생하는 에러다.

  1. 생성하지 않은 변수를 사용할 수 없다. 자바스크립트에서는 실수로 변수명을 잘못 적었다면 에러를 내지 않고 그냥 새로운 전역 변수가 생성되는데, strict 모드를 사용하면 이를 막을 수 있다.

    1
    2
    "use strict";
    pi = 3.1415; // Uncaught ReferenceError: pi is not defined
  2. 변수나 함수, 삭제할 수 없는 속성을 삭제할 수 없다.

    1
    2
    3
    4
    5
    6
    "use strict";
    var pi = 3.14;
    delete pi;
    function f(p1, p2) {};
    delete f; // Uncaught SyntaxError: Delete of an unqualified identifier in strict mode.
    delete Object.prototype; // TypeError: property "prototype" is non-configurable and can't be deleted
  3. 파라미터 이름을 중복으로 쓸 수 없다.

    1
    2
    "use strict";
    function f(p1, p1) {}; // SyntaxError: duplicate formal argument p1
  4. 8진수 리터럴, 8진수 이스케이프 문자를 사용할 수 없다.

    1
    2
    3
    "use strict";
    var oct = 010; // Uncaught SyntaxError: Octal literals are not allowed in strict mode
    var oct1 = "\010"; // Uncaught SyntaxError: Octal escape sequences are not allowed in strict mode.
  5. 읽기 전용 속성이나 GET 전용 속성을 다시 쓸 수 없다.

    1
    2
    3
    4
    5
    6
    "use strict";
    var obj = {get z() {return 0} };
    Object.defineProperty(obj, "x1", {value:123, writable:false});

    obj.x1 = "hello"; // Uncaught TypeError: Cannot assign to read only property 'x1' of object
    obj.z = "world"; // Uncaught TypeError: Cannot set property z of #<Object> which has only a getter
  6. 몇 가지 키워드를 변수명으로 사용할 수 없다.

    1
    2
    3
    "use strict";
    var eval = 'hello'; // SyntaxError: 'eval' can't be defined or assigned to in strict mode code
    var arguments = 'world'; // SyntaxError: 'arguments' can't be defined or assigned to in strict mode code

브라우저 지원

2009년 ECMAScript 5에서 처음 나온 기능으로 IE9과 그 이전 IE에서는 strict 모드를 지원하지 않는다. 다만 이전 브라우져에서 큰 문제는 되지 않는데, 이전 브라우져에서는 문법적(BNF)으로 보면 “use strict”를 그저 리터럴 표현으로 인식하기 때문에 side effect 를 발생시키지 않고 따라서 그냥 무시될 뿐이다.

strict 모드를 지원하는 브라우져는 아래와 같다.

  • 크롬 버전 10 이상
  • 파이어폭스 버전 4 이상
  • 사파리 버전 5.1 이상
  • IE 버전 10 이상

ECMAScript(Javascript) 호이스팅

호이스팅(Hosting)은 자바스크립트 인터프리터가 변수 및 함수의 선언을 항상 코드 상단으로 올리는 행위를 말한다. 엄밀하게 얘기하자면 컴파일 단계에서 변수 및 함수의 선언을 먼저 읽어서 메모리 상에 저장해두는 행위에 가깝다. 예제 코드를 한 번 보자.

1
2
3
a = 1;
console.log('Result:', a);
var a;

이 코드의 실행 결과는 아래와 같다.

1
Result: 1

알다시피 자바스크립트는 변수를 선언하기 전에 사용할 수 있다. 코드 1행에서 선언하지도 않았던 a에 1을 할당했고 2행에서 콘솔에 출력한 결과가 제대로 출력되었다. 정작 변수 a는 3행에서야 나타난다.

이를 호이스팅(hoisting)이라 한다. 호이스팅의 원형 hoist는 사전적 의미로 밧줄이나 장비를 이용하여 끌어올리다는 뜻이 있다. 이렇게 자바스크립트 인터프리터는 컴파일 단계에서 변수 및 함수의 선언을 항상 컨텍스트의 상단으로 끌려올린다. 인터프리터가 선언 코드를 물리적으로 상단으로 올리는 건 아니다. 다만 변수와 함수 선언을 컴파일 단계에서 메모리에 올려두고 undefined 값을 할당한다. 그 후에 코드를 실행하기 때문에 선언이 뒤에 있더라도 선언 앞에서 해당 변수를 사용할 수 있다.

다만 선언에 강조를 한 이유는 초기화 코드는 호이스팅의 대상이 아니기 때문이다.

1
2
3
var a = 5;
console.log('a: %d b: %d', a, b);
var b = 7;

위 코드를 크롬 콘솔에서 실행하면 아래의 결과가 나온다.

1
a: 5 b: NaN

자바스크립트 인터프리터가 위 코드를 호이스팅하면 아래와 같은 형태로 코드를 읽어들인다.

1
2
3
4
5
var a;
var b;
a = 5;
console.log('a: %d b: %d', a, b)
b = 7;

즉 인터프리터는 컴파일 단계에서 변수 a와 b를 메모리 상에 올려두고 undefined 값을 할당해 둔다. 다만 호이스팅은 선언만을 대상으로 하기 때문에 초기화 구문은 해당되지 않는다. 따라서 console로 값을 찍을 무렵에는 a에는 5만 할당되어 있고 b는 undefined 상태가 된다.

함수도 호이스팅의 영향을 받는다. 함수 선언식이 호이스팅의 영향을 받는다. 자바스크립트에서 함수를 사용하는 방식을 잠깐 설명하자면 자바스크립트에서 함수를 사용하는 방식은 함수 선언식(Function Declaration)과 함수 표현식(Function Expression)으로 구분할 수 있다.

함수 선언식은 다른 프로그래밍 언어에서 사용하는 함수 선언식과 동일하다.

1
2
3
function functionDeclaration() {
// 함수 내용
}

함수 표현식은 아래와 같다. Go언어같은 최신 언어에서는 자바스크립트와 같은 함수 표현식을 많이 지원하기도 한다.

1
2
3
var functionExpr = function() {
// 함수 내용
}

예를 들어 아래와 같은 코드가 있다면,

1
2
3
4
5
6
7
8
9
10
funcDecl();
funcExpr();

function funcDecl() {
return 'decl!';
}

var funcExpr = function() {
return 'expr!';
}

호이스팅때문에 인터프리터는 아래와 같이 해석한다.

1
2
3
4
5
6
7
8
9
10
function funcDecl() {
return 'decl!';
}
var funcExpr;
funcDecl();
funcExpr();

funcExpr = function() {
return 'expr!';
}

따라서 funcExpr()를 실행할 시점에는 funcExpr()이 정의되지 않았기 때문에 크롬에서 실행하면 아래와 같은 결과가 나온다.

1
Uncaught TypeError: funcExpr is not a function

이 호이스팅 때문에 의도하지 않은 버그를 내기도 한다. 문제는 자바스크립트 인터프리터의 동작을 이해하지 못하는 사람이 코드를 짜면 버그가 잘 날 수 있다는 사실, 그리고 그 버그가 나오면 찾기 어렵다는 문제가 있다. ~C계열의 undefined behavior의 향기가…
따라서 호이스팅을 막으려면

  1. 선언을 함수나 전역 상단에 적기
  2. let 사용
  3. strict 모드 사용 - use strict 을 사용 시 선언하지 않은 변수를 사용할 수 없다.
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×