Rust로 CLI 도구 만들어보며 배운 것들

배경

사내에서 사용하는 배포 자동화 스크립트가 Node.js로 작성되어 있었다. 문제는 새 팀원이 올 때마다 Node 버전 맞추고 의존성 설치하는 과정이 번거로웠다는 점. 바이너리 하나로 배포할 수 있다면 편할 것 같아서 Rust로 재작성을 시도했다.

기본 구조

clap 크레이트를 사용해 CLI 인자를 파싱했다. derive 매크로를 쓰니 보일러플레이트가 많이 줄었다.

use clap::Parser;

#[derive(Parser)]
#[clap(name = "deployer")]
struct Args {
    #[clap(short, long)]
    env: String,
    
    #[clap(short, long)]
    branch: Option<String>,
}

fn main() {
    let args = Args::parse();
    deploy(&args.env, args.branch.as_deref());
}

파일 처리

YAML 설정 파일을 읽어야 했는데, serde_yamlserde의 조합이 편했다. 하지만 에러 핸들링에서 Result 타입을 제대로 다루는 데 시간이 걸렸다.

use serde::Deserialize;
use std::fs;

#[derive(Deserialize)]
struct Config {
    server: String,
    path: String,
}

fn read_config(env: &str) -> Result<Config, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(format!("config/{}.yaml", env))?;
    let config: Config = serde_yaml::from_str(&content)?;
    Ok(config)
}

배운 점

소유권과 borrowing: 문자열을 여러 함수에 넘기면서 &strString의 차이를 체감했다. 처음엔 컴파일 에러가 답답했지만, 결국 메모리 안전성을 컴파일 타임에 보장받는다는 게 강점이었다.

에러 처리: ? 연산자가 편하긴 한데, 각 함수의 반환 타입을 명확히 해야 했다. anyhow 크레이트를 쓰면 더 간단해진다는 걸 나중에 알았다.

빌드 결과: 단일 바이너리로 빌드되니 배포가 정말 간단해졌다. 실행 속도도 Node 버전보다 체감상 빠르다.

결론

Rust가 가파른 학습 곡선을 가진 건 사실이지만, CLI 도구처럼 명확한 입출력이 있는 프로그램을 만들며 배우기엔 좋았다. 프로덕션에 쓰기엔 아직 이르지만, 사내 도구 정도는 충분히 실용적이다.