| name | rust-dev |
| description | 모던 Rust 개발 컨벤션을 강제하는 스킬. cargo(빌드/패키지 관리), clippy(린터), rustfmt(포매터), Rust 2021+ edition 문법을 사용한다. Rust 파일 생성, 프로젝트 초기화, Cargo.toml 수정, 의존성 추가, Rust 코드 작성, API 서비스 구축 등 Rust 관련 작업 시 반드시 이 스킬을 사용할 것. Makefile 기반 빌드, 수동 의존성 관리, nightly-only 기능 남용을 지양하고 cargo 표준 워크플로우를 따른다.
|
| hooks | {"PostToolUse":[{"matcher":"Write|Edit","hooks":[{"type":"command","command":"python3 ${CLAUDE_SKILL_DIR}/scripts/rust-post-edit-lint.py","timeout":30,"statusMessage":"Rust clippy/rustfmt 적용 중..."}]}],"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"python3 ${CLAUDE_SKILL_DIR}/scripts/rust-pre-commit-check.py","timeout":300,"statusMessage":"Rust 커밋 전 코드 검증 중 (clippy, rustfmt, test)..."}]}]} |
Modern Rust Development
이 스킬은 Rust 표준 툴체인 중심의 모던 개발을 강제한다. Claude가 기본적으로 제안할 수 있는 비표준 패턴 대신, cargo/clippy/rustfmt 기반의 관용적(idiomatic) Rust 워크플로우를 사용한다.
핵심 철학
- cargo 중심 워크플로우 — 빌드, 테스트, 벤치마크, 의존성 관리를 모두 cargo로 통합한다. Makefile이나 외부 빌드 스크립트에 의존하지 않는다.
- 관용적 Rust — Rust의 타입 시스템, 소유권, 라이프타임을 적극 활용한다.
unsafe는 명확한 이유 없이 사용하지 않는다. clone()을 소유권 문제 회피 수단으로 남용하지 않는다.
- Rust 2021 edition — 최신 안정 기능을 적극 사용한다.
edition = "2021"을 기본으로 설정한다.
- 에러 처리는 타입으로 —
unwrap()/expect()는 테스트와 초기화 코드에서만 허용한다. 라이브러리는 커스텀 에러 타입, 애플리케이션은 anyhow/thiserror를 사용한다.
금지 패턴 → 대체 매핑
| 금지 | 대체 | 이유 |
|---|
unwrap() (프로덕션 코드) | ? 연산자 + 적절한 에러 타입 | 패닉 대신 에러 전파가 안전하고 합성 가능 |
clone() 남용 | 참조, 라이프타임, Cow<T> | 불필요한 힙 할당 방지, 소유권 모델 활용 |
unsafe (명확한 이유 없이) | 안전한 추상화 | unsafe는 FFI, 성능 최적화 등 명확한 사유 필요 |
println! (로깅 목적) | tracing / log 크레이트 | 구조화된 로깅이 프로덕션에서 관찰 가능성 제공 |
| Makefile 기반 빌드 | cargo build / cargo xtask | cargo가 빌드 의존성과 기능 플래그를 통합 관리 |
Box<dyn Error> (라이브러리) | thiserror 커스텀 에러 | 호출자가 에러를 패턴 매칭으로 처리 가능 |
String (에러 메시지용) | thiserror / anyhow | 타입 기반 에러가 합성 가능하고 컨텍스트 제공 |
| nightly-only 기능 | stable 대안 | 프로덕션 코드는 stable 컴파일러에서 빌드되어야 함 |
프로젝트 초기화
새 프로젝트 생성
cargo init <project-name>
cd <project-name>
cargo init --lib <project-name>
워크스페이스 (다중 크레이트)
모노레포나 다중 크레이트 프로젝트는 워크스페이스를 사용한다.
[workspace]
resolver = "2"
members = ["crates/*"]
[workspace.package]
edition = "2021"
rust-version = "1.75"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
디렉토리 레이아웃
project-name/
├── Cargo.toml
├── Cargo.lock # 바이너리: 커밋, 라이브러리: .gitignore
├── src/
│ ├── main.rs # 바이너리 진입점
│ ├── lib.rs # 라이브러리 루트
│ ├── error.rs # 커스텀 에러 타입
│ └── modules/
│ └── mod.rs
├── tests/ # 통합 테스트
│ └── integration_test.rs
├── benches/ # 벤치마크
│ └── benchmark.rs
└── examples/ # 예제 코드
└── basic.rs
Cargo.toml 전체 템플릿은 references/cargo-toml-templates.md를 참조하라.
타입 시스템 & 소유권 규칙
필수 규칙
- 함수 시그니처에 명시적 타입을 선언한다 (로컬 변수는 타입 추론 활용 가능).
&self/&mut self/self 중 최소 권한 원칙에 따라 선택한다.
String 대신 &str을 파라미터로 받는다 (소유권이 필요한 경우만 String).
Vec<T> 대신 &[T]를 파라미터로 받는다 (소유권이 필요한 경우만 Vec<T>).
- 제네릭 바운드는
impl Trait 또는 where 절로 표현한다.
Before / After 예시
fn process_data(data: String) -> String {
let parsed: Value = serde_json::from_str(&data).unwrap();
let result = parsed["name"].as_str().unwrap().to_string();
result
}
fn process_data(data: &str) -> Result<String> {
let parsed: Value = serde_json::from_str(data)?;
let name = parsed["name"]
.as_str()
.context("missing 'name' field")?;
Ok(name.to_owned())
}
에러 처리
프로젝트 유형에 따라 에러 처리 전략을 선택한다.
라이브러리 크레이트
→ thiserror 기반 커스텀 에러 enum
이유: 호출자가 에러를 패턴 매칭으로 세밀하게 처리 가능
애플리케이션 (바이너리)
→ anyhow::Result + .context()
이유: 에러 체이닝과 백트레이스로 디버깅 용이
혼합 (lib + bin)
→ lib에 thiserror, bin에 anyhow
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("validation failed: {field}")]
Validation { field: String },
#[error("not found: {0}")]
NotFound(String),
}
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from {path}"))?;
let config: Config = toml::from_str(&content)
.context("invalid config format")?;
Ok(config)
}
에러 처리 상세 패턴은 references/error-handling-guide.md를 참조하라.
린팅 & 포매팅
clippy가 린터, rustfmt가 포매터를 담당한다.
clippy 설정
프로젝트 루트에 clippy.toml 또는 Cargo.toml의 [lints.clippy]에 설정한다.
[lints.clippy]
unwrap_used = "deny"
expect_used = "warn"
panic = "deny"
needless_pass_by_value = "warn"
cloned_instead_of_copied = "warn"
redundant_closure_for_method_calls = "warn"
doc_markdown = "warn"
missing_errors_doc = "warn"
must_use_candidate = "warn"
[lints.rust]
unsafe_code = "forbid"
실행 순서
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
cargo fmt --all
전체 clippy 설정과 lint 그룹은 references/clippy-config.md를 참조하라.
의존성 관리
cargo add serde --features derive
cargo add tokio --features full
cargo add --dev criterion
cargo add --dev tokio-test
cargo add --build cc
cargo remove <crate>
cargo update
cargo update -p serde
cargo install cargo-audit
cargo audit
Cargo.lock 관리
- 바이너리 프로젝트:
Cargo.lock을 git에 커밋한다 (재현 가능한 빌드 보장).
- 라이브러리 프로젝트:
Cargo.lock을 .gitignore에 추가한다 (소비자가 자체 락 파일 관리).
Feature 플래그
[features]
default = []
full = ["serde", "async"]
serde = ["dep:serde"]
async = ["dep:tokio"]
feature 플래그로 선택적 의존성을 관리한다. default는 비워두고 사용자가 명시적으로 활성화하게 한다.
테스팅
cargo test
cargo test test_name
cargo test module_name::
cargo test --doc
cargo test --release
cargo test -- --nocapture
테스트 구조
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_input() {
let result = parse("hello").unwrap();
assert_eq!(result, Expected::Value("hello"));
}
#[test]
fn test_parse_empty_input() {
let result = parse("");
assert!(result.is_err());
}
}
use my_crate::public_api;
#[test]
fn test_full_workflow() {
let result = public_api::process("input");
assert!(result.is_ok());
}
비동기 테스트
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_async_operation() {
let result = fetch_data("url").await.unwrap();
assert!(!result.is_empty());
}
}
비동기 런타임
비동기가 필요한 경우 tokio를 기본 런타임으로 사용한다.
[dependencies]
tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() -> anyhow::Result<()> {
Ok(())
}
비동기 패턴 상세는 references/async-patterns.md를 참조하라.
CLI 도구
CLI가 필요한 경우 clap을 사용한다. derive 매크로로 선언적으로 CLI를 정의한다.
cargo add clap --features derive
use clap::Parser;
#[derive(Parser)]
#[command(name = "mytool", about = "파일 처리 도구")]
struct Cli {
input: String,
#[arg(short, long, default_value = "output")]
output_dir: String,
#[arg(short, long)]
verbose: bool,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
Ok(())
}
웹 서비스
HTTP 서비스가 필요한 경우 axum을 기본 프레임워크로 사용한다.
cargo add axum tokio --features tokio/full
cargo add serde --features derive
cargo add tower-http --features cors,trace
use axum::{routing::get, Json, Router};
use serde::Serialize;
#[derive(Serialize)]
struct Health {
status: String,
}
async fn health() -> Json<Health> {
Json(Health { status: "ok".into() })
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let app = Router::new().route("/health", get(health));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
Serialization
데이터 직렬화/역직렬화는 serde를 사용한다.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct User {
id: u64,
user_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
}
QA 체크리스트
코드를 커밋하기 전에 다음을 실행한다.
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo doc --no-deps
모든 명령이 에러 없이 통과해야 한다.
Reference 파일
필요할 때 다음 파일을 읽어 상세 설정을 확인하라.
| 파일 | 로드 시점 |
|---|
references/cargo-toml-templates.md | 프로젝트 초기화 또는 Cargo.toml 수정 시 |
references/clippy-config.md | clippy 설정 추가/변경 또는 lint 에러 해결 시 |
references/error-handling-guide.md | 에러 타입 설계 또는 에러 처리 패턴 선택 시 |
references/async-patterns.md | tokio 비동기 코드, 동시성 패턴 작성 시 |