npm 패키지 제작기 6
CLI 개발
npm 에서 제공하는 모듈들을 사용하다 보면, 단순히 모듈로 사용할 뿐만 아니라 CLI에서 사용할 수 있는 모듈들이 있다.
내가 만든 모듈도 CLI를 사용해보고 싶어서 관련 내용을 찾아보았다.
기본적으로 nodejs의 설치와 함께 path가 설정된다.(nodejs 혹은 nvm 에 맞춘 경로)
해당 path에 내가 만든 모듈의 커맨드를 추가하려면 몇 가지 작업이 필요하다.
package.json
에 관련 설정을 추가한다.- 커맨드 관련 스크립트를 작성한다.
- 모듈을 global로 설치한다.
package.json
CLI에서 nodejs 모듈을 사용하려면 package.json
의 "bin"
필드에 사용할 스크립트 경로를 명시해줘야 한다.
간단한 예시로 확인해보자. 아래는 내가만든 모듈의 package.json
을 긁어온 것이다.
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
{
"name": "markdown-grouper",
"version": "1.6.2",
"description": "package for grouping html parsed from markdown. This package uses header for grouping. You can set class or id for each group, so you can customize css or etc for each group.",
"bin": {
"mdg": "./bin/index.mjs"
},
"main": "./lib/main.js",
"exports": {
".": {
"require": {
"default": "./lib/main.js",
"types": "./lib/main.d.ts"
},
"import": {
"default": "./lib/main.mjs",
"types": "./lib/main.d.mts"
}
}
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"uglify": "uglifyjs ./lib/main.js ./lib/main.mjs -o ./output/main.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/momo1108/markdown-grouper.git"
},
"keywords": [
"markdown",
"group"
],
"author": "momo1108",
"license": "ISC",
"bugs": {
"url": "https://github.com/momo1108/markdown-grouper/issues"
},
"homepage": "https://github.com/momo1108/markdown-grouper#readme",
"dependencies": {
"@liquify/prettify": "^0.5.5-beta.1",
"chalk": "^4.1.2",
"commander": "^11.1.0",
"marked": "^11.0.0"
},
"devDependencies": {
"gulp": "^4.0.2",
"gulp-javascript-obfuscator": "^1.1.6",
"gulp-rename": "^2.0.0",
"gulp-uglify": "^3.0.2"
}
}
"bin"
필드를 보면 모듈명인 "markdown-grouper"
는 너무 길어서 Key 부분에 따로 "mdg"
라고 명시해 두었다.
이렇게 되면 CLI에서 커맨드를 사용할 때 mdg
라는 커맨드로 사용이 가능하다.
mdg
커맨드가 실행될 경우, 프로젝트의 bin 폴더에 있는 index.mjs 스크립트를 실행하겠다는 뜻이다.
스크립트 작성
이제 커맨드를 실행하면 실제로 작동할 코드를 작성해야 하는데, Node.js 로 작성하는 가장 기본적인 방법은 process 모듈을 사용해 사용자의 입력값을 사용하는 것이다.
이에 관련된 간단한 스크립트를 작성해보자.
1
2
3
4
5
6
7
8
#!/usr/bin/env node
(()=>{
console.log("테스트 스크립트입니다.");
console.log("입력받은 커맨드들은 다음과 같습니다.");
console.log(process.argv);
})();
./bin/test.mjs
파일에 위와 같은 스크립트를 작성하고, 직접 node 명령어로 먼저 실행해 보자.
1
node ./bin/test.mjs test1 test2 test3
실행 결과는 다음과 같다.
1
2
3
4
5
6
7
[
'C:\\Users\\sheldon\\.nvm\\versions\\node\\v18.15.0\\bin\\node.exe',
'C:\\Users\\sheldon\\Desktop\\test\\test2.js',
'test1',
'test2',
'test3'
]
첫 번째 요소는 바로 Node.js 실행 경로process.execPath
이고, 두 번째 요소는 실행될 JavaScript 파일의 경로이다.
그 뒤에 등장하는 나머지 요소들이 바로 사용자가 입력한 값들이다. 보다시피 공백(스페이스)를 기준으로 하나씩 요소가 추가된다.
이 입력값들을 실행되는 스크립트 내부에서 구분하면서 그에따라 실행되는 로직을 작성하는 것이 가장 기본적인 사용법 같다.
이 상태에서 처음에 설명했던 package.json
의 bin
필드를 추가하고, 커맨드를 테스트할 수 있다.
1
2
3
4
5
6
7
8
{
"name": "mytestapp",
"version": "1.0.0",
"description": "test package",
"bin": {
"mytest": "./bin/test.mjs"
}
}
간단하게 테스트해보기 위한 package.json
의 예시이다.
bin
필드를 보면, mytest
라는 커맨드에 실행할 스크립트로 ./bin/test.mjs
를 명시해두었고, 이제 이를 테스트해보기 위해 global로 설치를 해주자.
npm i -g ./
명령어를 통해 현재 테스트 모듈을 글로벌로 설치할 수 있다.
설치시에 name
필드에 명시되어있는 이름으로 모듈 설치가 진행되므로, 테스트 후 삭제는 npm uninstall -g mytestapp
명령어를 사용하면 된다.
글로벌 설치 후 다음과 같이 커맨드를 실행해보자.
1
mytest test1 test2 test3
그러면 실행결과는 역시 다음과 같을 것이다.
1
2
3
4
5
6
7
[
'C:\\Users\\sheldon\\.nvm\\versions\\node\\v18.15.0\\bin\\node.exe',
'C:\\Users\\sheldon\\Desktop\\test\\test2.js',
'test1',
'test2',
'test3'
]
여기까지가 가장 기본적인 CLI 모듈을 만드는 방법이다.
스크립트 내용 중 첫번째 라인의 코드
#!/usr/bin/env node
는 무슨 의미일까?이는 shebang line 으로서 사용되는 라인이다.
shebang line 이란?
해쉬(#)와 느낌표(!)로 이루어진 문자 시퀀스이다. 실행 가능한 스크립트의 맨 첫줄에 위치하며, 셔뱅이 존재하는 스크립트의 경우 Unix 계열의 운영체제에서는 program loader가 #! 뒤쪽의 내용을 interpreter directive 로서 사용한다.
즉, 지정된 인터프리터 프로그램이 대신 실행되어 스크립트의 실행을 시도할 때 처음 사용되었던 경로를 인수로서 넘겨주게 된다.
CommanderJS
CLI 모듈을 만드는 방법을 알아보긴 했으나, 사용자로부터 받는 입력은 너무나도 생각해야 할 변수가 많다.
이러한 모든 변수들을 우리가 직접 처리하는 로직을 작성하기는 너무 빡세다.
그렇다면 이를 편하게 해주기 위한 모듈은 없을까?
없을리가 없다… 왠만한 모듈들은 이미 다 만들어져 있기 때문에, 우리는 검색만 잘 하면 된다.
이쪽 계열에 가장 인기가 많은 모듈을 찾아보니, 바로 Commander.js
라는 모듈이 있었다.
무려 주간 다운로드가 5900만;;
잡설은 각설하고 사용방법만 빠르게 알아보자.
일단 이 모듈은 실행될 스크립트 내부에서 사용되는 모듈이기 때문에, 기본적인 package.json
설정은 선행해주면 된다.
이전에 알아보았던 기본적인 커맨드 명령어를 잠깐 다시 살펴보자.
1
mytest test1 test2 test3
첫번째는 패키지의 실행 커맨드이고, 그 뒤부터가 입력값들이었다.
Commander.js
를 활용해서 내가 사용할 커맨드 구조는 다음과 같다.
1
mytest command -option
위 구조의 커맨드를 작성하기 위한 코드를 알아보자.
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
#!/usr/bin/env node
import { Command } from "commander";
const program = new Command();
program
.name("모듈명")
.description("모듈 설명")
.version("모듈 버전");
program
.command("커맨드명")
.description("커맨드 설명")
.argument("커맨드 입력값", "입력값에 대한 설명")
.option(
"커맨드 옵션명",
"커맨드 옵션 설명"
)
.action((arg, options) => {
// arg : 커맨드 입력값
// options : 커맨드 옵션 입력값
// 실행할 스크립트 작성
});
program.parse(); // 커맨드가 정의되면 불러와주는 역할.
위 설명이 잘 와닿지 않는다면, 간단한 예시를 한번 작성해보자.
숫자 두개를 입력받아서 더하거나 빼는 커맨드를 작성해보자.
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
#!/usr/bin/env node
import { Command } from "commander";
const program = new Command();
program
.name("mytestapp")
.description("test package")
.version("1.0.0");
program
.command("calc")
.description("calculate two numbers")
.argument("<number...>", "numbers")
.option(
"-m, --minus",
"Option for subtraction"
)
.action((arg, options) => {
let result = 0;
if(options.minus){
result = parseInt(arg[0]) - parseInt(arg[1]);
} else {
result = parseInt(arg[0]) + parseInt(arg[1]);
}
console.log(result);
});
program.parse();
command 함수에 calc
라는 커맨드명을 명시하고, argument
함수에 사용자로부터 받을 값이 숫자임을 명시하는데, argument 이름(number) 뒤에 ...
을 붙여서 여러개의 숫자를 배열 형태로 받을 수 있게 한다. 만약 ...
을 사용하지 않으면 첫번째 숫자만 입력받게 된다.
단, 입력값은 문자열 형태로 들어오기 때문에 숫자형태로 사용하려면 parseInt
함수를 사용해야 한다.
option 함수에 -m 혹은 –minus 옵션을 사용할 수 있도록 명시해 놓는다.
이는 사용자로부터 커맨드에 -m
혹은 --minus
옵션을 입력받을 수 있도록 하는 것이다.
사용된 경우에는 action 함수의 콜백함수에서 options
매개변수에 값이 추가된다.(아래의 객체처럼)
1
2
3
{
minus: true
}
최종적으로 사용자가 입력하는 커맨드 예시는 다음과 같다.
1
2
3
mytest calc 4 2 # 출력값 6
mytest calc 4 2 -m # 출력값 2
mytest calc 4 2 --minus # 출력값 2
정리
모듈을 만들면서 단순히 스크립트 내부에서 모듈의 역할을 하는 것 뿐 아니라, 커맨드라인에서의 사용을 가능하도록 하는 방법에 대해서 알아보았다.
다양한 모듈들로 인해 자동화가 되어있는 부분들이 대부분이기 때문에, 난이도는 어렵지 않았다.