사용자가 정답을 맞힐 때까지 재도전(!) 할 수 있또록 다중 입력 기능을 추가해보자.

 

무한 반복을 실행하는 loop 키워드를 사용하여 다음과 같이 입력 안내 멘트부터 match 표현식까지를 반복문 안으로 옮기자.

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("숫자를 맞혀봅시다!");

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

    println!("사용자가 맞혀야 할 숫자: {}", secret_number); // TODO: 테스트 후 제거할 코드

    loop {
        println!("정답이라고 생각하는 숫자를 입력하세요.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("입력한 값을 읽지 못했습니다.");

        let guess: u32 = guess
            .trim()
            .parse()
            .expect("입력한 값이 올바른 숫자가 아닙니다.");

        println!("입력한 값: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("입력한 숫자가 작습니다!"),
            Ordering::Greater => println!("입력한 숫자가 큽니다!"),
            Ordering::Equal => println!("정답!"),
        }
    }
}

언제까지 반복할 것인가? 사용자가 정답을 맞힐 때까지다.

그럼 사용자가 정답을 맞히면 이 무한의 굴레에서 빠져나갈 수 있도록 탈출구를 만들어 둬야겠다.

탈출 조건은 Ordering::Equal일 때이므로 이때 실행될 코드에 loop에서 탈출시켜주는 break 구문을 추가해준다.

match guess.cmp(&secret_number) {
    Ordering::Less => println!("입력한 숫자가 작습니다!"),
    Ordering::Greater => println!("입력한 숫자가 큽니다!"),
    Ordering::Equal => {
        println!("정답!");
        break;
    }
}

이제 cargo run 명령어를 입력하여 테스트를 해보자.

 

 

이제 정답을 맞힐 때까지 다중 입력이 가능하게 되었다.

 

그런데 사용자는 숫자가 아닌 값을 입력할 수도 있다. 즉, 숫자로 파싱이 되지 않는 값을 입력할 수도 있다. 그럴 때는 아래와 같이 에러가 발생하며 프로그램이 바로 종료된다.

 

thread 'main' panicked at '입력한 값이 올바른 숫자가 아닙니다.:
ParseIntError { kind: InvalidDigit }', src\main.rs:24:14

21번째부터 24번째 줄까지는 아래의 코드가 있다.

let guess: u32 = guess
    .trim()
    .parse()
    .expect("입력한 값이 올바른 숫자가 아닙니다.");

ParseIntError는 정수를 파싱할 때 발생할 수 있는 에러라고 한다. 지금처럼 명백하게 숫자가 아닌 값을 파싱하는 경우가 아닌 보통의 경우에 이 ParseIntError가 발생하는 케이스는 문자열에 존재하는 공백(whitespace)를 제거하지 않았을 때라고 한다. 따라서 파싱하기 전에 위와 같이 str::trim() 메서드를 먼저 사용해서 여백을 제거해주라고 안내되어있다.

그리고 이 코드에서 파싱은 문자열의 parse 메서드를 통해 이루어지는데, parse 메서드는 다양한 타입의 숫자 뿐만 아니라 boolean 타입도 파싱할 수 있다. (parse 메서드로 파싱할 수 있는 타입 목록)

 

*참고로 bool 타입을 파싱할 때는 숫자 0, 1이 아니라 문자열 "false", "true" false true에 각각 대응한다고 한다. (참고: https://github.com/rust-lang/rust/issues/53435)

 

아무튼 그래서 parse 메서드가 이처럼 다양한 타입을 파싱할 수 있기 때문에 사용 시 타입 추론에 문제가 있을 수 있다. 파싱이 가능한 여러 타입들 중 정확히 어떤 타입을 원하는지를 명시를 해줘야 하는데, 위와 같이 변수 이름 다음에 콜론(:) 사용해서 타입을 지정해주는 방법도 있고 아래와 같이 turbofish(turbot은 '넙치'인데 ::<> 이 모양이 넙치를 닮아서 이렇게 이름 지은 건가 싶다 ㅋ) 구문을 사용하는 방법도 있다.

let guess = guess
    .trim()
    .parse::<u32>()
    .expect("입력한 값이 올바른 숫자가 아닙니다.");

u32 타입(부호 없는 정수 타입)으로 변환하겠다고 지정해둔 상태로, 이 타입으로 변환할 수 없는 값을 입력했으니 에러가 발생한 것이다.

 

이렇게 에러가 발생했을 때, 지금처럼 프로그램을 바로 종료해버리는 것이 아니라 따로 에러 처리를 함으로써 사용자가 다시 올바른 값을 입력할 수 있도록 코드를 수정해보자.

expect 메서드를 호출하는 대신 match 표현식을 사용해서 분기 처리를 하면 된다. parse 메서드는 이전에 살펴본 Result 타입을 반환하고, Result 타입은 Ok(성공) 또는 Err(실패)을 표현하는 enum이므로 아래와 같이 코드를 작성할 수 있다.

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

Ok는 성공적으로 처리한 값을 가지고 있으므로, Ok일 경우에는 해당 값을 반환하여 guess에 이 값이 대입되도록 한다.

Err는 실패 값을 가지고 있으나 이를 사용하지는 않을 것이므로 일단 언더바(_)로 이 값을 받는다. 그리고 continue를 실행하여 현재 반복을 종료하고 loop의 처음으로 돌아가서 다음 반복을 실행하도록 한다. 즉 잘못 입력된 값은 무시하고 다시 입력을 하도록 처리하는 것이다.

 

이제 다시 cargo run을 통해 프로그램을 실행해보자.

 

 

우리의 의도대로, 숫자가 아닌 값을 입력하면 깔끔하게 무시해버리는 것을 확인할 수 있다. 이제 테스트를 위해 작성해두었던 매크로 코드만 삭제하면 드디어 대망의 숫자 게임이 완성된다.

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("숫자를 맞혀봅시다!");

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

    loop {
        println!("정답이라고 생각하는 숫자를 입력하세요.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("입력한 값을 읽지 못했습니다.");

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

        println!("입력한 값: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("입력한 숫자가 작습니다!"),
            Ordering::Greater => println!("입력한 숫자가 큽니다!"),
            Ordering::Equal => {
                println!("정답!");
                break;
            }
        }
    }
}
복사했습니다!