본문 바로가기

추리 게임

숫자 추리 게임을 만들며 변수·입력·Result·match를 사용하고 rand 크레이트 의존성을 연결합니다.

이제 작은 프로그램을 하나 만들며 러스트의 여러 기능을 처음 써 봅니다. 만들 것은 입문용으로 자주 쓰이는 숫자 추리 게임입니다. 프로그램이 1부터 100 사이의 정수 하나를 무작위로 고르면, 플레이어가 추리한 숫자를 입력하고, 프로그램은 그 숫자가 정답보다 작은지 큰지 알려 줍니다. 정답을 맞히면 축하 메시지를 출력하고 끝납니다.

이 과정에서 let과 match, 메서드, 연관 함수(associated function), 외부 크레이트를 쓰게 됩니다. 여기서는 각 기능을 게임에 필요한 만큼만 설명하고, 자세한 내용은 뒤의 장에서 다시 다룹니다. 아래 그림은 이 글에서 완성할 한 번의 추리가 입력에서 비교 결과까지 어떻게 흘러가는지 보여 줍니다.

한 번의 추릿값이 비교 결과가 되는 흐름

한 String 입력의 read_line Result→trim/parse Result→u32 cmp Ordering 한 결과라는 타입/실패 경계이다. 반복을 추가하지 않는다.

사용자가 한 번 입력한 문자열이 read_line의 Result 처리, trim과 숫자 parse를 거쳐 cmp와 match의 Less, Equal, Greater 결과로 나뉘는 흐름. 이 다이어그램은 반복, continue, break를 포함하지 않습니다.

Rust · input · Result · flowchart

한 번의 입력은 두 실패 경계를 지나 비교 결과가 된다

입력은 곧바로 숫자가 되지 않습니다. read_line의 I/O Result를 처리하고, 양끝 공백을 제거한 뒤 u32로 parse해야 비밀 숫자와 비교할 수 있습니다. 이 문맥의 실패 정책은 expect로 종료하는 것입니다.

한 번의 String 입력에서 두 Result와 세 비교 결과로 분기원입력String→readline→I/OResult에서Errpanic/Oktrimparse이다.parseErr는동일panic종료로향하고Ok만cmp에들어간다.cmp는Less/Equal/Greater각터미널로한갈래실행된다.각종류는동시에실행하는순서가아니고반복간선없다.EOF전용정상종료는이원단일입력프로그램의범위가아니다.빈 Stringlet mut guess입력 추가read_line(&mut guess)읽기 ResultOk(bytes) / Errexpect → panicI/O 또는 변환 실패양끝 공백 제거trim().parse::<u32>()변환 ResultOk(num) / Err비밀 숫자와 비교cmp → matchLessToo small!EqualYou win!GreaterToo big!ErrOkErrOk(num)LessEqualGreater입력과 변환의 오류는 다름한 번의 비교 · 반복 없음
단일 추릿값 입력과 비교 흐름 빈 String에 한 번 입력을 받고 read_line이 반환한 Result가 Err이면 expect로 종료합니다. Ok이면 trim과 parse를 거쳐 u32로 바꾸고, parse 실패 역시 expect로 종료합니다. 성공한 숫자는 cmp와 match를 거쳐 Less, Equal, Greater 출력 중 하나로 이어집니다. ErrErrOkOk(num)추릿값 한 번 입력let mut guess= String::new()표준 입력을 String에 추가read_line(&mut guess)read_line의 ResultOk(bytes) · Errexpect → panic입력·변환 실패현재 예제는 종료양끝 공백 제거 · 숫자 변환trim().parse::<u32>()parse의 ResultOk(num) · Err비밀 숫자와 비교cmp → match OrderingLess · 작은 값Too small!Equal · 정답You win!Greater · 큰 값Too big!
input

1. 빈 String에 한 번 입력

String::new()으로 버퍼를 만들고 read_line(&mut guess)가 입력을 추가합니다.

I/O Result

2. 성공과 실패를 분리

Ok(bytes)면 계속하고, Err(io::Error)면 이 문맥의 expect 정책에 따라 종료합니다.

convert

3. trim().parse::<u32>()

양끝 공백을 제거하고 숫자로 변환합니다. 여기서 반환되는 Result도 현재 예제에서는 expect로 처리하므로 잘못된 입력은 종료됩니다.

compare

4. cmp와 match

Less는 Too small!, Equal은 You win!, Greater는 Too big! 한 갈래만 실행합니다.

두 Result의 오류가 다르다

read_line의 Err는 I/O 실패이고, parse의 Err는 문자열을 u32로 바꾸지 못한 실패입니다.

이 그림의 종료점

한 번의 입력과 비교까지만 다룹니다. 다음 문서가 설명하는 loop, continue, break 의미는 이 흐름에 포함하지 않습니다.

타입 경계는 String → &str → Result<u32> → u32 → Ordering 순서입니다. 각 단계의 반환 타입을 확인하면 compiler 진단이 어느 경계에서 발생했는지 좁힐 수 있습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명원 입력 String→readline→I/O Result에서 Err는 panic 종료, Ok는 trim·parse로 이어진다. parse Err는 동일한 panic 종료로 향하고 Ok만 cmp에 들어간다. cmp는 Less/Equal/Greater 각 종료점 중 한 갈래로 실행된다. 각 종류는 동시에 실행하는 순서가 아니고 반복 간선은 없다. EOF 전용 정상 종료는 이 원 단일 입력 프로그램의 범위가 아니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
입력 수신

String에 한 줄을 받아 Result를 확인한다.

  • 소유 값: 입력 String
  • 경계: read_line Result
  • 입력 실패는 숫자 비교까지 진행하지 않는다.
숫자 변환

trim 뒤 u32 parse 결과를 판단한다.

  • 변환: trim → parse::<u32>
  • 실패 경계: parse Result
  • 문자열과 숫자는 다른 타입이며 파싱 성공이 필요하다.
비교 결과

성공한 숫자를 secret과 비교한다.

  • 출력 타입: Ordering
  • 결과: Less/Greater/Equal 중 하나
  • 이 도표의 단발 예에 반복을 추가하지 않는다.

한 번의 추릿값이 비교 결과가 되는 흐름

한 String 입력의 read_line Result→trim/parse Result→u32 cmp Ordering 한 결과라는 타입/실패 경계이다. 반복을 추가하지 않는다.

