Use Cases (업데이트: 2026. 6. 2.)

Claude Code로 Rust 개발하기: Cargo, 소유권, 테스트, CLI 리팩터링

Claude Code로 Rust를 시작하는 실전 가이드. Cargo, 소유권, 테스트, fmt, clippy, 오류 처리, CLI 예제를 다룹니다.

Claude Code로 Rust 개발하기: Cargo, 소유권, 테스트, CLI 리팩터링

검증 가능한 작은 Rust 작업부터 시작하기

Rust는 소유권, 빌림, 라이프타임, Result 기반 오류 처리 때문에 처음에는 어렵게 느껴집니다. 하지만 이 엄격함이 Rust의 장점입니다. Claude Code를 사용할 때도 “AI가 알아서 다 작성”하게 두기보다, 컴파일러 오류를 함께 읽고, 작은 범위만 편집하고, 테스트와 lint로 확인하는 흐름이 좋습니다.

이 글에서는 tasknote라는 작은 CLI를 만듭니다. tasks.txt[ ] task[x] task를 읽고 완료/미완료 개수를 출력하거나, 미완료 항목만 보여줍니다. 기준 문서는 공식 Rust Book의 소유권, Cargo Book의 프로젝트 생성, cargo test, rustfmt, Clippy, Claude Code overview입니다.

Claude Code 자체가 처음이라면 입문 가이드부터 보세요. 팀에서 반복적으로 쓸 규칙은 CLAUDE.md best practices에 정리하고, 실행 권한은 permissions guide로 좁혀 두는 편이 안전합니다.

flowchart LR
  Prompt["목표와 제약 전달"]
  Cargo["Cargo 프로젝트 생성"]
  Compiler["소유권 오류 읽기"]
  Tests["테스트로 동작 고정"]
  Quality["fmt와 clippy 실행"]
  Refactor["작은 차이로 리팩터링"]

  Prompt --> Cargo --> Compiler --> Tests --> Quality --> Refactor

Cargo 프로젝트 만들기

Cargo는 Rust의 표준 도구입니다. 패키지 생성, 빌드, 실행, 테스트, 의존성 관리를 맡습니다. Claude Code에 코드를 쓰게 하기 전에 어떤 명령으로 검증할지 먼저 정하면 작업의 끝이 분명해집니다.

cargo new tasknote --bin
cd tasknote
cargo run

생성된 프로젝트에는 Cargo.tomlsrc/main.rs가 있습니다. 최신 프로젝트에서는 edition = "2024"가 쓰일 수 있으므로, 오래된 글의 설정을 복사하지 말고 실제 Cargo.toml을 읽게 하세요. 이 예제에서는 CLI 인자 처리를 위한 clap, 오류에 문맥을 붙이는 anyhow만 사용합니다.

[package]
name = "tasknote"
version = "0.1.0"
edition = "2024"

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

처음부터 구현을 맡기지 말고 설계부터 요청합니다.

이 Cargo 프로젝트에서 `tasknote`라는 작은 CLI를 만듭니다.
`tasks.txt`의 `[ ] task`와 `[x] task` 라인을 읽습니다.
먼저 `Cargo.toml`, `src/lib.rs`, `src/main.rs` 설계만 제안하세요.
아직 파일을 편집하지 마세요. 소유권과 오류 처리 방침도 설명하세요.

Rust에서는 데이터가 어디서 소유되고 어디서 빌려지는지가 코드의 단순함을 결정합니다. 이 설계 단계를 건너뛰면 나중에 clone()이 불필요하게 늘어납니다.

소유권과 빌림을 질문하는 법

소유권은 값의 책임자가 누구인지 정하는 규칙입니다. 빌림은 값을 가져가지 않고 잠시 읽거나 수정하는 것입니다. 라이프타임은 참조가 유효한 범위입니다. 처음에는 용어보다 “누가 데이터를 갖고, 누가 읽기만 하는가”로 생각하면 쉽습니다.

이 CLI에서는 파일 내용을 String으로 읽고, parse_tasks&str로 그 내용을 빌려 읽습니다. 결과는 Vec<Task>가 소유합니다. summarize는 목록을 읽기만 하므로 &[Task]를 받습니다.

`parse_tasks(input: &str) -> Vec<Task>`와 `summarize(tasks: &[Task]) -> String`의 소유권을 설명하세요.
이 CLI 예제로만 설명하고, `String`, `&str`, `Vec<Task>`, `&[Task]`의 차이를 초보자용으로 풀어 주세요.
불필요한 `clone()`을 추가하지 않는 방향으로 설명하세요.

