CLI 만들기 이모저모
개발자로서 CLI(Command Line Interface) 도구는 일상적으로 사용하는 필수 도구다. npm, git, vite 등 매일 사용하는 도구들도 사실은 모두 CLI 프로그램이다. 이번 글에서는 Node.js를 사용해 직접 CLI 도구를 만드는 방법부터 배포하는 방법까지 알아보자.
CLI의 기본 개념
CLI(Command Line Interface)는 그래픽 인터페이스(GUI) 없이 텍스트 명령으로 프로그램을 실행하는 방식이다. 개발자들이 선호하는 이유는 간단하다:
- 자동화가 용이함: 스크립트로 여러 명령을 연결해 복잡한 작업을 자동화할 수 있다.
- 리소스 효율성: GUI보다 적은 시스템 리소스를 사용한다.
- 원격 작업: SSH 등을 통해 원격 서버에서도 쉽게 작업할 수 있다.
- 정확성과 재현성: 동일한 명령어는 항상 동일한 결과를 보장한다.
Node.js는 크로스 플랫폼 지원과 풍부한 패키지 생태계 덕분에 CLI 도구 개발에 매우 적합하다.
Node.js 패키지의 bin 속성 이해하기
NPM으로 패키지를 설치할 때 어떤 패키지는 전역 명령어를 제공하고, 어떤 패키지는 그렇지 않다. 이 차이는 package.json의 bin 속성에서 비롯된다.
bin 속성이란?
bin 속성은 패키지가 제공하는 실행 가능한 파일과 그 명령어 이름을 연결한다.
{
"name": "my-cli-tool",
"bin": {
"my-tool": "./bin/index.js"
}
}
또는 명령어가 하나만 있을 경우 더 간단하게:
{
"name": "my-cli-tool",
"bin": "./bin/index.js"
}
위 경우 패키지 이름이 곧 명령어 이름이 된다.
설치 시 일어나는 일
패키지를 설치하면:
- 로컬 설치: node_modules/.bin/ 디렉토리에 실행 파일 심볼릭 링크가 생성된다.
- 전역 설치: 시스템 PATH에 포함된 디렉토리(예: /usr/local/bin)에 심볼릭 링크가 생성된다.
심볼릭 링크란?
심볼릭 링크(Symbolic Link, Symlink)는 다른 파일이나 디렉토리를 가리키는 특별한 파일이다. 원본 파일에 대한 "바로가기"라고 생각하면 된다.
- 윈도우의 '바로가기'와 유사하지만 시스템 수준에서 동작한다.
- 실제 파일이 아닌 파일 경로를 가리키는 포인터다.
- 원본 파일이 수정되면 심볼릭 링크를 통해 접근해도 수정된 내용을 볼 수 있다.
CLI 도구에서 심볼릭 링크는 중요한 역할을 한다:
- 실제 실행 파일은 node_modules/패키지명/ 안에 있지만, 심볼릭 링크를 통해 node_modules/.bin/에서 바로 접근할 수 있다.
- 이 덕분에 package.json의 scripts에서 경로 없이 명령어 이름만으로 실행할 수 있다.
로컬 설치된 CLI 도구 실행하기
로컬에만 설치된 CLI 도구를 실행하는 방법은 세 가지가 있다:
- 전체 경로 사용: ./node_modules/.bin/vite
- npx 사용: npx vite
- npm scripts 사용:
{
"scripts": {
"dev": "vite"
}
}
vite를 예로 들어보자:
{
"bin": {
"vite": "bin/vite.js"
}
}
이 정의 덕분에 전역 설치 시 터미널에서 바로 vite 명령을 실행할 수 있다.
Shebang과 실행 권한
CLI 도구의 엔트리 파일 첫 줄에는 보통 다음과 같은 shebang 문이 있다:
#!/usr/bin/env node
Shebang이란?
Shebang(#!)은 스크립트 파일을 어떤 인터프리터로 실행할지 지정하는 선언이다. #!/usr/bin/env node는 "환경 변수 PATH에서 node를 찾아 이 파일을 실행하라"는 의미다.
이 선언이 중요한 이유:
- 사용자가 직접 node 명령어를 입력하지 않고도 스크립트를 실행할 수 있게 한다.
- 다양한 환경에서 Node.js의 위치가 다를 수 있는데, env를 통해 PATH에서 찾기 때문에 이식성이 좋다.
실행 권한 설정
유닉스 계열 시스템(Linux, macOS)에서는 파일에 실행 권한이 필요하다:
chmod +x ./bin/my-cli.js
npm은 패키지 설치 시 자동으로 bin 파일에 실행 권한을 부여한다.
설치 위치 확인하기
어떤 CLI 도구가 시스템 어디에 설치되었는지 확인하려면:
which vite # Unix/Linux/macOS
where vite # Windows
명령줄 인자 처리하기
CLI 도구의 핵심은 사용자 입력을 처리하는 것이다. Node.js에서는 process.argv 배열을 통해 명령줄 인자에 접근할 수 있다.
process.argv 이해하기
Node.js에서 process.argv는 명령줄에서 전달된 모든 인자를 담고 있는 배열이다. 이 배열에는 다음 정보가 순서대로 포함된다:
- Node.js 실행 파일 경로 (인덱스 0)
- 실행 중인 JavaScript 파일 경로 (인덱스 1)
- 사용자가 입력한 실제 인자들 (인덱스 2부터)
#!/usr/bin/env node
// 모든 명령줄 인자 출력
console.log(process.argv);
이 스크립트를 my-cli.js로 저장하고 다음과 같이 실행하면:
node my-cli.js arg1 arg2 --option value
출력:
[
'/usr/local/bin/node', // Node.js 실행 파일 경로
'/path/to/my-cli.js', // 실행된 스크립트 경로
'arg1', // 첫 번째 인자
'arg2', // 두 번째 인자
'--option', // 옵션 플래그
'value' // 옵션 값
]
실제 인자는 배열의 세 번째 요소(인덱스 2)부터 시작한다:
#!/usr/bin/env node
// 실제 인자만 처리 (Node 실행 파일과 스크립트 경로 제외)
const args = process.argv.slice(2);
console.log("Arguments:", args);
실행 결과:
Arguments: [ 'arg1', 'arg2', '--option', 'value' ]
기본적인 인자 파싱
간단한 CLI 도구라면 직접 인자를 파싱할 수 있다:
const args = process.argv.slice(2);
const options = {};
let currentOption = null;
// 기본적인 파싱 로직
args.forEach((arg) => {
if (arg.startsWith("--")) {
// 옵션 이름 저장 (--없이)
currentOption = arg.slice(2);
options[currentOption] = true; // 기본값은 true
} else if (currentOption) {
// 이전 옵션의 값 설정
options[currentOption] = arg;
currentOption = null;
}
});
console.log("Parsed options:", options);
예를 들어 다음과 같이 실행하면:
node my-cli.js --name John --age 30 --isAdmin
출력 결과:
Parsed options: { name: 'John', age: '30', isAdmin: true }
하지만 복잡한 CLI 도구라면 전문 라이브러리를 사용하는 것이 좋다.
Commander.js로 고급 CLI 만들기
Commander.js는 Node.js CLI 도구 개발에 가장 널리 사용되는 라이브러리다. 명령어, 옵션, 인자를 쉽게 정의하고 파싱할 수 있다.
설치
npm install commander
기본 사용법
#!/usr/bin/env node
// ES 모듈 형식 (mjs)
import { Command } from "commander";
const program = new Command();
// 메타데이터 설정
program.name("my-tool").description("CLI tool description").version("1.0.0");
// 옵션 정의
program
.option("-d, --debug", "디버그 모드 활성화")
.option("-f, --file <path>", "처리할 파일 경로")
.option("-n, --number <number>", "숫자 값", parseInt);
// 명령어 정의
program
.command("generate <n>")
.description("파일 생성")
.action((name, options) => {
console.log(`파일 생성: ${name}`);
if (program.opts().debug) {
console.log("디버그 정보:", { name, options });
}
});
// 프로그램 실행
program.parse();
이제 다음과 같이 사용할 수 있다:
my-tool --debug generate myfile
my-tool -f config.json -n 42 generate myproject
첫 번째 명령어 실행 결과:
파일 생성: myfile
디버그 정보: { name: 'myfile', options: {} }
두 번째 명령어 실행 결과:
파일 생성: myproject
참고로 --debug 옵션이 없으면 디버그 정보는 출력되지 않는다.
하위 명령어 구성
큰 CLI 도구는 git처럼 하위 명령어를 구성할 수 있다:
// ES 모듈 형식 (mjs)
import { Command } from "commander";
const program = new Command();
program
.command("init")
.description("프로젝트 초기화")
.action(() => {
console.log("프로젝트를 초기화합니다...");
console.log("package.json 파일을 생성했습니다.");
});
program
.command("build")
.description("프로젝트 빌드")
.option("--minify", "코드 최소화")
.action((options) => {
console.log("프로젝트 빌드를 시작합니다...");
if (options.minify) {
console.log("코드 최소화가 활성화되었습니다.");
}
console.log("빌드가 완료되었습니다!");
});
터미널에서 사용 예:
# 초기화 명령어 실행
$ my-tool init
프로젝트를 초기화합니다...
package.json 파일을 생성했습니다.
# 빌드 명령어 (기본 옵션)
$ my-tool build
프로젝트 빌드를 시작합니다...
빌드가 완료되었습니다!
# 빌드 명령어 (최소화 옵션 활성화)
$ my-tool build --minify
프로젝트 빌드를 시작합니다...
코드 최소화가 활성화되었습니다.
빌드가 완료되었습니다!
사용자 데이터 안전하게 저장하기
CLI 도구가 API 키나 비밀번호 같은 민감한 정보를 다룰 때는 안전한 저장 방법이 필요하다.
conf 라이브러리로 설정 관리하기
conf는 Node.js CLI 도구에서 설정을 안전하게 저장할 때 가장 많이 사용되는 라이브러리다:
npm install conf
기본 사용법
// ES 모듈 형식 (mjs)
import Conf from "conf";
import crypto from "crypto";
import os from "os";
// 사용자 환경에 기반한 고유 키 생성 (보안 강화)
function generateMachineKey() {
const hostname = os.hostname();
const username = os.userInfo().username;
const machineId = `${hostname}-${username}`;
return crypto
.createHash("sha256")
.update(machineId)
.digest("hex")
.substring(0, 32);
}
// 암호화된 설정 저장소 생성
const config = new Conf({
projectName: "my-cli-tool", // 설정 파일 이름 지정
schema: {
// 스키마로 타입 검증
apiKey: {
type: "string",
},
username: {
type: "string",
},
},
encryptionKey: generateMachineKey(), // 동적 암호화 키 사용
});
// 값 저장
config.set("apiKey", "secret-api-key");
config.set("username", "user123");
// 값 가져오기
const apiKey = config.get("apiKey");
console.log(apiKey); // 'secret-api-key'
// 전체 설정 보기
console.log(config.store); // { apiKey: 'secret-api-key', username: 'user123' }
// 값 삭제
config.delete("apiKey");
// 모든 값 삭제
config.clear();
주요 기능
- 자동 저장: 변경사항이 자동으로 디스크에 저장된다.
- 스키마 검증: 타입 스키마를 통해 저장 데이터의 유효성을 검증한다.
- 암호화: encryptionKey 옵션으로 민감한 정보를 안전하게 암호화한다.
- 마이그레이션: 버전 간 설정 마이그레이션을 지원한다.
저장 위치
conf는 설정 파일을 각 운영체제의 표준 위치에 저장한다:
- macOS: ~/Library/Preferences/my-cli-tool-nodejs
- Windows: %APPDATA%\my-cli-tool-nodejs\Config
- Linux: ~/.config/my-cli-tool-nodejs
API 키 관리 CLI 도구 예제
다음은 conf를 사용해 API 키를 안전하게 관리하는 CLI 도구의 예제다:
#!/usr/bin/env node
// api-manager.mjs
import { Command } from "commander";
import Conf from "conf";
import inquirer from "inquirer";
import crypto from "crypto";
import os from "os";
// 사용자 고유 암호화 키 생성
function generateSecureKey() {
const hostname = os.hostname();
const username = os.userInfo().username;
const machineId = `${hostname}-${username}`;
return crypto
.createHash("sha256")
.update(machineId)
.digest("hex")
.substring(0, 32);
}
// 프로그램 설정
const program = new Command();
program.name("api-keys").description("API 키 안전 관리 도구").version("1.0.0");
// 암호화된 설정 저장소 생성
const config = new Conf({
projectName: "api-keys-manager",
schema: {
apiKeys: {
type: "object",
default: {},
},
},
encryptionKey: generateSecureKey(),
});
// API 키 추가 명령어
program
.command("add <service>")
.description("새 서비스의 API 키 추가")
.action(async (service) => {
const answers = await inquirer.prompt([
{
type: "input",
name: "apiKey",
message: `${service}의 API 키를 입력하세요:`,
validate: (input) =>
input.length > 0 ? true : "API 키는 비워둘 수 없습니다",
},
]);
// 기존 API 키 가져오기
const apiKeys = config.get("apiKeys");
// 새 API 키 추가
apiKeys[service] = {
key: answers.apiKey,
createdAt: new Date().toISOString(),
};
// 암호화하여 저장
config.set("apiKeys", apiKeys);
console.log(`${service}의 API 키가 안전하게 저장되었습니다.`);
});
// API 키 목록 보기 명령어
program
.command("list")
.description("저장된 모든 서비스 목록 보기")
.action(() => {
const apiKeys = config.get("apiKeys");
const services = Object.keys(apiKeys);
if (services.length === 0) {
console.log("저장된 API 키가 없습니다.");
return;
}
console.log("저장된 서비스 목록:");
services.forEach((service) => {
// 보안을 위해 실제 키는 표시하지 않고 마스킹
const key = apiKeys[service].key;
const maskedKey =
key.substring(0, 4) +
"*".repeat(key.length - 8) +
key.substring(key.length - 4);
const createdDate = new Date(
apiKeys[service].createdAt
).toLocaleDateString();
console.log(`- ${service}: ${maskedKey} (생성일: ${createdDate})`);
});
});
// API 키 가져오기 명령어
program
.command("get <service>")
.description("특정 서비스의 API 키 가져오기")
.action((service) => {
const apiKeys = config.get("apiKeys");
if (!apiKeys[service]) {
console.error(`오류: '${service}'의 API 키를 찾을 수 없습니다.`);
process.exit(1);
}
console.log(apiKeys[service].key);
});
// API 키 삭제 명령어
program
.command("remove <service>")
.description("저장된 API 키 삭제")
.action((service) => {
const apiKeys = config.get("apiKeys");
if (!apiKeys[service]) {
console.error(`오류: '${service}'의 API 키를 찾을 수 없습니다.`);
process.exit(1);
}
delete apiKeys[service];
config.set("apiKeys", apiKeys);
console.log(`${service}의 API 키가 삭제되었습니다.`);
});
program.parse();
사용 예시
# API 키 추가
$ api-keys add github
? github의 API 키를 입력하세요: ghp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
github의 API 키가 안전하게 저장되었습니다.
# 저장된 서비스 목록 확인
$ api-keys list
저장된 서비스 목록:
- github: ghp_************************o5p6 (생성일: 2023-03-28)
# API 키 가져오기
$ api-keys get github
ghp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
# API 키 삭제
$ api-keys remove github
github의 API 키가 삭제되었습니다.
보안 강화 팁
CLI 도구에서 민감한 정보를 다룰 때 추가로 고려할 사항:
- 고유한 암호화 키: 위 예제처럼 사용자별 고유 키를 생성해 보안을 강화한다.
- 필요할 때만 정보 표시: list 명령어에서는 API 키를 마스킹하고, get 명령어로 필요할 때만 전체 값을 보여준다.
- 파일 권한 설정: 유닉스 계열 시스템에서는 chmod 600으로 설정 파일의 권한을 제한하는 것이 좋다.
- 비활성 타임아웃: 보안이 중요한 경우 일정 시간 후 자동 로그아웃 기능을 구현하는 것이 좋다.
대화형 프롬프트
사용자에게 정보를 입력받을 때는 inquirer와 같은 라이브러리가 유용하다:
// ES 모듈 형식 (mjs)
import inquirer from "inquirer";
async function promptCredentials() {
const answers = await inquirer.prompt([
{
type: "input",
name: "username",
message: "사용자 이름을 입력하세요:",
validate: (input) =>
input.length >= 3 ? true : "사용자 이름은 3자 이상이어야 합니다.",
},
{
type: "password",
name: "password",
message: "비밀번호를 입력하세요:",
mask: "*",
validate: (input) =>
input.length >= 8 ? true : "비밀번호는 8자 이상이어야 합니다.",
},
{
type: "list",
name: "role",
message: "역할을 선택하세요:",
choices: ["개발자", "디자이너", "관리자", "기타"],
},
{
type: "confirm",
name: "agreeTerms",
message: "이용약관에 동의하십니까?",
default: false,
},
]);
return answers;
}
// 사용 예시
async function setupUser() {
console.log("사용자 정보 설정을 시작합니다...");
const userInfo = await promptCredentials();
if (!userInfo.agreeTerms) {
console.log("이용약관에 동의해야 계속할 수 있습니다.");
process.exit(1);
}
console.log(`환영합니다, ${userInfo.username}님! (역할: ${userInfo.role})`);
// 비밀번호 같은 민감한 정보는 로그에 출력하지 않는다
}
// setupUser();
inquirer 주요 프롬프트 타입
inquirer는 다양한 형태의 사용자 입력을 처리할 수 있다:
- input: 일반 텍스트 입력
- password: 마스킹된 비밀번호 입력
- list: 단일 선택 목록
- checkbox: 다중 선택 목록
- confirm: 예/아니오 질문
- editor: 여러 줄 텍스트 편집기
- number: 숫자 입력
실행 결과 예시:
? 사용자 이름을 입력하세요: janekim
? 비밀번호를 입력하세요: ********
? 역할을 선택하세요: 개발자
? 이용약관에 동의하십니까? Yes
환영합니다, janekim님! (역할: 개발자)
CLI 배포하기
CLI 도구를 다른 사람들이 사용할 수 있도록 배포하는 방법을 알아보자.
package.json 준비
배포 전 package.json에 필요한 설정을 확인한다:
{
"name": "my-cli-tool",
"version": "1.0.0",
"description": "설명",
"bin": {
"my-tool": "./bin/index.js"
},
"files": ["bin", "lib"],
"keywords": ["cli", "tool"],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"commander": "^9.0.0"
},
"engines": {
"node": ">=12.0.0"
}
}
중요 필드:
- bin: CLI 진입점 정의
- files: npm 패키지에 포함할 파일/디렉토리 (나머지는 무시됨)
- engines: 지원하는 Node.js 버전 명시
NPM에 배포하기
계정이 없다면 먼저 npmjs.com에서 가입한다.
# NPM에 로그인
npm login
# 패키지 배포
npm publish
전역 설치 테스트
배포 후 설치 테스트:
# 전역 설치
npm install -g my-cli-tool
# 실행 테스트
my-tool --help
실전 예제: 간단한 파일 생성 CLI 도구
이제 배운 내용을 종합해 간단한 CLI 도구를 만들어보자.
#!/usr/bin/env node
// file-generator.mjs
import fs from "fs";
import path from "path";
import { Command } from "commander";
import inquirer from "inquirer";
// 프로그램 메타데이터 설정
const program = new Command();
program
.name("create-file")
.description("간단한 파일 생성 도구")
.version("1.0.0");
// 명령어 정의
program
.command("template <type>")
.description("템플릿 파일 생성 (html, css, js)")
.option("-n, --name <filename>", "파일 이름 (기본: index)")
.action(async (type, options) => {
// 지원하는 템플릿 확인
const validTypes = ["html", "css", "js"];
if (!validTypes.includes(type)) {
console.error(
`오류: '${type}'은 지원하지 않는 타입입니다. ${validTypes.join(
", "
)} 중 하나를 사용하세요.`
);
process.exit(1);
}
// 파일 이름 결정
let filename = options.name || "index";
filename = `${filename}.${type}`;
// 파일이 이미 존재하는지 확인
if (fs.existsSync(filename)) {
const { overwrite } = await inquirer.prompt([
{
type: "confirm",
name: "overwrite",
message: `'${filename}'이 이미 존재합니다. 덮어쓰시겠습니까?`,
default: false,
},
]);
if (!overwrite) {
console.log("작업이 취소되었습니다.");
process.exit(0);
}
}
// 템플릿 내용 생성
let content = "";
switch (type) {
case "html":
content = `<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>새 문서</title>
</head>
<body>
<h1>안녕하세요!</h1>
</body>
</html>`;
break;
case "css":
content = `/* ${filename} */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
padding: 20px;
line-height: 1.6;
}`;
break;
case "js":
content = `// ${filename}
function init() {
console.log('스크립트가 로드되었습니다.');
}
document.addEventListener('DOMContentLoaded', init);`;
break;
}
// 파일 작성
fs.writeFileSync(filename, content);
console.log(`'${filename}' 파일이 성공적으로 생성되었습니다.`);
});
// 프로그램 실행
program.parse();
사용 예시와 결과
- HTML 파일 생성:
$ create-file template html -n homepage
'homepage.html' 파일이 성공적으로 생성되었습니다.
- 이미 존재하는 파일 덮어쓰기:
$ create-file template css -n style
'style.css'이 이미 존재합니다. 덮어쓰시겠습니까? (y/n) y
'style.css' 파일이 성공적으로 생성되었습니다.
- 작업 취소:
$ create-file template js -n app
'app.js'이 이미 존재합니다. 덮어쓰시겠습니까? (y/n) n
작업이 취소되었습니다.
- 도움말 보기:
$ create-file --help
Usage: create-file [options] [command]
간단한 파일 생성 도구
Options:
-V, --version 버전 출력
-h, --help 도움말 표시
Commands:
template [options] <type> 템플릿 파일 생성 (html, css, js)
help [command] display help for command
배포 과정
이 CLI 도구를 배포하려면 package.json을 다음과 같이 설정한다:
{
"name": "create-file",
"version": "1.0.0",
"description": "간단한 파일 생성 CLI 도구",
"type": "module",
"bin": {
"create-file": "./file-generator.mjs"
},
"files": ["file-generator.mjs"],
"dependencies": {
"commander": "^9.0.0",
"inquirer": "^8.2.0"
}
}
실행 파일에 실행 권한을 부여하고 npm에 배포한다:
chmod +x file-generator.mjs
npm publish
그러면 사용자들이 다음 명령어로 설치할 수 있다:
npm install -g create-file
마치며
이제 Node.js로 CLI 도구를 개발하는 기본적인 방법을 알게 되었다. 명령줄 도구는 개발 워크플로우를 자동화하고 생산성을 높이는 강력한 도구다. 웹 개발자로서 반복적인 작업을 자동화하는 CLI 도구를 만들어 사용한다면 업무 효율성을 크게 높일 수 있을 것이다.
더 배워볼 만한 주제:
- chalk: 터미널 출력에 색상 입히기
- ora: 로딩 스피너 구현
- boxen: 터미널에 박스 UI 만들기
- update-notifier: CLI 도구 업데이트 알림
여러분만의 CLI 도구를 만들어 NPM에 공유해보자. 다른 개발자들에게도 도움이 될 수 있다!