본문으로 건너뛰기

안동민 개발노트

본문 시작
1장 : 시작해봅시다추리 게임

반복문과 게임 완성하기

추리 게임에 반복문을 추가하고 정답과 잘못된 입력을 처리하여 최종 프로그램을 완성합니다.

반복문을 이용하여 여러 번의 추리 허용하기

loop 키워드는 무한루프를 제공합니다.

루프를 추가하여 사용자들에게 숫자를 추리할 기회를 더 주겠습니다.

src/main.rs
    // --생략--

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

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

        // --생략--

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

보시다시피, 추릿값 입력 프롬프트부터 모든 것을 루프로 옮겼습니다.

루프 내부의 줄을 각각 네 칸씩 들여 쓰고 프로그램을 다시 실행하세요.

이제 프로그램이 계속해서 다른 추리를 요청하고, 새로운 문제가 발생했군요.

사용자가 이 프로그램을 종료할 수 없는 것 같습니다!

사용자는 ctrl-c 단축키를 이용하여 프로그램을 멈출 수 있습니다.

하지만 ‘비밀번호와 추릿값을 비교하기’에서 parse 메서드에 대해 언급했던 것처럼 이 만족할 줄 모르는 괴물에게서 빠져나올 다른 방법이 있습니다.

만약 사용자가 숫자가 아닌 답을 적는다면 프로그램이 멈춥니다.

이를 활용하여 아래와 같이 사용자가 종료할 수 있도록 할 수 있습니다.

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

quit를 입력하면 게임이 종료되지만, 보시다시피 숫자가 아닌 다른 입력값에도 게임이 종료됩니다.

정확한 숫자를 맞혔을 때 게임이 종료되기를 원하기 때문에 이는 차선책이라고 할 수 있습니다.

정답을 맞힌 후 종료하기

사용자가 정답을 맞혔을 때 게임이 종료되도록 break문을 추가합니다.

src/main.rs
        // --생략--

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

You win! 뒤에 break 라인을 추가하면 사용자가 비밀번호를 맞혔을 때 프로그램이 루프를 종료하게 됩니다.

루프가 main의 마지막 부분이므로 루프의 종료는 프로그램의 종료를 의미합니다.

잘못된 입력값 처리하기

게임의 동작을 더욱 다듬기 위해 사용자가 숫자가 아닌 값을 입력할 때 프로그램을 종료시키는 대신 이를 무시하여 사용자가 계속 추릿값을 입력할 수 있도록 만들어 봅시다.

예제 1-5에 나온 것처럼 guessString에서 'u32'로 변환되는 줄을 변경하면 그렇게 할 수 있습니다.

예제 1-5: 숫자가 아닌 추릿값에 대해 프로그램을 종료하는 대신 이를 무시하고 다른 추릿값을 요청하기
src/main.rs
        // --생략--

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

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

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

        // --생략--

expect 메서드 호출을 match 표현식으로 바꾸어 에러 발생 시 즉시 종료가 아닌 에러 처리로 바꾸었습니다.

parse 메서드가 Result 타입을 반환한다는 점, 그리고 ResultOkErr 배리언트를 가진 열거형임을 기억하세요.

여기서는 cmp 메서드의 Ordering 결과에서와 마찬가지로 match 표현식을 사용하고 있습니다.

만약 parse가 성공적으로 문자열을 정수로 변환할 수 있으면 결괏값이 들어있는 Ok를 반환합니다.

Ok 값은 첫 번째 갈래의 패턴에 매칭되고, match 표현식은 parse가 생성하여 Ok 값 안에 넣어둔 num 값을 반환합니다.

그 값은 새로 만들고 있는 guess 변수에 바로 위치될 것입니다.

만약 parse가 문자열을 정수로 바꾸지 못한다면, 에러에 대한 더 많은 정보를 담은 Err를 반환합니다.

Err는 첫 번째 갈래의 패턴인 Ok(num)과는 매칭되지 않지만, 두 번째 갈래의 Err(_)와는 매칭됩니다.

밑줄 _은 캐치올(catchall) 값이라 합니다; 모든 값에 매칭될 수 있으며, 이 예제에서는 Err내에 무슨 값이 있던지 상관없이 모든 Err를 매칭하도록 했습니다.

따라서 프로그램은 두 번째 갈래의 코드인 continue를 실행하며, 이는 loop의 다음 반복으로 가서 또 다른 추릿값을 요청하도록 합니다.

따라서, 프로그램은 효과적으로 parse에서 발생할 수 있는 모든 에러를 무시합니다!

이제는 프로그램의 모든 부분이 예상한 대로 동작해야 합니다.

한번 시도해봅시다.

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 4.45s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

멋집니다!

마지막 미세 조정만 추가하여 추리 게임을 마무리하겠습니다.

프로그램이 여전히 비밀번호를 출력하고 있다는 것을 떠올리세요.

테스트 때는 괜찮지만 게임을 망치게 됩니다.

비밀번호를 출력하는 println!을 삭제합시다.

예제 1-6은 최종 코드를 보여줍니다.

예제 1-6: 완성된 추리 게임 코드
src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;

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

    let secret_number = rand::thread_rng().gen_range(1..=100);

    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 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

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

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

이 시점에서는 추리 게임의 기본 기능이 완성된 상태입니다.


마무리 요약

이 프로젝트는 let, match, 메서드, 연관 함수, 외부 크레이트 사용과 같이 많은 새로운 러스트 개념들을 소개하기 위한 실습이었습니다.

다음 장들에서는 이 개념들의 세부적인 내용을 배울 것입니다.

3장은 대부분의 프로그래밍 언어들이 가지고 있는 변수, 데이터 타입, 함수를 소개하고 러스트에서의 사용법을 다룹니다.

4장에서는 다른 프로그래밍 언어와 차별화된 러스트의 특성인 소유권을 다룹니다.

5장에서는 구조체와 메서드 문법을 다루며 6장에서는 열거형에 대해 다룹니다.