초보자가 자주 하는 실수는 borrow checker 오류를 모두 clone()으로 덮는 것입니다. clone()이 필요한 경우도 있지만, 이유가 있어야 합니다. 읽기만 하는 함수라면 빌림이 보통 더 좋은 신호입니다.

그대로 붙여 넣어 실행하는 CLI 예제

핵심 로직은 src/lib.rs에 둡니다. 그러면 CLI를 실행하지 않고도 단위 테스트가 가능합니다.

// src/lib.rs
use anyhow::{Context, Result};
use std::{fs, path::Path};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Task {
    pub title: String,
    pub done: bool,
}

pub fn parse_tasks(input: &str) -> Vec<Task> {
    input.lines().filter_map(parse_task_line).collect()
}

fn parse_task_line(line: &str) -> Option<Task> {
    let trimmed = line.trim();

    if let Some(title) = trimmed
        .strip_prefix("[x] ")
        .or_else(|| trimmed.strip_prefix("[X] "))
    {
        return Some(Task {
            title: title.trim().to_string(),
            done: true,
        });
    }

    if let Some(title) = trimmed.strip_prefix("[ ] ") {
        return Some(Task {
            title: title.trim().to_string(),
            done: false,
        });
    }

    None
}

pub fn summarize(tasks: &[Task]) -> String {
    let total = tasks.len();
    let done = tasks.iter().filter(|task| task.done).count();
    let open = total.saturating_sub(done);

    format!("{total} tasks: {done} done, {open} open")
}

pub fn read_tasks(path: impl AsRef<Path>) -> Result<Vec<Task>> {
    let path = path.as_ref();
    let content = fs::read_to_string(path)
        .with_context(|| format!("failed to read {}", path.display()))?;

    Ok(parse_tasks(&content))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_markdown_style_tasks() {
        let tasks = parse_tasks("[ ] write parser\n[x] add tests\n[X] run clippy\n");

        assert_eq!(
            tasks,
            vec![
                Task {
                    title: "write parser".to_string(),
                    done: false,
                },
                Task {
                    title: "add tests".to_string(),
                    done: true,
                },
                Task {
                    title: "run clippy".to_string(),
                    done: true,
                },
            ]
        );
    }

    #[test]
    fn ignores_unrecognized_lines() {
        let tasks = parse_tasks("# Sprint notes\n- plain bullet\n[ ] keep this\n");

        assert_eq!(tasks.len(), 1);
        assert_eq!(tasks[0].title, "keep this");
    }

    #[test]
    fn summarizes_counts() {
        let tasks = parse_tasks("[ ] one\n[x] two\n[ ] three\n");

        assert_eq!(summarize(&tasks), "3 tasks: 1 done, 2 open");
    }
}

src/main.rs는 인자 처리와 출력만 담당합니다.

// src/main.rs
use anyhow::Result;
use clap::Parser;
use std::path::PathBuf;
use tasknote::{read_tasks, summarize};

#[derive(Parser, Debug)]
#[command(name = "tasknote", about = "Summarize simple task files")]
struct Cli {
    #[arg(short, long, default_value = "tasks.txt")]
    file: PathBuf,

    #[arg(long)]
    only_open: bool,
}

fn main() -> Result<()> {
    let args = Cli::parse();
    let tasks = read_tasks(&args.file)?;

    if args.only_open {
        for task in tasks.iter().filter(|task| !task.done) {
            println!("- {}", task.title);
        }
    } else {
        println!("{}", summarize(&tasks));
    }

    Ok(())
}

예시 tasks.txt입니다.

[ ] write parser
[x] add unit tests
[ ] run clippy

검증은 다음 순서로 충분합니다.

cargo fmt
cargo test
cargo clippy --all-targets -- -D warnings
cargo run -- --file tasks.txt
cargo run -- --file tasks.txt --only-open

mainResult<()>를 반환하므로 파일이 없을 때 unwrap()으로 갑자기 종료하지 않고, 어떤 파일을 읽지 못했는지 문맥을 보여줄 수 있습니다. CLI의 입력 파일 누락은 회복 가능한 오류이므로 사용자가 고칠 수 있는 메시지를 주는 편이 좋습니다.

테스트, fmt, Clippy를 작업 완료 조건에 넣기

Claude Code가 코드를 만들었다고 끝내면 안 됩니다. 아래처럼 검증 명령을 작업 조건에 포함하세요.

