핵심 요약
좋은 CLI 도구 하나가 라이브러리·SaaS보다 영향력 클 수 있음 (rg, fzf, gh, jq 등 모두 개인 시작). 2026년 시점 만들기·배포 가장 좋은 3개 — Rust, Go, Bun.
- Rust + clap: 가장 빠름, 단일 바이너리, 학습 부담
- Go + cobra: 가장 단순, 가장 빠른 출시
- Bun + commander: TypeScript 친화, npm 배포
1. 같은 도구 구현 — 파일 라인 수 카운터
Rust + clap
// src/main.rs
use clap::Parser;
use std::path::PathBuf;
use std::fs;
use walkdir::WalkDir;
#[derive(Parser)]
#[command(name = "lc", about = "Line counter")]
struct Args {
/// Path to count
path: PathBuf,
/// File extension filter
#[arg(short, long)]
ext: Option<String>,
}
fn main() {
let args = Args::parse();
let mut total = 0;
for entry in WalkDir::new(&args.path) {
let entry = entry.unwrap();
if !entry.file_type().is_file() { continue; }
if let Some(ext) = &args.ext {
if entry.path().extension().map(|e| e.to_str().unwrap()) != Some(ext.as_str()) {
continue;
}
}
let content = fs::read_to_string(entry.path()).unwrap_or_default();
total += content.lines().count();
}
println!("Total lines: {}", total);
}
Go + cobra
// main.go
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
)
var ext string
var rootCmd = &cobra.Command{
Use: "lc [path]",
Short: "Line counter",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
total := 0
filepath.Walk(args[0], func(path string, info os.FileInfo, err error) error {
if info.IsDir() { return nil }
if ext != "" && !strings.HasSuffix(path, "."+ext) { return nil }
data, _ := os.ReadFile(path)
total += strings.Count(string(data), "\n") + 1
return nil
})
fmt.Printf("Total lines: %d\n", total)
},
}
func init() {
rootCmd.Flags().StringVarP(&ext, "ext", "e", "", "File extension filter")
}
func main() {
rootCmd.Execute()
}
Bun + commander
// lc.ts
import { Command } from "commander"
import { readdir, readFile } from "fs/promises"
import { join } from "path"
const program = new Command()
program
.name("lc")
.description("Line counter")
.argument("<path>", "Path to count")
.option("-e, --ext <ext>", "File extension filter")
.action(async (path, options) => {
let total = 0
async function walk(dir: string) {
const entries = await readdir(dir, { withFileTypes: true })
for (const e of entries) {
const full = join(dir, e.name)
if (e.isDirectory()) await walk(full)
else if (!options.ext || full.endsWith(`.${options.ext}`)) {
const content = await readFile(full, "utf8")
total += content.split("\n").length
}
}
}
await walk(path)
console.log(`Total lines: ${total}`)
})
program.parse()
2. 빌드·배포
Rust
# cargo build --release
./target/release/lc
# cross-compile (모든 플랫폼)
cargo install cross
cross build --target x86_64-unknown-linux-gnu --release
cross build --target aarch64-apple-darwin --release
cross build --target x86_64-pc-windows-gnu --release
# 배포 — cargo publish (crates.io)
cargo publish
Go
go build -o lc
# cross-compile
GOOS=darwin GOARCH=arm64 go build -o lc-darwin-arm64
GOOS=linux GOARCH=amd64 go build -o lc-linux-amd64
GOOS=windows GOARCH=amd64 go build -o lc-windows.exe
# 배포 — GitHub Releases + Homebrew
goreleaser release
Bun
# 단일 바이너리 컴파일
bun build --compile --minify --sourcemap lc.ts --outfile lc
# 모든 플랫폼
bun build --compile --target=bun-linux-x64 lc.ts --outfile lc-linux
bun build --compile --target=bun-darwin-arm64 lc.ts --outfile lc-darwin-arm64
bun build --compile --target=bun-windows-x64 lc.ts --outfile lc-windows.exe
# 또는 npm 배포
npm publish
3. Homebrew 배포
# Formula 생성 (homebrew-tap repo)
class Lc < Formula
desc "Line counter"
homepage "https://github.com/myname/lc"
url "https://github.com/myname/lc/releases/download/v1.0.0/lc-#{Hardware::CPU.arch}-darwin.tar.gz"
sha256 "..."
version "1.0.0"
def install
bin.install "lc"
end
test do
system "#{bin}/lc", "--version"
end
end
# 사용자가
brew tap myname/tap
brew install lc
4. GoReleaser — Go 자동화
# .goreleaser.yml
builds:
- main: ./main.go
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
brews:
- tap:
owner: myname
name: homebrew-tap
homepage: https://github.com/myname/lc
description: Line counter
# 태그 push만으로 자동 빌드·릴리스·Homebrew 업데이트
git tag v1.0.0
git push --tags
5. 비교 — DX
| 항목 | Rust | Go | Bun |
|---|
| 학습 곡선 | 가파름 | 완만 | 가장 낮음 |
| 단일 바이너리 | O | O | O |
| 실행 속도 | ★★★★★ | ★★★★ | ★★★ |
| 크로스 컴파일 | cross 사용 | 네이티브 | 네이티브 |
| 크기 | ~3MB | ~10MB | ~75MB (Bun runtime 포함) |
| cold start | 2ms | 5ms | 50ms |
| npm 배포 | X | X | O |
6. 의사결정 매트릭스
| 워크로드 | 1순위 |
|---|
| 최고 성능 필요 | Rust |
| 빠른 출시·생태계 | Go |
| TypeScript 팀 | Bun |
| npm으로 배포 | Bun |
| cloud-native 도구 (k8s, terraform) | Go |
| system tool (rg, bat 같은) | Rust |
7. UX 베스트 프랙티스
- 도움말 (--help) 풍부하게
- colored output (단 isatty 체크)
- JSON 출력 옵션 (스크립트 친화)
- config file 지원 (~/.config/myapp/)
- 버전 표시 (--version)
- shell completion 자동 생성
- error 메시지는 actionable
8. shell completion
clap
// 자동 생성
use clap_complete::{generate, Shell};
#[derive(Parser)]
struct Args {
#[arg(long, value_enum)]
shell: Option<Shell>,
}
// lc --shell bash > ~/.local/share/bash-completion/lc.bash
cobra
// 자동
rootCmd.Execute()
// lc completion bash > /etc/bash_completion.d/lc
9. 테스트
# Rust
cargo test
# Go
go test ./...
# Bun
bun test
# 통합 테스트 — assert_cmd (Rust)
use assert_cmd::Command;
let mut cmd = Command::cargo_bin("lc").unwrap();
cmd.arg(".").assert().success().stdout(predicate::str::contains("Total"));
10. 인기 CLI 도구 학습
- ripgrep (rg) — Rust, 빠른 grep
- fzf — Go, fuzzy finder
- gh — Go, GitHub CLI
- bat — Rust, cat 대체
- delta — Rust, git diff 시각화
- lazygit — Go, git TUI
11. 첫 CLI 도구 출시 체크리스트
- [ ] README 명확 (사용 예 5개)
- [ ] --help 풍부
- [ ] --version 표시
- [ ] cross-platform 빌드 (Linux·macOS·Windows)
- [ ] GitHub Releases 자동화
- [ ] Homebrew tap (또는 npm)
- [ ] shell completion
- [ ] 통합 테스트
- [ ] semver 준수
- [ ] LICENSE (MIT 권장)
자주 묻는 질문
어느 언어로 시작?5분 안에 일단 만들고 싶다 → Bun. 운영급 → Rust 또는 Go.
npm 배포 vs Homebrew?둘 다 권장. Homebrew는 macOS·Linux 일반 사용자, npm은 Node 생태계.
Bun 75MB 너무 크다?맞다. 가벼움이 critical하면 Rust·Go. 단 75MB도 모바일 게임 1개 수준.
댓글 0