Rust · 기본
소유권부터 배우는 Rust
Rust 시작하기 - cargo 와 첫 빌드
rustup·cargo 설치 확인, cargo new/run/build/check, 크레이트 구조, println! 매크로
개발자KR · 원고 갱신
이 장에서 배우는 것
이 책은 Rust의 핵심인 소유권을 중심으로 언어를 배운다. 소유권을 다루려면 먼저 코드를 쓰고, 컴파일하고, 실행하는 반복 작업이 손에 익어야 한다. 이 장에서는 Rust 도구 모음이 제대로 설치되어 있는지 확인하고, 명령줄 가계부 프로젝트의 뼈대를 만든다. 그 뼈대에서 첫 프로그램을 빌드하고 실행한다. 이후 장에서는 이 프로젝트에 수입·지출 기록과 분류별 집계 기능을 하나씩 붙인다.
- rustup, rustc, cargo가 각각 무엇이고 설치를 어떻게 확인하는지 설명할 수 있다.
cargo new가 만드는 파일과 폴더의 역할을 구분할 수 있다.cargo check,cargo build,cargo run의 차이를 알고 상황에 맞게 고를 수 있다.println!으로 값을 끼워 넣은 문장을 출력하고, 자주 나는 형식 오류를 읽어 고칠 수 있다.
문제 상황
가계부를 스프레드시트로 관리하던 사람이 명령줄에서 돌아가는 작은 도구를 직접 만들기로 했다고 하자. 다른 언어로 변수, 조건문, 반복문, 함수는 써 본 경험이 있다. 그런데 Rust 첫 화면에서 막히는 지점은 언어 문법이 아니라 도구 쪽에서 나오는 경우가 많다.
- 설치는 했는데 터미널에서
cargo를 찾지 못한다. - 소스 파일 하나를
rustc로 직접 컴파일했더니, 나중에 프로젝트가 커졌을 때 어떻게 옮겨야 할지 모르겠다. - 코드를 조금 고칠 때마다 실행 파일까지 만들면서 기다리는 시간이 아깝다.
- 화면에 출력하는
println!이 함수 같은데 왜 느낌표가 붙는지 알 수 없다.
이 장은 위 네 가지를 차례로 풀어 가면서, 지출 세 건과 수입 한 건을 표 모양으로 찍어 주는 첫 가계부 프로그램을 완성한다. 아직 데이터를 저장하거나 합계를 계산하지는 않는다. 값을 담는 방법은 다음 장에서 다룬다.
도구 모음 확인하기
rustup, rustc, cargo
Rust를 설치하면 이름이 비슷한 도구가 세 개 생긴다. 역할이 다르므로 처음에 구분해 두면 이후 오류 메시지를 읽기 쉽다.
| 도구 | 역할 | 직접 치는 빈도 | 예 |
|---|---|---|---|
| rustup | 컴파일러와 도구의 버전을 설치하고 갱신한다 | 가끔 | rustup update |
| rustc | 소스 코드를 기계어로 바꾸는 컴파일러다 | 거의 없음 | rustc --version |
| cargo | 프로젝트 생성, 빌드, 실행을 맡는 관리 도구다 | 매일 | cargo run |
실제 작업에서는 rustc를 직접 호출하지 않고 cargo가 대신 호출한다. 설치 방법은 Rust 공식 설치 안내에 최신 절차가 있으므로 그쪽을 따르면 된다. macOS와 Linux 모두 rustup을 설치하면 나머지가 함께 들어온다. 설치 뒤에는 터미널을 새로 열어야 실행 경로(PATH)가 반영된다.
설치가 되었는지는 세 명령으로 확인한다.
rustup --version
rustc --version
cargo --version
각 명령은 도구 이름과 버전 번호를 한 줄로 출력한다. 이 책의 예제는 Rust 1.85 이상을 전제로 한다. 버전 숫자가 1.85보다 작으면 rustup update를 실행해 갱신한다. 1.85는 edition 2024를 처음 지원한 버전이다. edition(에디션)은 몇 년에 한 번 언어에 넣는 호환성 단위이며, 새 프로젝트는 기본으로 가장 최신 에디션을 쓴다.
cargo 프로젝트의 구조
cargo new로 뼈대 만들기
가계부 프로젝트를 gagyebu라는 이름으로 만든다. 폴더 이름에 한글이나 공백은 피하는 편이 안전하다.
cargo new gagyebu
cd gagyebu
첫 명령은 Creating binary (application) `gagyebu` package와 같은 안내를 출력하고, 폴더 안에 다음을 만든다.
| 경로 | 내용 | 직접 고치는가 |
|---|---|---|
Cargo.toml | 패키지 이름, 버전, 에디션, 의존성을 적는 설정 파일 | 예 |
src/main.rs | 프로그램 시작점이 들어 있는 소스 파일 | 예 |
.gitignore | 버전 관리에서 제외할 대상 목록(/target) | 가끔 |
target/ | 빌드 결과물이 쌓이는 폴더. 처음 빌드할 때 생긴다 | 아니오 |
Cargo.lock | 실제로 쓴 의존성 버전 기록. 처음 빌드할 때 생긴다 | 아니오 |
이미 git 저장소 안에서 만드는 경우가 아니라면 cargo는 새 git 저장소도 함께 초기화한다.
생성된 Cargo.toml은 다음과 같다.
[package]
name = "gagyebu"
version = "0.1.0"
edition = "2024"
[dependencies]
[dependencies] 아래는 비어 있다. 이 책은 표준 라이브러리만 쓰므로 끝까지 비워 둔다.
패키지와 크레이트
Rust에서는 컴파일러가 한 번에 처리하는 단위를 크레이트(crate)라고 부른다. Cargo.toml 하나로 묶인 프로젝트 전체는 패키지라고 한다. src/main.rs가 있으면 cargo는 그 파일을 시작점으로 삼아 실행 파일 크레이트를 만든다. 이 파일을 크레이트 루트라고 한다. 다른 프로그램에 가져다 쓰는 라이브러리 크레이트는 src/lib.rs가 시작점이 된다. 이 책은 실행 파일 크레이트만 다룬다. 파일을 여러 개로 나누는 방법은 모듈을 다루는 장에서 설명한다.
check, build, run
cargo에는 소스를 어디까지 처리하느냐가 다른 세 명령이 있다.
cargo check는 문법과 타입 등 컴파일러의 검사만 수행한다. 실행 파일은 만들지 않으므로 가장 빠르다.cargo build는 검사에 더해 기계어 생성까지 끝내target/debug/아래에 실행 파일을 만든다.cargo run은 필요하면 빌드한 다음 그 실행 파일을 바로 실행한다.
세 명령은 앞의 것이 뒤의 것에 포함되는 관계다. 코드를 고치는 중에는 cargo check로 오류만 빠르게 확인하고, 동작을 눈으로 볼 때 cargo run을 쓴다. Rust는 컴파일러가 소유권 규칙까지 검사하기 때문에, 이 책을 따라가는 동안 cargo check를 자주 쓰게 된다.
옵션 없이 cargo build를 하면 디버그 프로필로 빌드한다. 컴파일이 빠르고 디버깅 정보를 담지만 실행 속도는 느리다. 배포용으로 최적화하려면 cargo build --release를 쓰며 결과는 target/release/에 생긴다. 이 책의 예제는 모두 디버그 빌드로 충분하다.
println! 매크로
기본 생성된 src/main.rs의 핵심은 fn main() 함수와 println! 한 줄이다. main은 프로그램이 시작될 때 가장 먼저 실행되는 함수이고, 중괄호 안이 본문이다. 문장 끝에는 세미콜론을 붙이고, 들여쓰기는 공백 네 칸이 관례다. 관례대로 정리하고 싶으면 cargo fmt를 실행하면 된다.
println!의 이름 끝에 붙은 느낌표는 이것이 함수가 아니라 매크로(macro)라는 표시다. 매크로는 컴파일하는 동안 다른 코드로 펼쳐지는 도구다. 함수와 달리 인자 개수가 정해져 있지 않고, 첫 번째 인자인 형식 문자열을 컴파일러가 직접 읽어서 나머지 인자와 짝이 맞는지 검사한다. 그래서 실행해 보기 전에 형식 오류를 잡아낸다. 이 책에서 지금 필요한 규칙은 다음 정도다.
| 쓰는 형태 | 의미 | 예시 코드 | 출력 |
|---|---|---|---|
{} | 다음 인자를 사람이 읽는 형태로 끼운다 | println!("{}원", 500) | 500원 |
{:>7} | 폭 7칸에 오른쪽 정렬로 끼운다 | println!("{:>7}", 2500) | 2500 |
{:?} | 디버그용 표기로 끼운다 | println!("{:?}", "식비") | "식비" |
{{, }} | 중괄호 문자 자체를 출력한다 | println!("{{}}") | {} |
줄바꿈 없이 이어서 출력하려면 print!를, 오류 메시지처럼 표준 오류 스트림으로 보내려면 eprintln!을 쓴다. 가계부처럼 사람이 읽는 표를 찍을 때는 {}와 폭 지정만으로 충분하다. 폭 지정은 글자 수를 세기 때문에 한글처럼 화면에서 두 칸을 차지하는 글자가 섞이면 칸이 어긋난다. 예제에서는 숫자 열에만 폭을 준다.
완성 코드
다음 내용을 src/main.rs에 덮어쓴다. 표 머리와 거래 네 건을 찍는 프로그램이다.
fn main() {
println!("== 가계부 v0.1 ==");
println!("날짜 | 분류 | 금액");
println!("{} | {} | {:>7}", "2025-03-01", "식비", 12000);
println!("{} | {} | {:>7}", "2025-03-02", "교통", 2500);
println!("{} | {} | {:>7}", "2025-03-05", "월급", 3000000);
println!("금액 단위: {{원}}");
}
줄별 해설
fn main() {은main이라는 이름의 함수를 정의한다. 매개변수가 없고 반환값도 없다. 프로그램은 여기서 시작하고, 함수가 끝나면 프로그램도 정상 종료한다.println!("== 가계부 v0.1 ==");는 형식 문자열에{}가 없으므로 문자열을 그대로 한 줄 출력한다. 소스 파일은 UTF-8이므로 한글을 그대로 쓸 수 있다.println!("날짜 | 분류 | 금액");은 표 머리다.|는 특별한 뜻이 없는 일반 문자다.- 거래 세 줄은 형식 문자열에 자리표시자 세 개가 있고 인자도 세 개다. 앞의
{}두 개에는 날짜와 분류 문자열이, 마지막{:>7}에는 정수가 들어간다. 정수는 7칸 폭에 오른쪽 정렬되므로 자릿수가 달라도 오른쪽 끝이 맞는다. 12000은 앞에 공백 두 칸, 2500은 세 칸이 붙고, 7자리인 3000000은 공백이 붙지 않는다. println!("금액 단위: {{원}}");은 겹 중괄호가 중괄호 한 글자로 출력되는 것을 보여 준다. 자리표시자로 해석되지 않으므로 인자가 필요 없다.- 마지막
}가main을 닫는다. 모든println!문장은 세미콜론으로 끝난다.
프로그램은 표준 라이브러리 기능만 쓰고 사용하지 않는 값도 없으므로 경고 없이 컴파일된다.
실행 결과
먼저 오류만 확인해 본다. 문제가 없으면 컴파일 진행 안내만 나오고 끝난다.
cargo check
이어서 실행한다. -q는 cargo의 진행 안내를 생략하고 프로그램의 출력만 보여 주는 옵션이다.
cargo run -q
== 가계부 v0.1 ==
날짜 | 분류 | 금액
2025-03-01 | 식비 | 12000
2025-03-02 | 교통 | 2500
2025-03-05 | 월급 | 3000000
금액 단위: {원}
-q 없이 처음 실행하면 컴파일 안내가 먼저 나온다. 경로와 소요 시간은 환경마다 다르다.
cargo run
Compiling gagyebu v0.1.0 (/…/gagyebu)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.4s
Running `target/debug/gagyebu`
== 가계부 v0.1 ==
…
같은 명령을 소스 수정 없이 다시 실행하면 Compiling 줄이 없이 바로 Running이 나온다. cargo가 바뀐 부분이 없음을 알고 이전 빌드 결과를 재사용하기 때문이다.
실무에서 자주 틀리는 것
1. 매크로 이름에서 느낌표를 빼먹는다
다른 언어의 출력 함수 습관대로 쓰면 다음과 같이 된다.
fn main() {
println("2025-03-01");
}
컴파일러는 error[E0423]: expected function, found macro `println`이라고 알리고, 도움말로 use `!` to invoke the macro를 붙여 준다. 함수가 아니라 매크로를 찾았다는 뜻이다. 고친 코드는 다음과 같다.
fn main() {
println!("2025-03-01");
}
2. 자리표시자와 인자 개수가 맞지 않는다
fn main() {
println!("{} | {}", "식비");
}
오류의 요지는 2 positional arguments in format string, but there is 1 argument다. 형식 문자열에는 자리가 두 개인데 넘긴 인자가 하나라는 뜻이다. 실행 중에 뒤늦게 터지는 것이 아니라 컴파일 단계에서 막힌다. 인자를 채우거나 자리를 줄인다.
fn main() {
println!("{} | {}", "식비", 12000);
}
3. 프로젝트 폴더 밖에서 cargo를 실행한다
홈 디렉터리처럼 Cargo.toml이 없는 곳에서 cargo run을 실행하면 다음 오류가 난다.
error: could not find `Cargo.toml` in `/Users/me` or any parent directory
cargo는 현재 폴더에서 시작해 위쪽 폴더로 올라가며 Cargo.toml을 찾는다. 찾지 못했다는 뜻이므로 cd gagyebu로 프로젝트 안에 들어간 뒤 다시 실행한다. 프로젝트 안이라면 src 같은 하위 폴더에서 실행해도 동작한다.
4. 중괄호를 그대로 출력하려고 한 겹만 쓴다
fn main() {
println!("금액 단위: {원}");
}
Rust는 {원}을 이름이 원인 변수를 끼우라는 뜻으로 읽는다. 그런 변수가 없으므로 error[E0425]: cannot find value `원` in this scope가 나온다. 중괄호 문자를 출력하려면 두 겹으로 쓴다.
fn main() {
println!("금액 단위: {{원}}");
}
한눈에 보기
| 명령 | 하는 일 | 실행 파일 생성 | 쓰는 때 |
|---|---|---|---|
cargo new 이름 | 새 패키지 뼈대를 만든다 | 아니오 | 프로젝트를 시작할 때 |
cargo check | 컴파일러 검사만 한다 | 아니오 | 코드를 고치는 중에 오류만 볼 때 |
cargo build | target/debug/에 빌드한다 | 예 | 실행 파일이 필요할 때 |
cargo run | 빌드한 뒤 바로 실행한다 | 예 | 동작을 확인할 때 |
cargo build --release | 최적화해 target/release/에 빌드한다 | 예 | 배포용 파일이 필요할 때 |
println!은 {}로 값을 끼우고, {:>7}로 폭을 정하며, {{ }}로 중괄호를 출력한다. 자리와 인자의 수는 컴파일 단계에서 검사된다.
연습 문제
household라는 이름으로 새 프로젝트를 만들고 그 폴더로 들어가는 명령 두 줄을 쓰시오. 그리고 프로그램 시작점이 들어 있는 파일의 경로를 답하시오.- 완성 코드에
2025-03-07,문구, 4800원 거래를월급줄 바로 다음에 추가하는println!한 줄을 쓰고, 그 줄의 출력을 적으시오. - 다음 코드는 컴파일되지 않는다. 오류의 요지를 설명하고 고치시오.
fn main() { println!("합계: {}원"); } - 다음 세 상황에서
cargo check,cargo build,cargo run중 무엇이 알맞은지 고르시오. (가) 줄을 여러 개 고친 뒤 오류가 없는지만 빨리 보고 싶다. (나) 고친 결과가 화면에 어떻게 찍히는지 보고 싶다. (다) 실행 파일 자체를 만들어 다른 폴더로 복사하고 싶다.
정답과 해설
- 명령은
cargo new household와cd household다. 시작점 파일은src/main.rs이며, 프로젝트 폴더 기준의 상대 경로다. - 다음 한 줄을 추가한다.
출력은println!("{} | {} | {:>7}", "2025-03-07", "문구", 4800);2025-03-07 | 문구 | 4800이다. 4800은 네 자리이므로 폭 7칸을 채우려고 앞에 공백 세 칸이 붙고, 형식 문자열의|뒤 공백 한 칸과 합쳐|뒤에 공백이 네 칸 나온다. - 오류의 요지는
1 positional argument in format string, but no arguments were given이다. 자리표시자가 하나 있는데 채울 인자가 없다는 뜻이다. 예를 들어println!("합계: {}원", 17500);처럼 인자를 넘기면 된다. 값 없이 글자로{}를 출력하려던 것이라면{{}}로 쓴다. - (가)
cargo check. 실행 파일을 만들지 않아 가장 빠르다. (나)cargo run. 빌드와 실행을 한 번에 한다. (다)cargo build. 실행 파일은target/debug/에 생기며, 배포용이라면--release를 붙여target/release/의 파일을 복사한다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.