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나 시스템 레벨 도구에서 활용 기회를 찾아볼 예정이다.