러스트(Rust) 실전 프로젝트 예제 따라하기 시리즈 - 1편: 간단한 CLI 유틸리티 만들기

이번 시리즈에서는 지금까지 러스트 언어의 기초와 다양한 기능을 익힌 독자분들을 위해, 직접 손을 움직이며 실전 프로젝트를 구현해보는 시간을 가질 예정입니다. 이 시리즈는 “입문 시리즈” 이후 단계로, 이제는 하나하나 따라 해보면서 러스트 프로젝트를 실제로 구축하고 발전시켜 나가는 과정에 초점을 맞춥니다.

 

첫 번째 예제로는 러스트(Rust)를 활용해 간단한 CLI(Command Line Interface) 유틸리티를 만들어봅시다. 사용자로부터 검색어와 디렉토리를 입력받아 해당 디렉토리(및 하위 디렉토리)에서 검색어를 포함한 파일을 찾아 출력하는 간단한 툴입니다. 이 과정을 통해 다음과 같은 실전 감각을 익힐 수 있습니다.

  • Cargo를 통한 프로젝트 초기화 및 빌드 과정 이해
  • CLI 인자 파싱 및 명령행 툴 구조화 방법
  • 파일 시스템 순회 및 파일 I/O 처리
  • 에러 처리, 테스트, 코드 품질 관리(rustfmt, clippy) 등 종합적인 개발 흐름 경험

C++를 비롯한 다른 언어로 CLI 툴을 만들어본 경험이 있다면, 러스트를 사용했을 때 어떤 부분이 더 편리하고 안전한지 비교해보는 재미도 있을 것입니다. 그럼 지금부터 프로젝트를 진행해봅시다!

프로젝트 개요

우리가 만들 CLI 유틸리티는 문자열 검색기입니다. 기능은 다음과 같습니다.

  • 명령행 인자로 검색할 문자열(query)과 검색할 디렉토리(기본값: 현재 디렉토리)를 받는다.
  • 해당 디렉토리를 재귀적으로 순회하며 모든 파일을 검사한다.
  • 각 파일을 읽어, 검색어를 포함하고 있으면 해당 파일 경로를 콘솔에 출력한다.
  • 에러 발생 시 Result 타입과 ? 연산자를 사용해 명시적으로 처리하고, 친절한 에러 메시지를 제공한다.

이 예제를 통해 러스트 프로젝트의 기본기(프로젝트 초기화, 의존성 관리, 에러 처리, 테스트, 배포)와 함께 실전적인 코드 작성 패턴을 익혀보겠습니다.

프로젝트 초기화

cargo new file_searcher
cd file_searcher

Cargo.toml 파일에 CLI 인자 파싱을 위해 clap 크레이트를 추가하고, 에러 처리를 더 편리하게 하기 위해 anyhow를 사용하겠습니다.

[package]
name = "file_searcher"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }
anyhow = "1.0"

명령행 인자 파싱

// src/main.rs
use clap::Parser;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// 검색할 문자열
    query: String,
    /// 검색할 디렉토리 (기본값: 현재 디렉토리)
    #[arg(default_value = ".")]
    directory: String,
}

fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    println!("검색할 문자열: {}, 디렉토리: {}", args.query, args.directory);
    Ok(())
}

이제 cargo run -- "rust" "." 명령으로 인자를 받아보세요. 인자 파싱이 잘 되는지 확인할 수 있습니다.

디렉토리 순회 및 파일 내용 검사

use std::fs;
use std::path::Path;

fn search_files<P: AsRef<Path>>(directory: P, query: &str) -> anyhow::Result<Vec<String>> {
    let mut results = Vec::new();
    for entry in fs::read_dir(directory)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            let sub_results = search_files(&path, query)?;
            results.extend(sub_results);
        } else {
            if let Some(matched_path) = check_file(&path, query)? {
                results.push(matched_path);
            }
        }
    }
    Ok(results)
}

fn check_file<P: AsRef<Path>>(file_path: P, query: &str) -> anyhow::Result<Option<String>> {
    let content = fs::read_to_string(&file_path)?;
    if content.contains(query) {
        Ok(Some(file_path.as_ref().display().to_string()))
    } else {
        Ok(None)
    }
}

메인 함수에서 이 로직을 결합해봅시다.

fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    let results = search_files(&args.directory, &args.query)?;

    if results.is_empty() {
        println!("'{}'을(를) 포함하는 파일을 찾지 못했습니다.", args.query);
    } else {
        println!("'{}'을(를) 포함하는 파일:", args.query);
        for file_path in results {
            println!("{}", file_path);
        }
    }

    Ok(())
}

테스트 추가하기

tests 디렉토리를 만들어 일부 함수를 테스트해봅시다.

mkdir tests
touch tests/integration_test.rs

Cargo.toml에 테스트 용도로 tempfile 크레이트를 추가합니다.

[dev-dependencies]
tempfile = "3.5"

tests/integration_test.rs:

use file_searcher::check_file;
use std::fs;
use std::io::Write;

#[test]
fn test_check_file() {
    let mut temp_file = tempfile::NamedTempFile::new().unwrap();
    write!(temp_file, "Hello Rust!").unwrap();
    let path = temp_file.path();
    let result = check_file(path, "Rust").unwrap();
    assert!(result.is_some());
}

cargo test로 테스트를 돌려보면, 함수의 동작을 간단히 검증할 수 있습니다.

코드 품질 관리 및 배포

cargo fmt, cargo clippy를 통해 코드 스타일과 품질을 개선하고, 최종적으로 cargo install --path . 명령으로 이 툴을 로컬에 설치할 수 있습니다.

다음 단계로 정규식 지원, 특정 확장자 필터링, 에러 로그 개선 등 다양한 확장 기능을 고려해볼 수 있습니다.

결론

이번 글에서는 실전 프로젝트 예제 따라하기 시리즈의 첫 번째 예제로, 러스트를 활용한 간단한 CLI 유틸리티를 만들어보았습니다. 지금까지 배운 러스트 기초와 생태계 활용을 종합적으로 경험하는 좋은 사례가 되었을 것입니다.

 

앞으로 계속될 시리즈에서는 더 복잡한 요구사항을 가진 애플리케이션이나 네트워크 서버, WebAssembly 활용 등 러스트가 빛을 발하는 분야를 탐구하며 실제로 프로덕션급 코드를 작성하는 과정을 다뤄볼 예정입니다. 여기서 다룬 CLI 프로젝트를 기반으로 더욱 발전된 애플리케이션 구현에 도전해보시기 바랍니다.

유용한 링크와 리소스

반응형