Cargo, 모듈, 크레이트로 프로젝트 키우기
한 파일에서 이것저것 다 해보던 시기는 꽤 즐겁습니다. 코드를 열면 모든 것이 한눈에 보이고, 찾기도 쉽습니다. 그런데 프로그램이 조금만 커지면 금방 상황이 바뀝니다. main.rs가 끝도 없이 길어지고, 비슷한 역할의 코드가 여기저기 흩어지고, 어제 분명 내가 쓴 함수인데 오늘은 어디 있는지 못 찾는 일이 생깁니다.
Rust를 계속 제대로 써먹으려면 이제 한 단계 올라가야 합니다. 코드를 나누고, 숨기고, 다시 조립하는 법을 알아야 합니다. 그때 등장하는 핵심 도구가 바로 Cargo, 패키지, 크레이트, 모듈입니다.1

프로젝트가 커질수록 정리가 먼저다
작은 예제에서는 "돌아가기만 하면 됐다"가 통합니다. 하지만 실제 프로젝트는 다릅니다. 기능이 늘어나면 코드도 역할별로 나눠야 읽기 쉽고, 고치기 쉽고, 재사용하기 쉬워집니다.
예를 들어 할 일 관리 프로그램을 만든다고 해봅시다. 처음에는 할 일 추가, 출력, 완료 처리, 저장 기능을 전부 main.rs에 써도 괜찮습니다. 하지만 기능이 늘어나면 금방 이런 질문이 생깁니다.
할 일 데이터 구조는 어디에 두지?
화면 출력 함수는 어디로 빼지?
나중에 웹 버전으로 바꾸면 이 로직을 다시 쓸 수 있을까?
다른 파일에서 이 함수에 접근해도 될까, 아니면 숨겨야 할까?
이 질문들에 답하는 체계가 Rust의 모듈 시스템입니다. Rust는 단순히 파일을 나누는 것에서 끝나지 않고, 무엇을 공개하고 무엇을 감출지까지 꽤 엄격하게 다루게 해줍니다.1
패키지, 크레이트, 모듈을 한 번에 이해하기
이 셋은 처음 배우면 이름이 비슷해서 헷갈립니다. 하지만 역할을 나눠 보면 생각보다 단순합니다.
패키지(package): Cargo가 관리하는 프로젝트 단위입니다. 보통
Cargo.toml이 있는 폴더 하나를 떠올리면 됩니다.크레이트(crate): 컴파일되는 Rust 코드 단위입니다. 실행 파일이 되는 바이너리 크레이트와 재사용용 라이브러리 크레이트가 있습니다.
모듈(module): 크레이트 안에서 코드를 역할별로 나누는 구조입니다.
경로(path): 어떤 함수, 구조체, 모듈이 어디 있는지 가리키는 주소입니다.1
아주 거칠게 비유하면 이렇습니다. 패키지는 건물이고, 크레이트는 그 건물 안의 가게이며, 모듈은 가게 안의 진열대입니다. 경로는 "몇 층 어느 매장 안쪽 선반" 같은 주소입니다. 비유가 완벽하진 않지만, 처음 감을 잡는 데는 도움이 됩니다.
Cargo가 만들어 주는 기본 구조
cargo new my_project를 실행하면 Cargo는 기본 프로젝트 구조를 만들어 줍니다. 여기서 중요한 점은 Cargo가 관례를 많이 알고 있다는 것입니다. src/main.rs가 있으면 바이너리 크레이트의 시작점으로 보고, src/lib.rs가 있으면 라이브러리 크레이트의 시작점으로 봅니다. 또 src/bin 아래에 파일을 두면 각각 별도의 바이너리 크레이트로 취급합니다.2
즉, 아래 구조는 아주 Rust답습니다.
my_project/
├── Cargo.toml
└── src/
└── main.rs이 상태는 "실행 가능한 프로그램 하나"가 있는 패키지입니다.
여기에 src/lib.rs가 추가되면 이야기가 조금 달라집니다.
my_project/
├── Cargo.toml
└── src/
├── lib.rs
└── main.rs이제 이 패키지 안에는 두 크레이트가 들어 있습니다. 하나는 실행용 바이너리 크레이트, 다른 하나는 재사용 가능한 라이브러리 크레이트입니다.2
초보자에게는 이 구조가 아주 중요합니다. 실무에서는 실행은 main.rs가 맡고, 핵심 로직은 lib.rs 쪽에 모으는 방식이 자주 쓰이기 때문입니다. 이렇게 해두면 테스트하기도 쉬워지고, 다른 프로그램에서 재사용하기도 편합니다.
한 파일짜리 코드를 여러 파일로 나누기
이제 진짜로 프로젝트를 키워봅시다. 처음에는 아래처럼 한 파일에 다 들어 있을 수 있습니다.
fn main() {
println!("할 일 앱 시작");
}이 정도는 괜찮습니다. 하지만 할 일 데이터 구조와 출력 로직을 분리하고 싶다면 파일을 나눌 수 있습니다.
src/
├── main.rs
├── task.rs
└── ui.rsmain.rs에는 이렇게 씁니다.
mod task;
mod ui;
use task::Task;
fn main() {
let mut task = Task::new("Rust 모듈 정리하기");
task.complete();
ui::print_task(&task);
}여기서 mod task;는 "task라는 모듈이 있고, 구현은 다른 파일에 있다"는 뜻입니다. mod ui;도 마찬가지입니다. 그리고 use task::Task;는 긴 경로를 짧게 가져오는 선언입니다.
task.rs는 이렇게 만들 수 있습니다.
pub struct Task {
title: String,
done: bool,
}
impl Task {
pub fn new(title: &str) -> Self {
Self {
title: title.to_string(),
done: false,
}
}
pub fn complete(&mut self) {
self.done = true;
}
pub fn title(&self) -> &str {
&self.title
}
pub fn is_done(&self) -> bool {
self.done
}
}ui.rs는 이렇게 둘 수 있습니다.
use crate::task::Task;
pub fn print_task(task: &Task) {
let mark = if task.is_done() { "✅" } else { "⬜" };
println!("{} {}", mark, task.title());
}이 예제에서 꼭 눈여겨볼 점이 있습니다.
첫째, Task와 메서드들 앞에 pub가 붙었습니다. 다른 모듈에서 쓰게 하려면 공개해야 하기 때문입니다. Rust는 기본이 비공개입니다. 문을 닫아두고 필요할 때만 여는 방식이라고 생각하면 됩니다.
둘째, title과 done 필드는 그대로 비공개입니다. 대신 title()과 is_done() 같은 메서드로 읽게 했습니다. 이게 바로 구현은 숨기고, 사용 방법만 공개하는 방식입니다. 이렇게 해두면 내부 표현을 나중에 바꿔도 외부 코드를 덜 흔들 수 있습니다.1
mod, use, pub, crate를 헷갈리지 않는 법
초보자가 특히 자주 헷갈리는 네 단어가 있습니다.
mod: 모듈을 선언합니다. "이 코드 묶음을 프로젝트 구조에 포함시켜라"에 가깝습니다.use: 경로를 현재 범위로 가져옵니다. 긴 주소를 별명처럼 짧게 쓰게 해줍니다.pub: 다른 곳에서 접근 가능하게 공개합니다.crate: 현재 크레이트의 루트를 가리키는 기준점입니다.1
예를 들어 use crate::task::Task;는 "현재 크레이트의 루트에서 시작해서 task 모듈 안의 Task를 가져와라"라는 뜻입니다.
감각적으로 기억하면 좋습니다. mod는 방을 만들고, pub는 문을 열고, use는 자주 가는 길에 지름길 표지판을 붙이는 일입니다.
그리고 한 가지 더 기억해두면 좋은 것이 pub(crate)입니다. 이것은 "바깥 세상 전체에 공개하지는 않지만, 현재 크레이트 안에서는 쓰게 하겠다"는 뜻입니다. 완전 공개와 완전 비공개 사이의 중간 문이라고 생각하면 됩니다.
main.rs만 비대해지지 않게 만드는 패턴
실전에서는 main.rs에 모든 로직을 넣기보다, main.rs는 입구만 담당하게 두는 편이 좋습니다. 사용자의 입력을 받고, 프로그램 흐름을 시작하고, 실제 일은 다른 모듈에 맡기는 식입니다.
예를 들어 이런 구조가 더 건강합니다.
src/
├── main.rs
├── lib.rs
├── task.rs
└── ui.rslib.rs:
pub mod task;
pub mod ui;main.rs:
use my_project::task::Task;
use my_project::ui::print_task;
fn main() {
let task = Task::new("Cargo 구조 이해하기");
print_task(&task);
}이렇게 하면 핵심 기능은 라이브러리 크레이트에 모이고, main.rs는 그 기능을 불러 실행하는 얇은 껍데기가 됩니다. 나중에 테스트를 붙이거나, 같은 로직을 다른 바이너리에서 재사용할 때 훨씬 편합니다.
참고로 패키지 이름에 하이픈(-)이 있으면 코드에서 크레이트 이름은 언더스코어(_)로 바뀝니다. 예를 들어 패키지 이름이 todo-app이라면 코드에서는 todo_app으로 사용합니다.3
외부 크레이트를 추가해 프로젝트의 팔을 늘리기
Rust의 표준 라이브러리는 탄탄하지만, 모든 기능을 다 넣어두진 않습니다. 대신 Cargo가 외부 크레이트를 쉽게 가져다 쓰게 해줍니다. Cargo는 의존성을 내려받고, 빌드하고, 패키지를 만들고, 필요하면 crates.io에 올리는 일까지 맡습니다.4
외부 크레이트를 찾는 가장 대표적인 곳은 crates.io입니다. Rust 커뮤니티의 공식 크레이트 레지스트리라고 생각하면 됩니다.5
예를 들어 난수를 하나 뽑고 싶다면 Cargo.toml에 이렇게 적을 수 있습니다.
[dependencies]
rand = "0.9"또는 보통은 이렇게 명령어로 추가합니다.
cargo add rand그다음 코드에서 사용하면 됩니다.
use rand::Rng;
fn main() {
let mut rng = rand::rng();
let priority = rng.random_range(1..=3);
println!("오늘의 우선순위: {}", priority);
}여기서 중요한 점은 예제가 현재 버전 기준의 문법을 따라야 한다는 것입니다. Rust 생태계는 빠르게 발전하므로, 오래된 블로그 예제를 그대로 가져오면 경고가 뜨거나 더 이상 권장되지 않는 API가 나올 때가 있습니다.
또 하나 중요한 감각이 있습니다. Cargo.toml은 "나는 이런 라이브러리가 필요해요"라고 적는 설계서에 가깝고, Cargo.lock은 실제로 선택된 정확한 버전을 기록한 스냅샷에 가깝습니다. 그래서 팀 개발이나 배포 환경에서 빌드 결과를 안정적으로 맞추는 데 도움이 됩니다.6
버전과 기능(feature)을 너무 무섭게 볼 필요는 없다
초보자는 Cargo.toml을 열었을 때 오히려 코드보다 이 파일이 더 무서울 때가 있습니다. 하지만 처음에는 두 가지만 보면 됩니다.
첫째는 버전입니다. 예를 들어 rand = "0.9"처럼 쓰면 해당 크레이트의 호환 가능한 버전을 사용하겠다는 뜻입니다. 보통은 너무 빡빡하게 고정하기보다 Cargo가 안전한 범위 안에서 업데이트할 수 있게 두는 편이 편합니다.6
둘째는 기능(feature) 입니다. 어떤 크레이트는 필요한 기능만 골라 켤 수 있습니다. 예를 들어 serde에서 파생 매크로를 쓰고 싶다면 이렇게 적습니다.6
[dependencies]
serde = { version = "1.0", features = ["derive"] }이런 기능 옵션은 "세트 메뉴에서 토핑 추가" 정도로 생각하면 됩니다. 기본값만으로 충분한 경우도 많고, 필요할 때만 열면 됩니다.
크레이트를 고를 때는 단순히 이름만 보지 말고 문서, 최근 관리 상태, 예제 품질, 기능 구성을 함께 보는 습관을 들이면 좋습니다. crates.io의 각 페이지에는 버전과 문서 링크가 있고, 보통 docs.rs 같은 문서 사이트로도 연결됩니다.56
문서화는 미래의 나를 위한 편지다
코드를 나누기 시작하면, "이 함수가 정확히 뭐 하는 거였지?"라는 질문이 급격히 늘어납니다. 그때 문서 주석이 큰 힘을 발휘합니다.
Rust에서는 ///로 문서 주석을 달 수 있습니다.
/// 할 일 하나를 나타냅니다.
pub struct Task {
title: String,
done: bool,
}
impl Task {
/// 새로운 할 일을 만듭니다.
pub fn new(title: &str) -> Self {
Self {
title: title.to_string(),
done: false,
}
}
}그리고 아래 명령으로 프로젝트 문서를 생성할 수 있습니다.
cargo doc --open이 명령은 문서를 빌드하고 브라우저로 열어줍니다. 코드가 늘어날수록 이 기능의 가치가 커집니다. 직접 만든 모듈 구조를 웹 문서처럼 볼 수 있어서, "내 코드 사용법"을 프로젝트 안에 같이 남길 수 있습니다.7
초보자일수록 문서화를 거창하게 생각하지 않아도 됩니다. 함수 위에 "무엇을 하는지", "입력은 무엇인지", "언제 쓰는지"만 적어도 충분히 훌륭합니다.
자주 쓰는 Cargo 명령어만 먼저 손에 익히자
Cargo는 할 수 있는 일이 많지만, 처음에는 자주 쓰는 몇 개만 확실히 익히면 됩니다.
cargo check: 실행 파일을 만들지 않고 빠르게 문법과 타입 오류를 확인합니다.cargo build: 프로젝트를 빌드합니다.cargo run: 빌드 후 실행합니다.cargo add 크레이트이름: 의존성을 추가합니다.cargo doc --open: 문서를 생성하고 바로 엽니다.47
여기서 특히 cargo check는 습관처럼 자주 쓰면 좋습니다. Rust는 컴파일러가 꼼꼼한 편이라, 빨리 확인하는 습관이 생산성을 꽤 올려줍니다.
구조가 복잡해지면 시각화 도구도 도움이 된다
프로젝트가 커지면 "지금 모듈이 어떻게 연결돼 있지?"가 잘 안 보일 때가 있습니다. 그럴 때 cargo-modules 같은 도구가 도움이 됩니다. 이 도구는 크레이트의 계층 구조를 트리 형태로 보여주거나, 내부 의존 관계를 분석해줍니다. 즉, 내 프로젝트의 뼈대를 바깥에서 훑어보게 해주는 셈입니다.8
예를 들어 이런 식으로 쓸 수 있습니다.
cargo install cargo-modules
cargo modules structure초보자에게 필수 도구는 아니지만, 파일이 많아졌을 때 "왜 이렇게 꼬였지?" 싶은 순간 꽤 든든한 손전등이 됩니다.
초보자가 기억하면 좋은 구조 감각
프로젝트를 키울 때는 문법 지식보다 구조 감각이 더 중요할 때가 많습니다. 아래 원칙은 단순하지만 오래 갑니다.
main.rs는 가능한 한 얇게 두고, 실제 로직은 모듈이나lib.rs쪽으로 보냅니다.여러 곳에서 쓸 코드는 라이브러리 크레이트 쪽으로 모읍니다.
pub는 필요한 만큼만 열고, 기본은 닫아둡니다.데이터와 그 데이터를 다루는 동작을 가까이 둡니다.
이 원칙만 지켜도 프로젝트는 훨씬 읽기 쉬워집니다. 코드 정리는 미적 취향이 아니라, 버그를 줄이고 수정 비용을 낮추는 실전 기술입니다.
마무리
지금까지는 Rust 문법 자체를 배우는 시간이 컸다면, 이제부터는 Rust 코드를 어떻게 살아남게 만들 것인가를 배우는 단계입니다. Cargo는 프로젝트의 뼈대를 세우고, 크레이트는 컴파일 단위를 나누고, 모듈은 기능을 정리하며, pub와 use는 그 경계를 다듬습니다.
한 파일짜리 연습을 넘어 실제 프로젝트 구조를 이해하면, Rust는 갑자기 훨씬 덜 답답하고 훨씬 더 실용적인 언어가 됩니다. 이제 여러분은 코드를 "작성하는 사람"에서, 코드를 "구조화하는 사람"으로 한 걸음 옮겨온 셈입니다.
참고
1Packages, Crates, and Modules - The Rust Programming Language
2Packages and Crates - The Rust Programming Language
3Extern crates - The Rust Reference
4Introduction - The Cargo Book
5crates.io: Rust Package Registry
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.