출시·고도화 중
Rust 안내서 · 4/6
프로그램이 커지면 코드를 나누고, 외부 라이브러리를 가져다 쓰고, 여러 프로젝트를 함께 관리해야 합니다. 이 장에서는 Rust의 모듈 시스템과 Cargo의 패키지 관리, 워크스페이스, 기능 플래그(features)를 다룹니다.
| 용어 | 뜻 |
|---|---|
| 패키지(package) | Cargo.toml 하나로 묶인 단위. 크레이트를 하나 이상 담습니다 |
| 크레이트(crate) | 컴파일 단위. 실행 파일(binary) 또는 라이브러리(library) |
| 모듈(module) | 크레이트 안에서 코드를 나누고 공개 범위를 정하는 단위 |
| 경로(path) | crate::net::connect처럼 항목을 가리키는 이름 |
실행 크레이트의 뿌리는 src/main.rs, 라이브러리 크레이트의 뿌리는 src/lib.rs입니다. crates.io에 올라가 공유되는 라이브러리도 "크레이트"라고 부릅니다.
mod로 모듈을 만듭니다. 모듈 안의 항목은 기본적으로 비공개이며, 밖에서 쓰려면 pub을 붙여야 합니다. 구조체는 pub struct로 공개해도 필드는 따로 pub을 붙여야 공개됩니다. use는 긴 경로를 짧은 이름으로 가져옵니다.
mod geometry {
pub mod shapes {
pub struct Circle {
pub radius: f64,
}
impl Circle {
pub fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
}
pub fn double(x: f64) -> f64 {
helper(x) * 2.0
}
// pub 이 없으므로 geometry 모듈 안에서만 보입니다
fn helper(x: f64) -> f64 {
x
}
}
use geometry::shapes::Circle;
fn main() {
let c = Circle { radius: 2.0 };
println!("{:.2}", c.area());
println!("{}", geometry::double(1.5));
}경로는 크레이트 뿌리에서 시작하는 crate::, 현재 모듈 기준 self::, 부모 모듈 기준 super::로 씁니다. 공개 범위를 좁히고 싶으면 pub(crate)(이 크레이트 안에서만)나 pub(super)(부모 모듈까지만)를 씁니다.
mod config;처럼 본문 없이 선언하면 Cargo는 src/config.rs(또는 src/config/mod.rs)에서 내용을 찾습니다. 하위 모듈은 src/config/ 폴더에 둡니다.
// src/main.rs
mod config;
use config::Config;
fn main() {
let cfg = Config::new();
println!("port = {}", cfg.port);
}// src/config.rs
pub struct Config {
pub port: u16,
}
impl Config {
pub fn new() -> Self {
Config { port: 8080 }
}
}use 문은 여러 항목을 한 번에 가져오거나(use std::collections::{HashMap, HashSet};), 이름을 바꾸거나(use std::io::Result as IoResult;), 다시 내보낼 수 있습니다. 라이브러리에서 pub use로 내부 경로를 다시 내보내면 사용자는 깊은 경로를 몰라도 됩니다.
공개 라이브러리는 crates.io에 모여 있고, 문서는 docs.rs에서 볼 수 있습니다. 의존성은 cargo add로 추가하는 것이 가장 쉽습니다.
cargo add serde --features derive
cargo add serde_json
cargo add --dev pretty_assertions
cargo update # Cargo.lock 의 버전을 호환 범위 안에서 올립니다명령을 실행하면 Cargo.toml에 다음과 같이 기록됩니다. 버전 "1"은 SemVer 호환 범위(1.x)를 뜻하며, 실제로 고른 정확한 버전은 Cargo.lock에 고정됩니다.
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dev-dependencies]
pretty_assertions = "1"이제 코드에서 크레이트 이름으로 바로 쓸 수 있습니다. 이름에 하이픈이 있으면 코드에서는 밑줄로 바꿔 씁니다.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
name: String,
age: u32,
}
fn main() -> Result<(), serde_json::Error> {
let user = User { name: "kim".to_string(), age: 30 };
let json = serde_json::to_string(&user)?;
println!("{json}"); // {"name":"kim","age":30}
let back: User = serde_json::from_str(&json)?;
println!("{back:?}");
Ok(())
}여러 패키지를 한 저장소에서 함께 개발할 때는 워크스페이스를 씁니다. 멤버들은 Cargo.lock과 target/ 폴더를 공유하므로 빌드가 빠르고 버전이 어긋나지 않습니다.
# 루트 Cargo.toml
[workspace]
members = ["app", "shared"]
resolver = "2"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
# app/Cargo.toml
[dependencies]
shared = { path = "../shared" }
serde = { workspace = true }[workspace.dependencies]에 버전을 한 번만 적고 멤버에서는 workspace = true로 물려받습니다. 특정 멤버만 빌드하거나 실행하려면 cargo build -p app, cargo run -p app처럼 -p를 붙입니다. 루트에 패키지가 없는 워크스페이스는 resolver를 직접 적는 것이 좋습니다(2021 에디션은 "2", 2024 에디션은 "3").
features는 선택적으로 켜고 끌 수 있는 컴파일 옵션입니다. 무거운 의존성을 필요할 때만 포함하게 할 때 주로 씁니다.
[features]
default = ["json"]
json = ["dep:serde_json"]
[dependencies]
serde_json = { version = "1", optional = true }코드에서는 #[cfg(feature = "json")]로 해당 기능이 켜졌을 때만 컴파일할 부분을 표시합니다. 빌드할 때 cargo build --no-default-features로 기본 기능을 끄거나 cargo build --features json으로 켭니다.
mod 이름;은 같은 이름의 파일을 불러오며, use로 경로를 짧게 씁니다.cargo add로 추가하고, 정확한 버전은 Cargo.lock에 고정됩니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.