콘텐츠로 이동

Rust Cargo와 크레이트

시작하기 페이지에서 cargo new와 cargo run을 써 봤죠. 그건 시작에 불과해요. Cargo는 Rust의 빌드 도구이자 패키지 매니저예요. 코드를 컴파일하고, 테스트를 실행하고, 파일을 정리(포맷)하고, 다른 사람이 만든 라이브러리를 내려받아요.

이런 라이브러리를 크레이트(crate)라고 불러요. 앱 스토어의 앱을 떠올려 보세요. 누군가 이미 어떤 문제(JSON 읽기, 웹 요청 보내기, 날짜 파싱)를 해결해 두었고, 명령어 한 줄로 그 해결책을 내 프로젝트에 넣을 수 있어요. 이 페이지에서는 그게 어떻게 돌아가는지, 그리고 좋은 크레이트를 어떻게 고르는지 알아볼게요.

자주 나오는 단어가 두 개 있어요.

  • 크레이트는 컴파일러가 빌드하는 Rust 코드의 한 단위예요. 바이너리(실행할 수 있는 프로그램, src/main.rs에서 시작)이거나 라이브러리(다른 프로그램이 가져다 쓰는 코드, src/lib.rs에서 시작) 중 하나예요.
  • 패키지는 cargo new가 만들어 주는 것이에요. Cargo.toml 파일과 하나 이상의 크레이트가 들어 있는 폴더예요.

평소에는 둘 다 그냥 “크레이트”라고 불러요. 누가 “serde 크레이트를 써”라고 하면 “crates.io에 있는 serde 패키지를 프로젝트에 추가해”라는 뜻이에요.

모든 패키지에는 Cargo.toml 파일이 있어요. 간단한 설정 형식인 TOML (영어)로 쓰여 있어요. cargo new hello_rust를 실행하면 이런 파일이 생겨요.

[package]
name = "hello_rust"
version = "0.1.0"
edition = "2024"
[dependencies]
  • [package] 섹션은 프로젝트를 설명해요. name과 version은 나중에 공개(publish)했을 때 다른 사람들이 보게 될 정보예요.
  • edition 항목은 어떤 에디션의 Rust 언어를 쓸지 정해요. 에디션(2018, 2021, 2024) 덕분에 Rust는 옛날 코드를 망가뜨리지 않으면서 발전할 수 있어요. cargo new가 써 준 값을 그대로 두면 돼요.
  • [dependencies] 섹션에는 코드가 쓰는 크레이트를 적어요. 처음에는 비어 있어요.

프로젝트가 커지면 다른 섹션도 보게 될 거예요.

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dev-dependencies]
# 테스트와 예제에서만 쓰고, 실제 프로그램에서는 쓰지 않아요
pretty_assertions = "1.4"
[profile.release]
# `cargo build --release`용 설정
opt-level = 3

[dependencies]를 손으로 고칠 수도 있지만, 가장 쉬운 방법은 cargo add예요. crates.io에서 최신 버전을 찾아서 줄을 대신 써 줘요.

Terminal window
cargo add serde_json

어떤 크레이트에는 기능(feature)이라고 부르는 선택 부분이 있어요. 빌드를 작게 유지하려고 기본으로는 꺼져 있고, --features로 켜요.

Terminal window
cargo add serde --features derive

이 두 명령을 실행하면 Cargo.toml에 대략 이런 내용이 들어가요.

[dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"

이제 코드에서 크레이트를 써 보세요. 다음 cargo run 때 크레이트를 내려받아 한 번 컴파일하고, 프로그램에 연결(link)해요.

use serde_json::json;
fn main() {
let crab = json!({ "legs": 10, "name": "Ferris" });
println!("{}", crab);
println!("이름: {}", crab["name"]);
}

실행 결과:

{"legs":10,"name":"Ferris"}
이름: "Ferris"
▶ 직접 실행해 보기

"Ferris"에 따옴표가 붙은 건 crab["name"]이 아직 평범한 Rust 문자열이 아니라 JSON 값이기 때문이에요.

Rust Playground에는 인기 있는 크레이트가 이미 들어 있어서, cargo add 없이도 여기서 실행돼요. 내 컴퓨터에서는 먼저 cargo add serde_json을 실행해야 해요. 이 크레이트는 Serde로 JSON 다루기 페이지에서 훨씬 자세히 배워요.

더 이상 필요 없는 크레이트를 지우려면 cargo remove serde_json을 실행하세요.

serde_json = "1.0"이라고 쓰면, 정확히 1.0 버전을 달라는 뜻이 아니에요. “1.0과 호환되는 버전이면 뭐든 괜찮아”라는 뜻이에요. Cargo는 유의적 버전(Semantic Versioning, SemVer)을 따라요. 버전은 MAJOR.MINOR.PATCH 형태이고, 메이저 숫자가 바뀔 때만 코드가 깨지는 변경이 허용돼요.

이렇게 쓰면 Cargo가 쓸 수 있는 버전 이유
"1.0" ("^1.0"과 같음) >=1.0.0 그리고 <2.0.0 1.x 버전은 모두 호환돼야 하니까
"1.4.2" >=1.4.2 그리고 <2.0.0 최소 1.4.2, 하지만 여전히 1.x
"0.4" >=0.4.0 그리고 <0.5.0 1.0 전에는 마이너 숫자가 깨지는 변경으로 취급돼요
"~1.4" >=1.4.0 그리고 <1.5.0 패치 업데이트만
"=1.4.2" 정확히 1.4.2 거의 쓸 일이 없어요

그럼 정확한 버전은 누가 정할까요? 그게 바로 Cargo.lock 파일이 하는 일이에요. 처음 빌드할 때 Cargo는 요구 사항에 맞는 가장 새로운 버전을 골라서 Cargo.lock에 적어 둬요. 그 뒤로는 내 컴퓨터든 다른 사람 컴퓨터든, 모든 빌드가 똑같은 버전을 써요.

  • Cargo.lock은 손으로 고치지 마세요. Cargo가 관리해요.
  • git에 커밋하세요. 특히 프로그램이라면 더 그래요. 그러면 팀원과 CI가 내가 빌드한 것과 똑같이 빌드해요.
  • cargo update 명령은 Cargo.toml에 여전히 맞는 가장 새로운 버전으로 올리고 Cargo.lock을 다시 써요. 새 메이저 버전으로는 절대 넘어가지 않아요. 그러려면 Cargo.toml을 직접 고쳐야 해요.

계속 들르게 될 웹사이트가 세 곳 있어요.

  • crates.io (영어)는 공식 저장소(레지스트리)예요. 공개된 크레이트는 모두 여기 있어요. “csv”나 “date” 같은 단어로 검색해 보세요.
  • docs.rs (영어)는 모든 크레이트의 문서를 자동으로 빌드해서 올려 둬요. 예를 들면 docs.rs/serde_json 같은 식이에요.
  • lib.rs (영어)는 비공식이지만 더 빠른 목록 사이트로, 크레이트를 분야별로 묶어 둬요. 둘러보기에 좋아요.

같은 문제를 푸는 크레이트가 여러 개인 경우가 많은데, 어떻게 고를까요? 추가하기 전에 잠깐 이런 것들을 확인해 보세요.

  1. 다운로드 수. 인기 있는 크레이트는 다운로드가 수백만 번이에요. 그만큼 많은 사람이 나보다 먼저 버그를 찾아서 고쳤다는 뜻이에요.
  2. 최근 업데이트. 최신 릴리스 날짜를 보세요. 몇 년째 바뀌지 않은 크레이트도 괜찮을 수 있지만(그냥 “완성된” 것도 있어요), 저장소에 답이 없는 이슈가 잔뜩 쌓여 있지 않은지 확인하세요.
  3. 문서. docs.rs 페이지를 열어 보세요. 좋은 크레이트는 첫 페이지가 깔끔하고, 복사해서 쓸 수 있는 예제가 있어요.
  4. 1.0 이상의 버전. 꼭 필요하진 않지만, 작성자가 안정적인 API를 약속한다는 신호예요.
  5. 적은 의존성. 크레이트를 하나 추가하면 그 크레이트의 의존성도 함께 따라와요. 작을수록 믿기 쉬워요.

잘 모르겠을 때는 “rust”와 해결하려는 문제를 함께 검색해서, 커뮤니티가 반복해서 추천하는 크레이트가 무엇인지 보세요.

외울 필요는 없어요. 이 표를 가까이 두고 쓰다 보면 저절로 익혀져요.

명령어 하는 일
cargo new my_app 새 바이너리 프로젝트 만들기(라이브러리는 --lib 추가)
cargo build target/debug/에 컴파일하기
cargo run 컴파일하고 실행하기(인자는 -- 뒤에: cargo run -- input.txt)
cargo check 프로그램을 만들지 않고 에러만 검사하기. build보다 훨씬 빨라요
cargo test 테스트 실행하기(Rust 테스트 참고)
cargo fmt 모든 코드를 표준 스타일로 정리하기
cargo clippy 린터 실행하기. 더 간단하고 Rust다운 코드를 제안해 줘요
cargo doc --open 내 프로젝트와 모든 의존성의 HTML 문서를 만들고 열기
cargo update Cargo.lock을 호환되는 최신 버전으로 업데이트하기
cargo build --release target/release/에 최적화된 프로그램 컴파일하기

좋은 습관 하나: 코드를 쓰는 동안 cargo check를 자주 실행하고, 커밋하기 전에 cargo fmt와 cargo clippy를 돌리세요. cargo doc --open도 숨은 보석이에요. 내가 쓰는 바로 그 버전의 크레이트 문서를 보여 주고, 오프라인에서도 돼요.

Cargo는 기본으로 디버그 빌드를 만들어요. 컴파일이 빠르고, 정수 오버플로가 나면 패닉을 일으키는 것 같은 추가 안전 검사도 남아 있어요. 개발하는 동안에는 이게 딱 좋아요.

릴리스 빌드(cargo build --release 또는 cargo run --release)는 컴파일이 더 오래 걸리지만 훨씬 빠르게 실행돼요. 10배 이상 빠른 경우도 흔해요. 프로그램을 배포하거나 속도를 잴 때 쓰세요.

코드에서 지금 어떤 빌드인지 알아낼 수도 있어요.

fn main() {
if cfg!(debug_assertions) {
println!("디버그 빌드: 추가 검사가 켜져 있어요");
} else {
println!("릴리스 빌드: 속도에 맞게 최적화됐어요");
}
}

실행 결과:

디버그 빌드: 추가 검사가 켜져 있어요
▶ 직접 실행해 보기

Playground에서는 메뉴에서 “Debug”를 “Release”로 바꾼 다음 다시 실행하면 다른 줄이 나오는 걸 볼 수 있어요.

바이너리 크레이트와 라이브러리 크레이트 함께 쓰기

섹션 제목: “바이너리 크레이트와 라이브러리 크레이트 함께 쓰기”

패키지 하나에 라이브러리와 바이너리를 동시에 둘 수 있어요. 아주 흔한 구성이에요. 실제 로직은 src/lib.rs에 두고, src/main.rs는 작게 유지하면서 라이브러리를 부르기만 해요.

greeter/
├── Cargo.toml
└── src/
├── lib.rs # 라이브러리 크레이트: 재사용할 수 있는 로직
└── main.rs # 바이너리 크레이트: 실행하는 프로그램
src/lib.rs
pub fn greet(name: &str) -> String {
format!("안녕하세요, {name}님!")
}
src/main.rs
fn main() {
// 라이브러리 이름은 패키지 이름을 따라가요
println!("{}", greeter::greet("Ferris"));
}

왜 이렇게 나눌까요? lib.rs의 코드는 테스트하기 쉽고, 다른 프로그램(또는 두 번째 바이너리)에서 다시 쓸 수 있어요. Playground에는 파일이 하나뿐이지만, Rust 모듈 페이지에서처럼 pub mod를 쓰면 비슷한 느낌을 낼 수 있어요.

mod greeter {
pub fn greet(name: &str) -> String {
format!("안녕하세요, {name}님!")
}
}
fn main() {
println!("{}", greeter::greet("Ferris"));
}

실행 결과:

안녕하세요, Ferris님!
▶ 직접 실행해 보기

프로젝트가 커져서 함께 묶이는 패키지가 여러 개가 되면(예를 들어 명령줄 앱, 웹 서버, 공용 라이브러리), 하나의 워크스페이스(workspace)에 넣을 수 있어요. 워크스페이스 안의 패키지들은 Cargo.lock 하나와 target/ 폴더 하나를 함께 써요. 그래서 모두 같이 빌드되고 같은 버전의 의존성을 써요.

맨 위의 Cargo.toml에 멤버를 적어요.

[workspace]
members = ["app", "core"]
resolver = "3"
my_project/
├── Cargo.toml # 위의 워크스페이스 파일
├── Cargo.lock # 모든 멤버가 함께 써요
├── app/ # 바이너리 패키지
└── core/ # 라이브러리 패키지

그다음 맨 위에서 cargo build를 하면 전부 빌드되고, cargo run -p app으로 멤버 하나를 실행해요. 처음 만드는 프로젝트에서는 필요 없지만, 다른 사람의 코드를 읽을 때 알아볼 수 있으면 좋아요.

크레이트를 추가하지 않고 쓰기. use serde_json::json;이라고 썼는데 cargo add serde_json을 깜빡했다면, 컴파일러가 크레이트를 찾지 못해요.

error[E0432]: unresolved import `serde_json`
--> src/main.rs:1:5
|
1 | use serde_json::json;
| ^^^^^^^^^^ use of unresolved module or unlinked crate `serde_json`
|
= help: you might be missing a crate named `serde_json`

“serde_json을 가져올 수 없고, 그런 이름의 크레이트가 빠진 것 같다”는 뜻이에요. cargo add serde_json을 실행하면 해결돼요. 철자도 확인하세요. 크레이트 이름에는 -나 _를 쓰는데, 코드에서는 -가 항상 _로 바뀌어요.

기능(feature) 켜는 걸 잊기. #[derive(Serialize)]에서 derive 매크로를 찾을 수 없다는 에러가 나면, 아마 --features derive 없이 serde를 추가했을 거예요. cargo add serde --features derive를 다시 실행하세요. 기존 줄을 업데이트해 줘요.

디버그 빌드로 속도 재기. 프로그램이 느리게 느껴지면, 코드를 고치기 전에 먼저 cargo run --release로 실행해 보세요.

1. Playground에서 크레이트 써 보기

섹션 제목: “1. Playground에서 크레이트 써 보기”

serde_json의 json! 매크로로 크레이트 하나를 설명하는 값을 만드세요. 키는 downloads(숫자), name, version 세 개예요. 값 전체를 출력한 다음, 버전만 따로 출력하세요.

힌트: cargo add로 의존성 추가하기의 crab 예제 모양을 그대로 따라 하세요. 내 컴퓨터에서는 먼저 cargo add serde_json을 실행하세요. Playground에는 이미 들어 있어요.

정답 보기
use serde_json::json;
fn main() {
let krate = json!({
"downloads": 500,
"name": "serde_json",
"version": "1.0"
});
println!("{}", krate);
println!("버전: {}", krate["version"]);
}

실행 결과:

{"downloads":500,"name":"serde_json","version":"1.0"}
버전: "1.0"
▶ 직접 실행해 보기

변수 이름이 krate인 건 crate가 Rust의 예약어이기 때문이에요. Rust 코드에서 흔히 보는 말장난 같은 철자예요. 앞에서처럼 버전이 따옴표와 함께 출력되는 건 아직 JSON 값이기 때문이에요. 평범한 텍스트로 꺼내는 방법은 Serde로 JSON 다루기 페이지에서 볼 수 있어요.

2. 디버그 빌드에서만 나오는 메시지

섹션 제목: “2. 디버그 빌드에서만 나오는 메시지”

[debug] 뒤에 메시지를 붙여 출력하는 함수 debug_log(msg: &str)를 만드세요. 단, 디버그 빌드에서만 출력해야 해요. main에서 간단한 계산 앞뒤로 이 함수를 쓰고, 결과는 평소처럼 출력하세요.

힌트: 디버그 빌드와 릴리스 빌드에서처럼 println!을 if cfg!(debug_assertions) { ... }로 감싸세요.

정답 보기
fn debug_log(msg: &str) {
if cfg!(debug_assertions) {
println!("[debug] {msg}");
}
}
fn main() {
debug_log("계산을 시작해요");
let mut total = 0;
for n in 1..=10 {
total += n;
}
debug_log("계산이 끝났어요");
println!("1부터 10까지의 합: {total}");
}

실행 결과:

[debug] 계산을 시작해요
[debug] 계산이 끝났어요
1부터 10까지의 합: 55
▶ 직접 실행해 보기

Playground를 “Release”로 바꾸거나 내 컴퓨터에서 cargo run --release로 다시 실행해 보세요. 마지막 줄만 나와요. 실제 프로젝트라면 로깅 페이지에서 debug, info 같은 레벨로 이 일을 제대로 해 주는 크레이트를 소개해요.

  • Cargo는 코드를 빌드하고, 실행하고, 테스트하고, 정리해 줘요. 그리고 crates.io에서 크레이트를 내려받아요.
  • Cargo.toml은 내가 원하는 것, Cargo.lock은 실제로 받은 정확한 버전의 기록이에요. 둘 다 커밋하세요.
  • cargo add 이름 --features x로 의존성을 추가해요. "1.0"은 “호환되는 1.x 버전이면 뭐든”이라는 뜻이에요.
  • 크레이트는 다운로드 수, 최근 업데이트, docs.rs의 좋은 문서로 판단하세요.
  • 코드를 쓸 때는 cargo check, 커밋 전에는 cargo fmt와 cargo clippy, 속도가 필요하면 --release를 쓰세요.
  • 재사용할 로직은 src/lib.rs에 두고, src/main.rs는 작게 유지하세요.

이해도 점검

1. Cargo.toml에서 serde_json = "1.0"은 무슨 뜻일까요?

2. 빌드에 쓰인 모든 의존성의 정확한 버전을 기록하는 파일은 무엇일까요?

3. 프로그램을 만들지 않고 최대한 빠르게 코드의 에러만 확인하고 싶어요. 어떤 명령어가 가장 알맞을까요?

4. derive 기능을 켠 채로 serde를 추가하려면 어떻게 할까요?

점수: 0 / 4