`src/lib.rs`와 `src/main.rs`를 구현하세요.
편집 후 `cargo fmt`, `cargo test`, `cargo clippy --all-targets -- -D warnings`를 실행하세요.
실패하면 먼저 오류를 요약하고, 가능한 작은 차이로 수정하세요.
테스트 밖에서는 `unwrap()`을 사용하지 마세요.

Claude Code는 공식 문서에서 코드베이스를 읽고, 파일을 편집하고, 명령을 실행하는 agentic coding tool로 설명됩니다. 그래도 최종 책임은 개발자에게 있습니다. git diff를 직접 보고, 작업 범위 밖 파일이 바뀌지 않았는지 확인하세요.

실제 유스케이스

첫 번째는 기존 Rust CLI에 작은 기능을 추가하는 경우입니다. 예를 들어 --json 출력을 추가한다면 summarize의 동작은 유지하고 JSON 포맷 함수만 새로 만들라고 요청합니다.

두 번째는 소유권 오류 학습입니다. cannot move out ofborrowed value does not live long enough가 나오면 오류 전체와 관련 함수를 붙여 넣고, 어떤 값이 소유되고 어떤 참조가 너무 오래 사는지 설명하게 합니다.

세 번째는 테스트 우선 버그 수정입니다. 빈 줄, [X], 잘못된 라인을 먼저 테스트로 고정한 뒤 파서를 고칩니다. Rust 타입 시스템은 강하지만 제품 규칙은 테스트가 필요합니다.

네 번째는 안전한 리팩터링입니다. main.rs가 커지면 파싱, 파일 I/O, 표시 로직을 나누되 공개 함수 시그니처를 바꾸지 말라고 요청합니다.

흔한 함정

소유권 오류를 모두 clone()으로 없애지 마세요. 새 clone()이 생기면 “정말 소유해야 하는가, 빌림으로 충분한가”를 물어야 합니다.

프로덕션 CLI 경로에 unwrap()을 남기지 마세요. 파일, 설정, 사용자 입력은 실패할 수 있습니다. Result와 문맥 있는 오류가 낫습니다.

테스트 없이 리팩터링하지 마세요. Claude Code는 큰 차이를 빠르게 만들 수 있지만, 큰 차이는 검토하기 어렵습니다.

공유 workspace 전체에 무작정 cargo fmt를 걸지 마세요. 패키지, 파일 범위, 허용 명령을 명확히 하세요.

안전한 리팩터링 프롬프트

`tasknote` 파서를 안전하게 리팩터링하세요.

조건:
- `[ ] task`와 `[x] task`의 의미를 바꾸지 않기
- `parse_tasks`, `summarize`, `read_tasks`의 공개 시그니처를 바꾸지 않기
- `src/lib.rs`와 해당 테스트만 수정하기
- 불필요한 `clone()`을 추가하지 않기
- 먼저 3줄 계획을 제시하고 승인 후 편집하기

편집 후:
- `cargo fmt`
- `cargo test`
- `cargo clippy --all-targets -- -D warnings`
- 변경 요약과 소유권 판단 보고

이런 프롬프트는 Claude Code에 작업 경계와 검증 절차를 제공합니다. Rust에서는 컴파일러, 테스트, linter가 모두 품질을 확인해 주므로 이 방식과 잘 맞습니다.

수익화 동선과 다음 단계

개인 연습이라면 JSON 출력, CSV 내보내기, 디렉터리 스캔, serde 기반 포맷으로 확장해 보세요. 팀 도입이라면 Rust edition, 필수 명령, unwrap() 정책, 허용 crate, 리뷰 체크리스트를 CLAUDE.md에 남겨야 합니다.

ClaudeCodeLab은 Claude Code 프롬프트, 설정 가이드, CLAUDE.md 템플릿, 팀 교육을 제공합니다. 명령을 빠르게 확인하려면 무료 치트시트를 보고, 템플릿은 제품 페이지에서 확인하세요. 실제 저장소에 Rust 개발 흐름을 도입하려면 교육 및 상담이 적합합니다. 이어서 Claude Code TDD리뷰 워크플로 체크리스트도 함께 보면 좋습니다.

실제로 이 구조를 적용해 보니, 로직을 src/lib.rs에 두고 cargo test로 동작을 고정한 뒤 Claude Code에 소유권을 설명하게 하는 방식이 가장 안정적이었습니다. 한 번에 전체 CLI를 맡기는 것보다 불필요한 clone()이 줄고 리뷰 가능한 차이가 나왔습니다.

#Claude Code #Rust #CLI #테스트 #리팩터링
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.