[RUST] 2. 숫자 맞히기 게임의 구현

    1~100사이의 난수를 생성해서 입력을 받아 입력한 값이 더 큰지 작은지 알려주어 숫자를 맞추는 게임을 구현해보자.

    새 프로젝트 셋업하기

    책에서는 cargo new 명령어를 통해 새프로젝트를 생성하라고 되어있지만 나는 IntelliJ를 통해 guessing_game 프로젝트를 생성하였다.

    플레이어가 예측한 값 처리하기

    먼저 플레이어에게 예측한 값을 묻는 코드를 작성해보자

    use std::io;
    
    fn main() {
        println!("숫자를 맞혀봅시다!");
    
        println!("정답이라고 생각하는 숫자를 입력하세요.");
    
        let mut guess = String::new();
    
        io::stdin().read_line(&mut guess)
            .expect("입력한 값을 읽지 못했습니다.");
    
        println!("입력한 값: {}", guess);
    }

    변수에 값 지정하기

    • use 구문을 통해 프로그램에 사용할 기능을 가져올 수 있다.
    • let 은 변수를 생성하는 구문이다.
    • mut 키워드는 값을 변경할수 있는 변수를 생성하기 위해 사용된다.
    • String 은 표준 라이브러리가 제공하는 문자열 타입이며 UTF-8 형식으로 인코딩된 텍스트를 표현한다.
    • read_line에서는 인수를 변경가능한 참조를 전달하기 위해 &guess 가 아니라 &mut guess로 표기해야한다.

    Result 타입을 이용해 잠재적인 오류 처리하기

    .expect 이후 코드 부분은 붙여서 작성할수도 있지만 책에서는 코드의 가독성을 위해 두줄로 나누어서 작성하였다. read_line 메서드는 사용자가 입력한 값을 문자열에 대입하기도 하지만 io::Result 타입의 값을 리턴하기도 한다. Result 타입은 enums 으로 표기하기도 한다. Result 의 열것값은 Ok와 Err가 있다. io::Result 타입의 인스턴스가 Err라면 expect 메서드는 프로그램을 종료하고 expect 메서드의 메시지를 표시한다. 반대로 Ok라면 Ok값이 보관하고 있는 값만 읽어와 리턴한다. 그리고 expect 메서트를 호출하지 않으면 컴파일시 경고 메시지가 나타난다.

    println! 자리지정자를 이용해 값 출력하기

    println!에서 사용한 {} 중괄호는 자리지정자(placeholder)라고 부르며 다음에 나열된 값을 출력한다. 다음과 같이 여러개의 값을 출력 할 수도 있다.

    let x = 5;
    let y = 10;
    
    println!("x = {}, y = {}", x, y);
    //x = 5, y = 10 을 출력한다.

    코드 테스트

    난수 생성하기

    1에서 100 사이의 숫자 중 임의로 생성하도록 해보자. 러스트의 표준 라이브러리는 난수를 추출하는 기능을 제공하지 않는다. rand 크레이트를 통해 구현해보자.

    크레이트를 이용해 필요한 기능 추가

    크레이트는 소스 파일의 집합이다. Cargo.toml 파일을 수정해서 rand 크레이트를 의존 패키지로 등록해주어야 한다. 책에서는 0.6.1 이였으나 IntelliJ 에서는 0.6.5까지 자동 완성되어 0.6.5로 입력해보았다.

    [depedencies]
    
    rand = "0.6.5"

    그 다음 프로젝트를 다시 빌드해보면

    rand 크레이트 관련 컴파일링이 되었음을 확인할 수 있다. 이후 Cargo.lock 파일을 이용해서 재생산 가능한 빌드를 구현할 수 있다. 새 버전의 크레이트로 업그레이드 하기 위해서는 Cargo.toml 파일에 지정된 조건에 해당하는 가장 최신 버전을 다시 찾는다. 책 예제에서는 0.6.1 에서 cargo update 명령어를 통해 0.6.2로 업데이트 되어있는데 이미 0.6.5버전으로 해버려서 업데이트가 되지 않는다. 역시 처음할땐 책을 따라하는게 좋을 것 같다.

    난수 생성하기

    Cargo.toml 파일에 rand 크레이트를 추가했으므로 코드를 작성해보자.

    use std::io;
    use rand::Rng;
    
    fn main() {
        println!("숫자를 맞혀봅시다!");
    
        let secret_number = rand::thread_rng().gen_range(1, 101);
        println!("사용자가 맞혀야 할 숫자 : {}", secret_number);
    
        println!("정답이라고 생각하는 숫자를 입력하세요.");
    
        let mut guess = String::new();
    
        io::stdin().read_line(&mut guess).expect("입력한 값을 읽지 못했습니다.");
    
        println!("입력한 값: {}", guess);
    }
    • use rand::Rng Rng 트레이트(trait)는 난수 생성기에 구현된 메서드를 정의한다.
    • rand::thread_rng 함수는 우리가 사용할 난수 생성기를 리턴하고, 생성기의 gen_range 함수를 호출한다.

    난수와 사용자의 입력 비교하기

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

    위 코드만 추가하고 빌드하면 다음과 같은 빌드 에러가 발생한다.

    에러 메시지의 핵심은 타입 불일치이다. guess 변수는 String 타입이고 secret_number 변수는 숫자 타입 이다. 별도로 숫자 값을 저장할 수 있는 타입을 명시하지 않았기에 i32 타입으로 이해한다. 그래서 다음과 같이 코드를 수정하여 빌드 에러를 해결할 수 있다.

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

    guess 라는 변수는 이미 선언 했지만 러스트는 변수가 보관하던 값을 새로운 변수의 값으로 가려버린다.(shadowing) 덕분에 변수 이름을 재사용할 수 있다. 이제 프로그램을 실행해보자.

    반복문을 이용해 다중 입력 지원하기

    loop 키워드는 무한 반복을 실행한다. 반복문을 빠져나오려면 break 구문을 사용하면 된다.

        /* 중략 */
        println!("사용자가 맞혀야 할 숫자 : {}", secret_number);
    
        loop{        
            /* 중략 */
    
            println!("입력한 값: {}", guess);
    
            match guess.cmp(&secret_number) {
                Ordering::Less => println!("입력한 숫자가 작습니다!"),
                Ordering::Greater => println!("입력한 숫자가 큽니다!"),
                Ordering::Equal => {
                    println!("정답!");
                    break;
                },
            }
        }

    올바르지 않은 입력 처리하기

    숫자가 아닌 값을 입력하면 프로그램을 종료하는 대신 다시 값을 입력할수 있도록 수정해보자. expect 메서드 호출을 match 표현식으로 변경한다.

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

    이제 플레이를 통해 테스트 해보자. 숫자가 아닌값을 입력하면 다시 입력을 받도록 되었다.

    기능이 완성되었으니 정답을 출력하는 구문을 삭제하고 게임을 완성하자.

    요약

    • let, match, 메서드, 연관 함수, 외부 크레이트의 사용법 등 러스트의 다양한 개념들을 소개
    반응형

    댓글

    Designed by JB FACTORY