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

배경

팀에서 사용하는 배포 스크립트가 있었다. Node.js로 작성되어 있고, 파일 복사/압축/업로드를 수행한다. 실행 시간이 긴 건 아니었지만, Rust를 실무급으로 학습해보고 싶어서 재작성을 시도했다.

구현 과정

기본 구조

use std::fs;
use std::path::Path;
use clap::Parser;

#[derive(Parser)]
struct Args {
    #[arg(short, long)]
    source: String,
    #[arg(short, long)]
    target: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();
    copy_directory(&args.source, &args.target)?;
    Ok(())
}

clap 라이브러리로 CLI 인자를 파싱했다. derive 매크로로 보일러플레이트를 줄일 수 있었다.

소유권 시스템

가장 고생한 부분이다. String과 &str의 차이, 값의 이동과 차용을 이해하는 데 시간이 걸렸다.

fn process_file(path: &Path) -> Result<String, std::io::Error> {
    let content = fs::read_to_string(path)?;
    // content를 반환하면 소유권이 이동
    Ok(content.trim().to_string())
}

처음엔 컴파일 에러와 싸우느라 답답했지만, 컴파일러 메시지가 친절해서 점차 패턴이 익숙해졌다.

에러 처리

Node.js의 try-catch와 달리 Result 타입으로 명시적으로 처리한다.

match fs::remove_file(path) {
    Ok(_) => println!("삭제 완료"),
    Err(e) => eprintln!("삭제 실패: {}", e),
}

? 연산자로 에러를 상위로 전파할 수 있어서 편했다.

결과

Node.js 버전 대비 실행 시간이 30% 정도 줄었다. 바이너리 하나로 배포할 수 있다는 점도 장점이었다.

하지만 개발 시간은 3배 이상 걸렸고, 팀원들이 유지보수하기 어렵다는 피드백을 받았다. 결국 프로덕션엔 도입하지 않았다.

배운 점

  • 컴파일 타임에 많은 버그를 잡을 수 있다
  • 러닝 커브가 가팔라서 팀 전체 도입은 신중해야 한다
  • 성능이 중요한 특정 모듈에 부분 도입하는 게 현실적이다

당분간은 학습용으로만 사용하고, WebAssembly나 시스템 레벨 도구에서 활용 기회를 찾아볼 예정이다.