Rust 공식 문서를 참고한 글입니다.
Rust를 사용해서 출력하고 컴파일하는 과정까지 공부했다. 이번에는 숫자 맞히기 게임을 구현할 것이다.
게임 규칙은 다음과 같다.
- 1~100 사이의 난수를 생성한다.
- 사용자로부터 예측 값을 입력 받는다.
- 사용자가 입력한 예측 값과 난수를 비교해서 더 큰지, 더 작은지 출력한다.
- 예측 값과 난수가 같다면 축하합니다 메세지와 함께 게임이 종료된다.
1. 새 프로젝트 생성하기
cargo를 사용해 새로운 프로젝트를 만들어야 한다.
$ cargo new guessing_game
$ cd guessing_game
지난 번에도 봤듯, 프로젝트를 생성하면 guessing_game이라는 폴더 안에 Cargo.toml과 src 폴더가 생성된다.
2. 예측 값 처리
사용자에게 예측 값을 입력하라는 메세지를 출력하고, 값을 입력받아야 한다.
// main.rs
use std::io;
fn main() {
println!("Guess then 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;
일반적으로, Rust는 표준 라이브러리에 정의된 항목들을 각 프로그램의 스코프로 가져온다. 이 항목들의 집합을 Prelude라고 하는데, 만약에 사용하려는 타입이 없다면 use 문으로 직접 스코프를 가져와야 한다.
다음으로 함수 부분은 이전 글에서도 봤듯, 프로그램의 입구 역할을 한다. fn은 새로운 함수를 선언하고, 함수 이름이 main이다. () 안이 비어있기 때문에 파라미터는 존재하지 않으며 {} 안에 함수 body가 들어있다. 그리고 body에는 사용자에게 메세지를 출력하고 있다.
fn main() {
println!("Guess then number!");
println!("Please input your guess.");
..
변수 선언
사용자에게 입력을 받고 이를 변수에 저장하는 부분을 살펴보자.
let mut guess = String::new();
let으로 변수를 선언하는데, Rust에서 변수는 immutable(불변)이기 때문에, 한 번 값을 할당하면 새로운 값으로 변경할 수 없다. 그런데 변수를 새롭게 할당할 수 있도록 하려면, mut를 함께 사용하면 된다.
다시 돌아가서, guess 라는 가변 변수를 선언하고, String은 Prelude에 포함되어 있는 표준 라이브러리가 제공하는 문자열 자료형이다. :: 구문은 ::new 라인에서 new가 String과 연관 함수(associated function)이라는 것을 말한다. 연관 함수란 해당 타입에 구현된 함수로, 이 경우에는 String 타입에 구현된 함수다. 그래서 new 함수는 새로운 문자열을 생성한다.
정리하면, guess 라는 가변 변수에 새로운 문자열을 바인딩하는 줄이다.
사용자에게 입력 받기
프로그램 첫 번째 줄에서 표준 라이브러리의 입출력 기능을 가져왔다. io 모듈의 stdin()을 사용해 사용자 입력을 처리할 수 있다.
io::stdin()
.read_line(&mut guess)
use std::io;로 가져오지 않았다면 std::io::stdin()으로 선언할 수 있다. stdin 함수는 터미널의 표준 입력에 대한 핸들을 나타내는 std::io::Stdin의 인스턴스를 반환한다. 다음으로 .read_line(&mut guess)는 read_line 함수를 호출해서 사용자로부터 입력을 받고, guess을 인자로 전달해 사용자 입력을 지정할 문자열을 지정한다. 이때, guess에 새로운 문자열을 지정해야 하므로, 가변 변수이어야 한다. &는 '참조'를 뜻하며 데이터를 메모리에 여러 번 복사하지 않고 코드의 여러 부분에서 하나의 데이터를 접근할 수 있게 한다. 이때, 참조도 불변성을 띄기 때문에 mut과 함께 사용해야 한다.
에러 핸들링
.expect("Failed to read line");
위에서 read_line은 사용자가 입력한 내용을 우리가 전달한 문자열에 넣지만, Result도 반환한다. 보통 Result는 enum 타입으로 여러 가지 가능한 상태 중에 하나를 뜻한다. 각각의 상태를 variant라고 하고, Result의 variant는 Ok와 Err이다. Ok는 말그대로 작업이 오류 없이 완료되면 반환되고, 성공적으로 생성된 값을 가진다. Err는 작업이 실패했음을 뜻하고, 실패 원인을 담고 있다. Result는 expect라는 메서드를 가지는데, Ok인 경우 expect를 통해 성공된 값을 반환하고, Err가 발생하면 오류 메세지를 출력한다. 따라서, Result를 사용할 때 예외 처리를 해주지 않으면 오류가 발생한다.
placeholder 값 출력하기
println!("You guessed: {}", guess);
이제, 사용자로부터 입력받은 값을 출력할 차례다. 우리가 지금까지 봤던 println!()과 약간 차이가 있다. {}는 placeholder로, C언어의 %d처럼 값이 들어가는 구역이라고 생각하면 된다. 중괄호로 표현하고, 중간에 ,를 사이에 두고 열거하면 된다.
프로그램 실행
우리가 짠 코드를 cargo run 명령어를 통해 실행하면, 사용자로부터 입력을 받고 입력 받은 값을 잘 출력하는 모습을 확인할 수 있다.
3. 난수 생성
사용자로부터 수를 입력 받았으니, 이제는 1~100 사이의 난수를 생성할 차례다. Rust의 표준 라이브러리는 난수 생성 기능을 지원하지 않는데, Rust 팀이 제공하는 rand 크레이트(crate)를 사용하면 된다.
crate는 Rust 소스 코드 파일들의 모음이다. 지금까지 우리가 작성한 프로젝트는 실행 가능한 바이너리 크레이트인데, rand
크레이트는 다른 프로그램에서 사용되도록 작성된 라이브러리 크레이트이다. 때문에 단독으로 실행될 수는 없다.
Cargo를 통해서 외부 크레이트를 가져올 수 있는데, rand를 사용하려면 Cargo.toml에 의존성을 추가해야 한다. Cargo.toml 의존성 관리 부분에 아래와 같은 코드를 추가해야 한다.
[dependencies]
rand = "0.8.5"
다음으로 cargo build를 통해 업데이트된 의존 패키지를 최신 버전으로 맞추어 준다. Cargo.toml에 명시된 버전과 맞게 최신 버전으로 밪추어 주려면 cargo update 명령어를 사용해서 Cargo.lock를 업데이트할 수 있다.
다시 돌아와서, 난수 생성 코드를 살펴보자.
// main.rs
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_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}");
}
rand 스코프 추가와 난수 출력
첫 줄의 use rand::Rng; 는 난수 생성기에 구현된 메서드를 정의하는 trait이다. 이 부분은 추후에 알게 될 것이라 이 정도로만 알고 지나가자.
이제 추가된 메서드를 사용하여 난수 생성 코드를 작성할 것이다.
let secret_number = rand::thread_rng().gen_range(1..=100);
rand::thread_rng()는 사용할 난수 생성기를 가져오는데, 이 생성기는 현재 실행 중인 스레드의 로컬에 존재하며, 운영체제에 의해 seed가 결정된다. 다음으로, 난수 생성기에서 gen_range()를 사용하여 주어진 범위 내의 난수를 생성한다. 참고로 범위 표현식은 start..=end 형식이다.
이제 프로그램에서 삭제할 것이지만, 난수를 출력해보자.
println!("The secret number is: {secret_number}");
출력해보면, 생성된 난수를 확인할 수 있다.
4. 난수와 Guess 값 비교하기
이제 난수와 Guess 값을 비교해 출력해야 한다.
// main.rs
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_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!"),
}
}
스코프 추가
코드를 살펴보면, std::cmp::Ordering 라는 또 다른 스코프를 가져온다. Ordering 타입은 enum 타입으로 Less, Greater, Equal라는 variant를 가진다. 이는 두 값을 비교할 때 반환하는 값들이다.
값 비교
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
cmp 메서드를 사용해서 guess와 secret_number을 비교한다. 그리고 Ordering의 variant를 반환하는데, match 표현식으로 출력할 수 있다. 이때 match는 여러 개의 arms를 가지는데, 일치시킬 패턴과 패턴에 맞을 경우 실행할 코드로 구성된다. 그래서 match에 주어진 값을 순차적으로 패턴과 비교해서 일치하는 경우 코드를 실행시킨다.
이제 cargo run 명령어를 통해 프로그램을 실행해보자. 그러면 아래와 같은 오류가 발생한다.
에러를 해석해보면, type이 일치하지 않아 발생한 것이다. 조금 전에 우리는 guess 라는 가변 변수를 String 타입으로 선언했다. 그런데 생성된 난수를 number 타입이기 때문에 비교가 불가하다. 특별히 지정하지 않으면 Rust는 32비트 숫자인 i32 을 사용한다.
타입 오류 해결
let guess: u32 = guess.trim().parse().expect("Please type a number!");
이를 해결하기 위해서 사용자로부터 입력받은 문자열을 숫자 타입으로 변환해야 한다.
그래서 guess라는 변수를 생성한다. 우리가 전에 guess 변수를 선언할 적이 있다. 그러나 Rust는 shadowing 이라는 기능으로 기존의 변수에 새로운 변수를 선언해서 덮어쓸 수 있다. guess_str과 guess라는 두 개의 변수를 사용하지 않아도 된다.
코드를 살펴보면, guess는 trim().parse()에 바인딩된다. 여기에서 guess는 기존의 string 타입의 변수를 가리킨다. u32 타입의 변수와 비교하려면, trim()을 사용해 문자열의 시작과 끝의 공백을 제거해야 한다. read_line()에 따라 숫자를 입력하려면 엔터를 쳐야 하기 때문에 공백이 추가되기 때문이다. 그리고 parse()는 문자열을 숫자 타입으로 변환하기 위해서 사용된다.
이에 따라 완성된 코드는 아래와 같다.
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_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!"),
}
}
실행해보면 오류 없이 잘 출력되는 것을 볼 수 있다.
5. 반복하기
이제 모든 기능을 구현하였으니, 게임을 반복하는 기능을 추가할 것이다. 반복을 사용하는 방법은 다양하지만 이번에는 loop{}를 사용해볼 것이다.
//main.rs
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
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!"),
}
}
}
6. 기능상의 보완
종료하기
종료하는 방법은 ctrl-C로도 가능하지만, 명확한 조건에 따라 게임을 종료하도록 만들 것이다. 만약 사용자 입력값과 난수가 동일하다면 종료하도록 구현했다. 그 외의 조건에서는 프로그램이 무한 반복된다.
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
입력 검증
현재는 숫자가 아닌 값을 입력하면 그대로 프로그램이 종료된다. 그것보다는 사용자에게 새로운 값을 입력받도록 하는 것이 더 좋을 것이다.
이제는 guess가 parse()를 통해서 문자열이 숫자로의 변경이 성공인 경우, guess가 바인딩되고 실패한다면 continue에 따라 반복문의 시작부분으로 돌아간다.
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
아, 그리고 실행 전에 secret_number 출력 부분을 주석 처리하면 제대로된 게임을 할 수 있을 것이다.
'언어 > Rust' 카테고리의 다른 글
[Rust] 4-1. 소유권(Ownership) (0) | 2024.09.28 |
---|---|
[Rust] 3-3. 제어문(Control Flow) (0) | 2024.09.26 |
[Rust] 3-2. 함수와 주석 (0) | 2024.09.25 |
[Rust] 3-1. 변수와 자료형 (1) | 2024.09.25 |
[Rust] 1. 환경 구축하기 (0) | 2024.09.23 |