본문 바로가기
Etc2026년 4월 28일14분 읽기

CLI 도구 만들기 실전 — Rust clap·Go cobra·Bun 비교 + 배포 전략

YS
김영삼
조회 3
CLI 도구 만들기 실전 — Rust clap·Go cobra·Bun 비교 + 배포 전략

핵심 요약

좋은 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

항목RustGoBun
학습 곡선가파름완만가장 낮음
단일 바이너리OOO
실행 속도★★★★★★★★★★★★
크로스 컴파일cross 사용네이티브네이티브
크기~3MB~10MB~75MB (Bun runtime 포함)
cold start2ms5ms50ms
npm 배포XXO

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

아직 댓글이 없습니다.
Ctrl+Enter로 등록