사용자가 한 번 입력한 문자열이 read_line의 Result 처리, trim과 숫자 parse를 거쳐 cmp와 match의 Less, Equal, Greater 결과로 나뉘는 흐름. 이 다이어그램은 반복, continue, break를 포함하지 않습니다.

Rust · input · Result · flowchart

한 번의 입력은 두 실패 경계를 지나 비교 결과가 된다

입력은 곧바로 숫자가 되지 않습니다. read_line의 I/O Result를 처리하고, 양끝 공백을 제거한 뒤 u32로 parse해야 비밀 숫자와 비교할 수 있습니다. 이 문맥의 실패 정책은 expect로 종료하는 것입니다.

한 번의 String 입력에서 두 Result와 세 비교 결과로 분기원입력String→readline→I/OResult에서Errpanic/Oktrimparse이다.parseErr는동일panic종료로향하고Ok만cmp에들어간다.cmp는Less/Equal/Greater각터미널로한갈래실행된다.각종류는동시에실행하는순서가아니고반복간선없다.EOF전용정상종료는이원단일입력프로그램의범위가아니다.빈 Stringlet mut guess입력 추가read_line(&mut guess)읽기 ResultOk(bytes) / Errexpect → panicI/O 또는 변환 실패양끝 공백 제거trim().parse::<u32>()변환 ResultOk(num) / Err비밀 숫자와 비교cmp → matchLessToo small!EqualYou win!GreaterToo big!ErrOkErrOk(num)LessEqualGreater입력과 변환의 오류는 다름한 번의 비교 · 반복 없음
단일 추릿값 입력과 비교 흐름 빈 String에 한 번 입력을 받고 read_line이 반환한 Result가 Err이면 expect로 종료합니다. Ok이면 trim과 parse를 거쳐 u32로 바꾸고, parse 실패 역시 expect로 종료합니다. 성공한 숫자는 cmp와 match를 거쳐 Less, Equal, Greater 출력 중 하나로 이어집니다. ErrErrOkOk(num)추릿값 한 번 입력let mut guess= String::new()표준 입력을 String에 추가read_line(&mut guess)read_line의 ResultOk(bytes) · Errexpect → panic입력·변환 실패현재 예제는 종료양끝 공백 제거 · 숫자 변환trim().parse::<u32>()parse의 ResultOk(num) · Err비밀 숫자와 비교cmp → match OrderingLess · 작은 값Too small!Equal · 정답You win!Greater · 큰 값Too big!
input

1. 빈 String에 한 번 입력

String::new()으로 버퍼를 만들고 read_line(&mut guess)가 입력을 추가합니다.

I/O Result

2. 성공과 실패를 분리

Ok(bytes)면 계속하고, Err(io::Error)면 이 문맥의 expect 정책에 따라 종료합니다.

convert

3. trim().parse::<u32>()

양끝 공백을 제거하고 숫자로 변환합니다. 여기서 반환되는 Result도 현재 예제에서는 expect로 처리하므로 잘못된 입력은 종료됩니다.

compare

4. cmp와 match

Less는 Too small!, Equal은 You win!, Greater는 Too big! 한 갈래만 실행합니다.

두 Result의 오류가 다르다

read_line의 Err는 I/O 실패이고, parse의 Err는 문자열을 u32로 바꾸지 못한 실패입니다.

이 그림의 종료점

한 번의 입력과 비교까지만 다룹니다. 다음 문서가 설명하는 loop, continue, break 의미는 이 흐름에 포함하지 않습니다.

타입 경계는 String → &str → Result<u32> → u32 → Ordering 순서입니다. 각 단계의 반환 타입을 확인하면 compiler 진단이 어느 경계에서 발생했는지 좁힐 수 있습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명원 입력 String→readline→I/O Result에서 Err는 panic 종료, Ok는 trim·parse로 이어진다. parse Err는 동일한 panic 종료로 향하고 Ok만 cmp에 들어간다. cmp는 Less/Equal/Greater 각 종료점 중 한 갈래로 실행된다. 각 종류는 동시에 실행하는 순서가 아니고 반복 간선은 없다. EOF 전용 정상 종료는 이 원 단일 입력 프로그램의 범위가 아니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
입력 수신

String에 한 줄을 받아 Result를 확인한다.

  • 소유 값: 입력 String
  • 경계: read_line Result
  • 입력 실패는 숫자 비교까지 진행하지 않는다.
숫자 변환

trim 뒤 u32 parse 결과를 판단한다.

  • 변환: trim → parse::<u32>
  • 실패 경계: parse Result
  • 문자열과 숫자는 다른 타입이며 파싱 성공이 필요하다.
비교 결과

성공한 숫자를 secret과 비교한다.

  • 출력 타입: Ordering
  • 결과: Less/Greater/Equal 중 하나
  • 이 도표의 단발 예에 반복을 추가하지 않는다.

사용자가 입력한 한 줄은 먼저 String에 담기고, 읽기에 실패했는지 확인하는 Result를 거칩니다. 그다음 앞뒤의 공백을 지우고 u32 숫자로 바꾸는데, 이 변환도 실패할 수 있으므로 또 한 번 Result를 확인합니다. 이 글에서는 두 실패 모두 expect로 프로그램을 끝내며, 변환에 성공한 숫자만 비밀 숫자와 비교해 작다, 크다, 같다 가운데 한 결과로 나뉩니다. 여러 번 추리하게 하는 반복과 잘못된 입력을 다시 받는 처리는 다음 글에서 더합니다.

새 프로젝트 만들기

Cargo 기초에서 만든 projects 폴더로 가서 새 프로젝트를 만듭니다. 명령은 앞 글의 hello_cargo를 만들 때와 같고 이름만 다릅니다. 이 이름이 패키지 이름이 되어 Cargo.toml의 name에 들어가며, 빌드한 실행 파일의 이름도 guessing_game이 됩니다.

cargo new guessing_game
cd guessing_game

cargo new는 첫 인수로 받은 이름의 폴더를 만들고, 그 안에 Cargo.toml과 Hello, world!를 출력하는 src/main.rs를 만듭니다. cargo run으로 컴파일하고 실행해 프로젝트가 제대로 만들어졌는지 확인합니다.

cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/guessing_game`
Hello, world!

이 게임처럼 조금씩 고치고 바로 실행해 보는 일을 되풀이할 때 cargo run이 편리합니다. 이제 src/main.rs를 열어 두세요. 이 글의 코드는 모두 이 파일에 씁니다.

추릿값 입력받기

먼저 플레이어에게 숫자를 입력받아 그대로 출력하는 부분을 만듭니다. src/main.rs의 내용을 다음 코드로 바꿉니다.

src/main.rs
use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

입력을 받고 출력하려면 표준 라이브러리 std의 입출력 모듈 io가 필요하므로, 첫 줄의 use std::io;로 io를 이 파일에서 쓸 수 있게 가져옵니다. 러스트는 모든 프로그램에 기본으로 가져오는 표준 라이브러리 항목의 묶음인 프렐류드(prelude)를 두는데, 프렐류드에 없는 것은 이처럼 use로 직접 가져와야 합니다.

fn main()은 프로그램의 시작점이고, 그 안의 두 println!은 게임 이름과 입력 안내를 출력합니다.

변수에 값 저장하기

다음 줄은 입력을 담을 변수를 만듭니다. 사용자가 입력하기 전에, 그 내용을 받아 둘 빈 문자열을 먼저 마련하는 것입니다. 이 한 줄에 let, mut, String::new() 세 가지가 들어 있으므로 차례로 봅니다.

    let mut guess = String::new();

let은 변수를 만드는 문장입니다. 예를 들어 let apples = 5;는 apples라는 변수를 만들고 5를 넣습니다. 러스트의 변수는 기본으로 불변(immutable)이어서 한 번 값을 넣으면 바꿀 수 없으며, 바꿀 수 있는 가변(mutable) 변수를 만들려면 이름 앞에 mut을 붙입니다. 불변과 가변은 변수와 가변성에서 자세히 다룹니다.

let apples = 5; // 불변
let mut bananas = 5; // 가변

//부터 그 줄 끝까지는 주석으로, 컴파일러가 무시합니다. 그래서 let mut guess는 바꿀 수 있는 변수 guess를 만들며, = 오른쪽의 String::new()가 돌려준 값이 그 처음 값이 됩니다. String은 표준 라이브러리가 제공하는 문자열 타입으로, UTF-8로 인코딩한 글자를 담고 길이를 늘릴 수 있습니다.

String::new의 ::는 new가 String 타입에 딸린 연관 함수라는 뜻입니다. 연관 함수는 어떤 값이 아니라 타입 자체에 구현된 함수이며, new는 빈 String을 새로 만들어 돌려줍니다. 여러 타입이 새 값을 만드는 연관 함수에 흔히 new라는 이름을 붙입니다. 정리하면 let mut guess = String::new();는 비어 있는 새 String을 담은 가변 변수를 만듭니다.

사용자 입력받기

io 모듈의 stdin 함수는 터미널의 표준 입력을 다루는 std::io::Stdin 값을 돌려줍니다. 첫 줄에서 use std::io;로 io를 가져왔으므로 io::stdin()이라고 쓸 수 있으며, 가져오지 않았다면 std::io::stdin()이라고 전체 경로를 적어야 합니다.

    io::stdin()
        .read_line(&mut guess)

그다음 줄의 .read_line(&mut guess)는 표준 입력에서 한 줄을 읽어 guess에 덧붙이는 메서드를 호출합니다. read_line은 기존 내용을 지우지 않고 뒤에 덧붙이므로, 읽은 내용을 담을 문자열을 인수로 받습니다. 그 문자열의 내용을 바꿔야 하므로 가변이어야 합니다.

&는 이 인수가 참조(reference)라는 뜻입니다. 참조를 쓰면 값을 메모리에 다시 복사하지 않고도 코드의 여러 곳에서 같은 데이터를 쓸 수 있습니다. 참조도 변수처럼 기본으로 불변이므로, 내용을 바꿀 수 있는 참조를 넘기려면 &guess가 아니라 &mut guess라고 씁니다. 참조는 러스트의 복잡한 기능 가운데 하나이며 참조와 대여에서 자세히 설명합니다.

Result 타입으로 잠재적 실패 다루기

세 번째 줄은 앞의 두 줄과 이어진 하나의 문장입니다. 앞 줄 끝에 세미콜론이 없으므로, 이 줄의 .expect는 read_line이 돌려준 값에 이어서 메서드를 부릅니다.

        .expect("Failed to read line");

io::stdin().read_line(&mut guess).expect("Failed to read line");처럼 한 줄에 써도 되지만, .메서드() 형태로 메서드를 이어 부를 때는 이렇게 줄을 나누어 읽기 쉽게 쓰는 경우가 많습니다.

read_line은 입력을 문자열에 넣는 일과 함께, 그 일이 성공했는지를 담은 Result 값을 돌려줍니다. Result는 열거형(enumeration, 줄여서 enum)으로, 여러 가능한 상태 가운데 하나를 갖는 타입이며 각 상태를 배리언트(variant)라고 부릅니다. Result의 배리언트는 성공을 뜻하는 Ok와 실패를 뜻하는 Err 두 가지이고, Ok에는 성공해서 만든 값이, Err에는 무엇이 왜 실패했는지에 대한 정보가 들어 있습니다. 열거형은 열거형 정의에서 자세히 다룹니다.

Result 값에도 메서드가 있습니다. expect는 값이 Err이면 프로그램을 멈추고 인수로 받은 메시지를 출력하며, 값이 Ok이면 그 안의 값을 꺼내 돌려줍니다. read_line의 경우 Ok 안에는 읽은 바이트 수가 들어 있습니다. 운영체제에서 입력을 읽지 못하는 경우처럼 read_line이 실패하면 이 프로그램은 Failed to read line과 함께 멈춥니다.

expect를 부르지 않으면 프로그램은 컴파일되지만, 컴파일러가 unused Result that must be used라는 경고를 냅니다. 돌려받은 Result를 쓰지 않아 생길 수 있는 오류를 처리하지 않았다는 뜻입니다. 경고를 없애는 올바른 방법은 오류를 처리하는 코드를 쓰는 것이지만, 이 게임에서는 읽기에 실패하면 그냥 멈추면 되므로 expect를 씁니다. 오류에서 복구하는 방법은 Result와 복구 가능한 오류에서 배웁니다.

println! 자리표시자로 값 출력하기

마지막 줄은 사용자가 입력한 문자열을 출력합니다. guess는 바로 앞에서 read_line이 채운 문자열이므로, 이 줄로 입력이 제대로 들어왔는지 확인할 수 있습니다.

    println!("You guessed: {guess}");

중괄호 {}는 자리표시자(placeholder)로, 그 자리에 값을 넣어 출력합니다. 변수의 값을 출력할 때는 중괄호 안에 변수 이름을 쓰고, 식을 계산한 결과를 출력할 때는 빈 중괄호를 둔 뒤 문자열 뒤에 쉼표로 구분해 식을 적습니다. 빈 중괄호와 식은 앞에서부터 차례로 짝지어집니다.

let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);
x = 5 and y + 2 = 12

{x}는 변수 x의 값 5로 채워지고, 빈 {}는 문자열 뒤의 첫 번째 식 y + 2를 계산한 12로 채워집니다. 중괄호 안에는 변수 이름만 쓸 수 있고 {y + 2}처럼 식을 쓸 수는 없으므로, 계산이 필요한 값은 이렇게 빈 중괄호와 따로 넘긴 식으로 출력합니다.

첫 부분 실행해 보기

cargo run으로 실행하고, 안내가 나오면 숫자를 입력한 뒤 Enter를 누릅니다. 아래 출력의 6은 직접 입력한 줄이며, Cargo가 출력하는 빌드 줄은 생략했습니다.

Guess the number!
Please input your guess.
6
You guessed: 6

입력한 줄의 끝에는 Enter로 입력한 줄바꿈 문자가 함께 들어 있어서, You guessed: 6 다음에는 빈 줄이 하나 더 출력됩니다. 키보드로 입력을 받아 출력하는 게임의 첫 부분이 완성되었습니다.

비밀 숫자 만들기

다음으로 플레이어가 맞힐 비밀 숫자를 만듭니다. 게임을 할 때마다 1부터 100 사이에서 새로 고르며, 따로 뽑으므로 이전 게임과 같은 숫자가 우연히 다시 나올 수도 있습니다. 러스트의 표준 라이브러리에는 아직 무작위 수를 만드는 기능이 없으므로, Rust Rand 프로젝트가 관리하는 rand 크레이트를 씁니다.

크레이트로 기능 더하기

크레이트는 러스트 소스 파일의 묶음입니다. 지금 만드는 프로젝트는 실행 파일을 만드는 바이너리 크레이트(binary crate)이고, rand는 혼자 실행되지 않고 다른 프로그램이 가져다 쓰는 라이브러리 크레이트(library crate)입니다. rand를 쓰려면 먼저 Cargo.toml의 [dependencies] 아래에 의존성으로 적습니다.

Cargo.toml
[dependencies]
rand = "0.10.2"

Cargo.toml에서 섹션 머리글 아래의 내용은 다음 머리글이 나올 때까지 그 섹션에 속하며, [dependencies]에는 프로젝트가 쓰는 외부 크레이트와 필요한 버전을 적습니다. Cargo는 버전 번호를 붙이는 표준 방식인 유의적 버전(Semantic Versioning, 줄여서 SemVer)을 이해합니다. "0.10.2"는 정확히 그 버전만 쓰라는 뜻이 아니라 ^0.10.2의 줄임말로, 0.10.2 이상이면서 0.11.0보다 낮은 버전을 허용합니다. 같은 0.10 버전대 안에서는 공개 API가 0.10.2와 호환된다고 보기 때문이며, 0.11.0 이상은 API가 바뀌었을 수 있으므로 자동으로 고르지 않습니다.

아래 그림처럼 Cargo.toml은 허용하는 범위만 적고, 실제로 어떤 버전을 쓸지는 Cargo가 정합니다.

Cargo 의존성 요구사항에서 API 사용까지

manifest rand 허용 범위→resolver 선택→Cargo.lock 정확한 기록→locked 빌드/API라는 산출물/선택 상태이다. lock만으로 동일 바이너리를 보장하지 않는다.

Cargo.toml의 rand 0.10.2 요구 범위를 resolver가 registry index와 기존 lockfile을 참고해 package graph로 해결하고, Cargo.lock에 정확한 선택을 기록한 뒤 cargo build와 rand random_range API 사용으로 이어지는 흐름

Rust · Cargo · dependency flow

manifest는 허용 범위를, lockfile은 선택 결과를 기록한다

rand = "0.10.2"는 정확한 버전 고정이 아니라 caret 요구 범위입니다. Cargo resolver가 registry와 다른 제약을 함께 계산하고, 실제로 선택한 package graph를 Cargo.lock에 남긴 뒤 그 API를 빌드합니다.

Cargo 요구 범위에서 잠긴 선택과 API 빌드까지원4노드3간선의manifest허용범위→resolver선택→lock결과→build/API를모바일에서그린다.0.10.2는caret범위이며0.11미만,lock은exactselection이다.동일바이너리에는toolchain,target,features,profile,nativeinput등이따로필요하다.
허용 범위 선언Cargo.tomlrand = "0.10.2">=0.10.2 · <0.11
연결 관계허용 범위 선언 · Cargo.toml · rand = "0.10.2" · >=0.10.2 · <0.11 → 의존성 해석 · Cargo resolver · manifest · lock · index · 하위 의존성 포함
의존성 해석Cargo resolvermanifest · lock · index하위 의존성 포함
연결 관계의존성 해석 · Cargo resolver · manifest · lock · index · 하위 의존성 포함 → 선택 결과 잠금 · Cargo.lock · 정확한 버전 · source · checksum / revision
선택 결과 잠금Cargo.lock정확한 버전 · sourcechecksum / revision
연결 관계선택 결과 잠금 · Cargo.lock · 정확한 버전 · source · checksum / revision → 빌드·API 사용 · cargo build --locked · rand::random_range · (1..=100)
빌드·API 사용cargo build --lockedrand::random_range(1..=100)

lock은 선택한 package의 기록동일 바이너리의 모든 조건 아님

Cargo 의존성 해결과 잠금 흐름 Cargo.toml의 rand 0.10.2 요구사항은 0.11 미만의 호환 범위를 허용합니다. Cargo resolver는 registry index와 기존 lockfile을 참고해 하위 의존성을 포함한 graph를 선택하고 Cargo.lock에 정확한 package, source, checksum을 기록합니다. cargo build는 잠긴 선택으로 rand random_range API를 컴파일합니다. 요구 범위 선언Cargo.tomlrand = "0.10.2">=0.10.2<0.11의존성 해석Cargo resolvermanifest 제약기존 lock · index하위 의존성 포함선택 결과 잠금Cargo.lock정확한 버전·sourcechecksumrevision빌드·API 사용cargo build--lockedrand::random_range(1..=100)
  1. 요구 범위 · Cargo.toml

    rand = "0.10.2"는 >=0.10.2, <0.11인 caret 범위입니다. 정확한 선택은 아직 정해지지 않았습니다.

  2. resolver · registry

    Cargo가 manifest 제약, 기존 lock entry, registry index와 하위 의존성을 함께 계산해 호환 가능한 package graph를 고릅니다.

  3. 선택 결과 · Cargo.lock

    정확한 package 버전, source, checksum 또는 revision을 기록합니다. 이후 resolution은 이 선택을 우선 사용합니다.

  4. 빌드 · API 사용

    cargo build --locked로 package 선택 drift를 막고 rand::random_range(1..=100)를 컴파일합니다.

cargo update가 바꾸는 것

인수 없이 실행하면 lockfile의 모든 package를 요구 범위 안에서 업데이트하려고 합니다. cargo update -p rand는 지정 package를 보수적으로 갱신하며, 기본적으로 manifest 요구 범위를 넓히지 않습니다.

lockfile의 보장 경계

잠긴 package 선택은 재현성의 한 입력입니다. 동일 바이너리에는 toolchain, target, feature, profile, build script 입력, native dependency와 환경까지 통제해야 합니다.

Rust 2024와 rand 0.10.2는 rustc 1.85 이상을 요구합니다. 이 문서의 모든 rand 예제는 현재 top-level API인 rand::random_range를 사용합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명원 4노드 3간선의 manifest 허용 범위→resolver 선택→lock 결과→build/API를 모바일에서 그린다. 0.10.2는 caret 범위이며 0.11 미만이고, lock은 exact selection이다. 동일 바이너리에는 toolchain, target, features, profile, native input 등이 따로 필요하다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
허용 범위

manifest가 rand의 허용 버전 조건을 준다.

  • 입력: Cargo.toml
  • 조건: rand 허용 범위
  • 범위 선언은 정확한 단일 버전 기록과 다르다.
선택과 기록

resolver가 고른 버전을 lockfile에 남긴다.

  • 선택: 의존 버전·그래프
  • 산출물: Cargo.lock
  • 실제 선택을 기록해야 뒤의 빌드에서 같은 해석을 요구할 수 있다.
locked 빌드

lockfile 조건을 지켜 API를 빌드한다.

  • 명령 조건: --locked
  • 한계: 동일 바이너리 자체 보장 아님
  • toolchain·target·환경 등 빌드 결과의 다른 조건도 남는다.

Cargo 의존성 요구사항에서 API 사용까지

manifest rand 허용 범위→resolver 선택→Cargo.lock 정확한 기록→locked 빌드/API라는 산출물/선택 상태이다. lock만으로 동일 바이너리를 보장하지 않는다.

Cargo.toml의 rand 0.10.2 요구 범위를 resolver가 registry index와 기존 lockfile을 참고해 package graph로 해결하고, Cargo.lock에 정확한 선택을 기록한 뒤 cargo build와 rand random_range API 사용으로 이어지는 흐름

Rust · Cargo · dependency flow

manifest는 허용 범위를, lockfile은 선택 결과를 기록한다

rand = "0.10.2"는 정확한 버전 고정이 아니라 caret 요구 범위입니다. Cargo resolver가 registry와 다른 제약을 함께 계산하고, 실제로 선택한 package graph를 Cargo.lock에 남긴 뒤 그 API를 빌드합니다.

Cargo 요구 범위에서 잠긴 선택과 API 빌드까지원4노드3간선의manifest허용범위→resolver선택→lock결과→build/API를모바일에서그린다.0.10.2는caret범위이며0.11미만,lock은exactselection이다.동일바이너리에는toolchain,target,features,profile,nativeinput등이따로필요하다.
허용 범위 선언Cargo.tomlrand = "0.10.2">=0.10.2 · <0.11
연결 관계허용 범위 선언 · Cargo.toml · rand = "0.10.2" · >=0.10.2 · <0.11 → 의존성 해석 · Cargo resolver · manifest · lock · index · 하위 의존성 포함
의존성 해석Cargo resolvermanifest · lock · index하위 의존성 포함
연결 관계의존성 해석 · Cargo resolver · manifest · lock · index · 하위 의존성 포함 → 선택 결과 잠금 · Cargo.lock · 정확한 버전 · source · checksum / revision
선택 결과 잠금Cargo.lock정확한 버전 · sourcechecksum / revision
연결 관계선택 결과 잠금 · Cargo.lock · 정확한 버전 · source · checksum / revision → 빌드·API 사용 · cargo build --locked · rand::random_range · (1..=100)
빌드·API 사용cargo build --lockedrand::random_range(1..=100)

lock은 선택한 package의 기록동일 바이너리의 모든 조건 아님

Cargo 의존성 해결과 잠금 흐름 Cargo.toml의 rand 0.10.2 요구사항은 0.11 미만의 호환 범위를 허용합니다. Cargo resolver는 registry index와 기존 lockfile을 참고해 하위 의존성을 포함한 graph를 선택하고 Cargo.lock에 정확한 package, source, checksum을 기록합니다. cargo build는 잠긴 선택으로 rand random_range API를 컴파일합니다. 요구 범위 선언Cargo.tomlrand = "0.10.2">=0.10.2<0.11의존성 해석Cargo resolvermanifest 제약기존 lock · index하위 의존성 포함선택 결과 잠금Cargo.lock정확한 버전·sourcechecksumrevision빌드·API 사용cargo build--lockedrand::random_range(1..=100)
  1. 요구 범위 · Cargo.toml

    rand = "0.10.2"는 >=0.10.2, <0.11인 caret 범위입니다. 정확한 선택은 아직 정해지지 않았습니다.

  2. resolver · registry

    Cargo가 manifest 제약, 기존 lock entry, registry index와 하위 의존성을 함께 계산해 호환 가능한 package graph를 고릅니다.

  3. 선택 결과 · Cargo.lock

    정확한 package 버전, source, checksum 또는 revision을 기록합니다. 이후 resolution은 이 선택을 우선 사용합니다.

  4. 빌드 · API 사용

    cargo build --locked로 package 선택 drift를 막고 rand::random_range(1..=100)를 컴파일합니다.

cargo update가 바꾸는 것

인수 없이 실행하면 lockfile의 모든 package를 요구 범위 안에서 업데이트하려고 합니다. cargo update -p rand는 지정 package를 보수적으로 갱신하며, 기본적으로 manifest 요구 범위를 넓히지 않습니다.

lockfile의 보장 경계

잠긴 package 선택은 재현성의 한 입력입니다. 동일 바이너리에는 toolchain, target, feature, profile, build script 입력, native dependency와 환경까지 통제해야 합니다.

Rust 2024와 rand 0.10.2는 rustc 1.85 이상을 요구합니다. 이 문서의 모든 rand 예제는 현재 top-level API인 rand::random_range를 사용합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명원 4노드 3간선의 manifest 허용 범위→resolver 선택→lock 결과→build/API를 모바일에서 그린다. 0.10.2는 caret 범위이며 0.11 미만이고, lock은 exact selection이다. 동일 바이너리에는 toolchain, target, features, profile, native input 등이 따로 필요하다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
허용 범위

manifest가 rand의 허용 버전 조건을 준다.

  • 입력: Cargo.toml
  • 조건: rand 허용 범위
  • 범위 선언은 정확한 단일 버전 기록과 다르다.
선택과 기록

resolver가 고른 버전을 lockfile에 남긴다.

  • 선택: 의존 버전·그래프
  • 산출물: Cargo.lock
  • 실제 선택을 기록해야 뒤의 빌드에서 같은 해석을 요구할 수 있다.
locked 빌드

lockfile 조건을 지켜 API를 빌드한다.

  • 명령 조건: --locked
  • 한계: 동일 바이너리 자체 보장 아님
  • toolchain·target·환경 등 빌드 결과의 다른 조건도 남는다.

crates.io는 러스트 사용자들이 만든 오픈 소스 크레이트를 올려 다른 사람이 쓸 수 있게 하는 공개 레지스트리(registry)이고, 색인(index)은 그곳에 올라온 크레이트마다 어떤 버전이 있고 각 버전이 어떤 크레이트에 의존하는지를 적은 목록입니다. Cargo는 이 색인과 다른 의존성의 조건을 함께 따져 허용 범위 안의 버전을 고르고, 고른 결과를 Cargo.lock에 기록한 뒤 그 버전으로 빌드합니다. 코드는 그대로 둔 채 프로젝트를 빌드해 봅니다.

cargo build
    Updating crates.io index
     Locking ... packages to latest Rust 1.99.1 compatible versions
  Downloaded rand v0.10.3
  ...
   Compiling rand v0.10.3
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.03s

Cargo는 먼저 crates.io의 최신 색인을 받아 오고, 허용 범위 안에서 가장 새 버전을 고릅니다. 이 글을 쓰는 2026년 10월에는 0.10.3이 가장 새 버전이므로 0.10.3이 선택되지만, 여러분이 빌드하는 때에 따라 다른 0.10.x 버전이 보일 수 있습니다. rand가 쓰는 다른 크레이트도 함께 내려받아 컴파일하므로, ...로 줄인 자리에는 운영체제에 따라 다른 여러 크레이트의 이름이 나오고 그 순서도 다를 수 있습니다.

아무것도 바꾸지 않고 다시 cargo build를 실행하면 Finished 줄만 나옵니다. 의존성은 이미 내려받아 컴파일했고 Cargo.toml과 코드도 그대로이므로 할 일이 없기 때문입니다. src/main.rs만 조금 고쳐 저장한 뒤 빌드하면, 의존성은 다시 쓰고 이 프로젝트만 다시 컴파일하므로 Compiling guessing_game과 Finished 두 줄이 나옵니다.

Cargo.lock으로 선택한 버전 고정하기

Cargo.lock은 Cargo가 처음 의존성을 고를 때 만들어지며, 고른 크레이트의 정확한 버전과 출처를 기록합니다. 이후의 빌드는 이 기록이 Cargo.toml의 조건과 맞는 동안 같은 버전을 그대로 씁니다. 그래서 나중에 rand의 0.10.4가 나와도, 직접 갱신하지 않는 한 이 프로젝트는 계속 0.10.3으로 빌드됩니다. 같은 Cargo.lock을 공유하는 다른 개발자와 CI 서버도 같은 버전을 씁니다. CI(continuous integration, 지속적 통합)는 코드가 저장소에 합쳐질 때마다 빌드와 테스트를 자동으로 돌리는 방식이며, React 교재의 CI/CD 기초 개념 소개에서 다룹니다.

이 프로젝트처럼 실행 파일을 만드는 프로젝트는 Cargo.lock을 소스 코드와 함께 버전 관리에 넣는 것이 일반적입니다. 다만 같은 Cargo.lock을 써도 컴파일러 버전이나 대상 플랫폼, 빌드 설정이 다르면 결과물은 달라질 수 있으므로, 이 파일은 같은 의존성 버전을 쓰게 해 줄 뿐 똑같은 실행 파일까지 보장하지는 않습니다. CI에서 cargo build --locked를 쓰면 Cargo.lock을 바꿔야 하는 상황에서 빌드가 실패하므로, 의존성 버전이 모르는 사이에 바뀌는 것을 막을 수 있습니다.

크레이트를 새 버전으로 갱신하기

크레이트를 새 버전으로 올리고 싶다면 cargo update를 씁니다. 이 명령은 Cargo.lock을 무시하고 Cargo.toml의 조건에 맞는 가장 새 버전을 다시 골라 Cargo.lock에 기록합니다.

cargo update

rand = "0.10.2"라는 조건은 그대로이므로 cargo update는 0.10 버전대 안에서만 버전을 올리며, 0.11.0이 나와도 그쪽으로 옮기지 않습니다. 나중에 0.11 버전대를 쓰려면 Cargo.toml의 조건을 rand = "0.11.0"처럼 직접 바꾸고, 바뀐 API에 맞게 코드도 함께 확인해야 합니다. 버전을 올리고 크레이트를 배포하는 일은 Crates.io 배포에서 더 다룹니다.

더 깊이: 0.x 버전의 호환 범위와 라이브러리의 Cargo.lock

Cargo가 호환된다고 보는 범위는 유의적 버전 명세와 조금 다릅니다. SemVer는 1.0.0 이전의 버전을 모두 서로 호환되지 않는다고 보지만, Cargo는 가장 왼쪽의 0이 아닌 자리가 같으면 호환된다고 봅니다. 그래서 "0.10.2"는 0.11.0 미만까지 허용하고, "0.0.3"은 0.0.3 이상 0.0.4 미만, 곧 사실상 그 버전 하나만 허용합니다.

서로 호환되지 않는 버전은 한 빌드 안에 함께 들어갈 수 있습니다. 예를 들어 우리 프로젝트는 rand의 0.10을, 우리가 쓰는 다른 크레이트는 0.9를 요구한다면, Cargo는 둘을 하나로 합치지 않고 두 버전을 모두 빌드합니다. 이때 컴파일러는 두 버전의 타입을 이름이 같아도 서로 다른 타입으로 보므로, 한쪽 버전의 타입을 다른 쪽 버전을 기대하는 함수에 넘기면 타입 오류가 납니다. 반대로 같은 호환 범위 안에서 조건이 서로 맞지 않으면, 예를 들어 한쪽은 =0.4.11을, 다른 쪽은 =0.4.8을 요구하면 Cargo는 오류를 냅니다.

Cargo.lock은 그 파일이 있는 프로젝트를 빌드할 때만 쓰입니다. Cargo FAQ는 Cargo.lock이 패키지를 가져다 쓰는 쪽에는 영향을 주지 않고 Cargo.toml만 영향을 준다고 설명하며, cargo install도 --locked를 주지 않으면 최신 의존성을 고른다고 적습니다. Cargo 팀은 예전에 라이브러리는 Cargo.lock을 버전 관리에 넣지 말라고 권했지만, 2023년 8월에 이 권고를 바꿔 프로젝트의 사정에 맞게 정하라고 안내했고, 그에 맞춰 cargo new도 라이브러리의 Cargo.lock을 무시 목록에 넣지 않게 되었습니다.

임의의 숫자 생성하기

이제 rand로 비밀 숫자를 만듭니다. src/main.rs를 다음과 같이 바꿉니다.

src/main.rs
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

use rand::prelude::*;는 rand가 자주 쓰는 항목을 모아 둔 프렐류드를 가져옵니다. 그 안에는 무작위 수 생성기가 쓰는 메서드를 정의한 트레이트(trait)인 RngExt가 들어 있는데, 트레이트의 메서드를 쓰려면 그 트레이트를 가져와야 합니다. 트레이트는 트레이트와 공통 동작에서 자세히 다룹니다.

rand::rng()는 이 스레드에서 쓸 무작위 수 생성기를 돌려주며, 그 생성기의 random_range 메서드가 인수로 받은 범위 안의 수 하나를 고릅니다. 1..=100은 시작과 끝을 모두 포함하는 범위이므로 1부터 100까지 가운데 하나가 나옵니다. 어떤 트레이트와 메서드를 써야 하는지는 각 크레이트의 문서에 있으며, 프로젝트 폴더에서 cargo doc --open을 실행하면 의존성들의 문서를 만들어 브라우저에서 열어 줍니다.

두 번째로 추가한 println!은 비밀 숫자를 출력합니다. 게임을 만드는 동안 확인하는 데 쓰고, 완성할 때 지웁니다. 몇 번 실행해 보면 다음과 같이 나옵니다.

Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

비밀 숫자는 실행할 때마다 1부터 100 사이에서 새로 고르므로, 여러분의 숫자는 위와 다를 것입니다.

비밀 숫자와 추릿값을 비교하기

입력과 비밀 숫자가 갖추어졌으니 둘을 비교합니다. 다음 코드는 아직 컴파일되지 않으며, 그 이유를 고쳐 나가는 것이 이 절의 내용입니다.

src/main.rs (컴파일되지 않음)
use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

먼저 use로 표준 라이브러리의 std::cmp::Ordering을 가져옵니다. Ordering도 열거형이며 Less, Greater, Equal 세 배리언트를 가집니다. 두 값을 비교했을 때 나올 수 있는 세 결과입니다.

cmp 메서드는 두 값을 비교하며, 비교할 수 있는 값이라면 무엇에든 부를 수 있습니다. guess.cmp(&secret_number)는 guess를 secret_number의 참조와 비교해 Ordering의 배리언트 하나를 돌려주고, match 식이 그 결과에 따라 무엇을 할지 정합니다. match는 갈래(arm)로 이루어지며, 각 갈래는 비교할 패턴(pattern)과 그 패턴에 맞았을 때 실행할 코드로 되어 있습니다. 러스트는 match에 주어진 값을 갈래의 패턴과 위에서부터 차례로 비교해 처음 맞는 갈래의 코드를 실행합니다. 패턴과 match는 match 제어 흐름 구조와 패턴과 매칭에서 자세히 다룹니다.

예를 들어 플레이어가 50을 입력했고 비밀 숫자가 38이라면, 50은 38보다 크므로 cmp가 Ordering::Greater를 돌려줍니다. match는 첫 갈래의 Ordering::Less와 비교해 맞지 않으므로 건너뛰고, 다음 갈래의 Ordering::Greater와 맞으므로 Too big!을 출력합니다. 맞는 갈래를 찾으면 나머지 갈래는 보지 않으므로 마지막 갈래는 검사하지 않고 match가 끝납니다.

그런데 이 코드는 아직 컴파일되지 않습니다.

cargo build
error[E0308]: mismatched types
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                 --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
   |                 |
   |                 arguments to this method are incorrect

오류의 핵심은 타입이 맞지 않는다(mismatched types)는 것입니다. 러스트는 정적 타입 시스템을 가지면서도 타입 추론(type inference)을 하므로, let mut guess = String::new()라고만 써도 guess가 String이라는 것을 알아냅니다. 반면 secret_number는 1부터 100 사이의 정수이므로, 32비트 부호 있는 정수 i32나 부호 없는 u32, 64비트 i64 같은 여러 정수 타입이 될 수 있습니다. 다른 곳에서 타입을 정해 주지 않으면 러스트는 정수를 i32로 보는데, 어느 쪽이든 문자열과 숫자는 비교할 수 없습니다. 오류 메시지의 {integer}는 아직 구체적인 타입이 정해지지 않은 정수라는 뜻입니다.

그래서 입력받은 String을 숫자로 바꿔야 합니다. println!("You guessed: {guess}"); 앞에 다음 줄을 더합니다.

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

이미 guess라는 변수가 있는데 같은 이름으로 다시 let을 썼습니다. 러스트는 새 변수가 이전 변수를 가리는 섀도잉(shadowing)을 허용하므로, guess_str과 guess처럼 이름을 둘 만들지 않고 같은 이름을 다시 쓸 수 있습니다. 이렇게 값을 다른 타입으로 바꿀 때 흔히 쓰며, 섀도잉은 변수와 가변성에서 자세히 다룹니다.

새 변수 guess는 guess.trim().parse()로 만듭니다. 이 식의 guess는 입력을 담은 원래의 String입니다. String의 trim 메서드는 앞뒤의 공백 문자를 지운 문자열을 돌려주는데, u32에는 숫자만 들어갈 수 있으므로 이 처리가 필요합니다. 사용자가 5를 입력하고 Enter를 누르면 guess에는 줄바꿈 문자까지 들어가 5\n이 되고, Windows에서는 캐리지 리턴과 줄바꿈이 함께 들어가 5\r\n이 됩니다. trim은 이것들을 지워 5만 남깁니다.

parse 메서드는 문자열을 다른 타입의 값으로 바꿉니다. 여러 타입으로 바꿀 수 있으므로 어떤 타입으로 바꿀지 알려 주어야 하며, let guess: u32처럼 변수 이름 뒤에 콜론과 타입을 적는 타입 표기(type annotation)가 그 일을 합니다. u32는 32비트 부호 없는 정수로, 작은 양수를 담기에 알맞습니다. 다른 정수 타입은 데이터 타입에서 다룹니다. 또 guess를 u32로 정했으므로, 그와 비교하는 secret_number도 u32라고 러스트가 추론하게 되어 이제 같은 타입끼리 비교합니다.

parse도 실패할 수 있습니다. 문자열이 A👍%처럼 숫자로 바꿀 수 없는 내용이면 실패하므로, read_line처럼 Result를 돌려주고 같은 방식으로 expect를 부릅니다. 바꿀 수 없으면 Err이므로 Please type a number!를 출력하고 프로그램이 멈추며, 바꿀 수 있으면 Ok 안의 숫자를 꺼내 줍니다. 지금까지의 전체 코드는 다음과 같습니다.

src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

실행해 숫자 앞에 공백을 두고 입력해 봅니다. 비밀 숫자는 실행할 때마다 다릅니다.

Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

76처럼 공백을 앞에 넣어도 trim이 지우므로 76으로 바뀌고, 76은 58보다 크므로 Too big!이 나옵니다. 이제 숫자가 너무 큰 경우, 너무 작은 경우, 맞힌 경우를 모두 확인해 볼 수 있습니다. 하지만 아직은 한 번만 추리할 수 있으므로, 반복문과 게임 완성하기에서 반복문을 더해 게임을 완성합니다.

정리

  • let은 변수를 만들며, 변수는 기본으로 불변이고 mut을 붙여야 바꿀 수 있습니다.
  • String::new처럼 ::로 부르는 함수는 타입에 딸린 연관 함수입니다.
  • read_line과 parse는 실패할 수 있으므로 Ok와 Err 가운데 하나인 Result를 돌려주며, expect는 Err이면 메시지와 함께 프로그램을 멈춥니다.
  • 외부 크레이트는 Cargo.toml의 [dependencies]에 버전 조건으로 적고, Cargo가 고른 정확한 버전은 Cargo.lock에 기록됩니다.
  • "0.10.2"라는 조건은 0.10.2 이상 0.11.0 미만을 허용하며, cargo update는 그 범위 안에서만 버전을 올립니다.
  • cmp는 두 값을 비교해 Ordering의 배리언트를 돌려주고, match는 그 결과에 맞는 갈래를 실행합니다.
  • 문자열과 숫자는 비교할 수 없으므로 trim과 parse로 입력을 숫자로 바꾸며, 같은 이름의 새 변수로 가리는 섀도잉을 씁니다.

연습 문제

1. 허용하는 버전

Cargo.toml에 rand = "0.10.2"라고 적었습니다. crates.io에 0.10.1, 0.10.3, 0.11.0이 있을 때, Cargo.lock이 없는 상태에서 처음 빌드하면 Cargo는 어느 버전을 고르나요? 그 뒤 0.10.5가 나왔을 때 cargo build와 cargo update는 각각 어떻게 하나요?

해설 보기

조건이 0.10.2 이상 0.11.0 미만이므로 0.10.1과 0.11.0은 범위 밖이고, 범위 안의 가장 새 버전인 0.10.3을 골라 Cargo.lock에 기록합니다. 0.10.5가 나온 뒤에도 cargo build는 Cargo.lock에 기록된 0.10.3을 그대로 씁니다. cargo update를 실행해야 범위 안의 가장 새 버전인 0.10.5로 올라가며, 0.11.0으로는 조건을 바꾸지 않는 한 올라가지 않습니다.

2. 입력이 숫자로 바뀌기까지

완성한 코드에서 플레이어가 (가) 42, (나) 42 , (다) 4 2, (라) -5를 입력하면 각각 어떻게 되나요?

해설 보기

(가)와 (나)는 trim이 앞뒤 공백과 줄바꿈을 지워 42가 되므로 u32로 바뀌고 비밀 숫자와 비교됩니다. (다)는 trim이 가운데 공백을 지우지 않아 4 2가 남으므로 parse가 실패하고, Please type a number!와 함께 프로그램이 멈춥니다. (라)의 -5는 음수인데 u32는 부호 없는 정수라 음수를 담을 수 없으므로, 역시 parse가 실패해 프로그램이 멈춥니다.

3. 비교하기 전의 타입

let guess: u32 = ... 줄을 넣기 전의 코드에서 match 부분을 통째로 지우면, secret_number는 어떤 타입이 되나요? 그 코드는 컴파일되나요?

해설 보기

컴파일됩니다. secret_number의 타입을 정해 줄 다른 코드가 없으므로, 러스트는 정수의 기본 타입인 i32로 정합니다. match가 있을 때는 String과 비교하려 해서 타입이 맞지 않았고, guess를 u32로 바꾼 뒤에는 그 비교 때문에 secret_number도 u32로 추론됩니다.

다음 글 반복문과 게임 완성하기에서는 여러 번 추리할 수 있도록 반복문을 더하고, 정답을 맞히면 끝내며, 숫자가 아닌 입력은 다시 받도록 고쳐 게임을 완성합니다.