Rust #2: 코드 따라하기(1/2) - 2장 추측 게임 프로그래밍

·

4 min read

개요

러스트 홈페이지에서 제공하는 The Rust Programming Language 온라인 북에 나와 있는 코드를 따라 해보면서 Rust의 특징을 파악하고 손에 익히도록 합니다.

2장 추측 게임 프로그래밍

여러 프로그래밍 언어에서 실습을 위해 예제로 곧잘 활용하는 숫자 맞추기 게임입니다. 온라인 북은 이를 통해 Cargo 설정파일 및 외부 Crate를 어떻게 포함시키는지 숫자 맞추기 게임을 구현하기 위해 간단한 Rust 코드를 학습 시킵니다.

cargo를 이용해 기본 패키지를 생성합니다.

$ cargo new guessing_game
cd guessing_game

기본 cargo.toml 구조입니다. 지금은 자동생성된 toml 설정파일의 기본 구조만 살펴봅니다.

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

기본 생성된 main.rs 소스코드 입니다.

fn main() {
    println!("Hello, world!");
}

심지어 이 코드는 cargo run을 통해 바로 실행도 잘 됩니다.

$ cargo run
   Compiling guessing_game v0.1.0 (W:\rust-learning\guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s
     Running `target\debug\guessing_game.exe`
Hello, world!

훌륭합니다. 기본 컴파일 속도도 빠르고, 별도의 의존성 없이 곧잘 실행되네요.

VS Code로 코딩을 계속 진행합니다.

실행 명령을 통해 Rust를 실행하려면 launch.json을 다음과 같이 작성합니다.

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "lldb",
            "request": "launch",
            "name": "Debug",
            "program": "${workspaceFolder}/target/debug/guessing_game",
            "args": [],
            "cwd": "${workspaceFolder}"
        }
    ]
}

기본 생성되는 설정파일에 program의 경로만 설정했습니다. 이제 VS Code로 코딩을 계속할 수 있습니다.

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);
}

첫번째 그럴싸한 Rust 코드입니다. C# 프로그래머 입장에서는 대략적인 의미를 파악할 수 있는 형식과 무엇인지 알 수 없는 부분이 있습니다. 가령, use std::io는 std의 io 모듈을 사용하겠다라는 영역지정의 의미이지만, 왜 stdio() 앞에 io::가 붙어야 하는지는 아직은 모르겠습니다. 함수명이 중복되는것을 최대한 막기 위해 모듈명을 적어줘야 하는게 기본 동작으로 보입니다. 그리고 fn은 function으로 함수를 의미한다고 유추할 수 있고 main() 함수가 최초 진입점임을 알 수 있습니다. println!()형태의 함수는 느낌표가 들어가 특이한데, 매크로라는 의미라고 합니다. 친절하게 F12를 누르면 해당 기능의 소스코드를 살펴볼 수 있습니다. 아마도 가변 함수 인자를 컴파일 타임에 처리하기 위함으로 보입니다. guessString:new()로 할당하는데요, 특이한 점은 mut 키워드 입니다. 기본적으로 Rust는 변경가능한 변수가 아닌 변경불가능한 불변(immutable) 변수를 생성하는게 기본 동작으로 보입니다. io:stdin().read_line() 함수를 통해 입력한 값을 문자열로 받기 위함인데요, 반환값이 아닌 참조 인자로 받는 것은 특이한 점입니다. 아마 관련해서 Rust 언어의 특징이 드러날 것으로 보입니다. read_line() 함수에서 오류가 발생할 경우 expect()의 인자를 오류처리시 사용하는것으로 보입니다.

최종적으로 println!()함수를 통해 입력된 값을 출력하네요. {}에 값이 들어가는 것으로 보입니다.

다음으로 난수값을 이용하기 위해 rand Crate를 Cargo.toml에 추가합니다.

[dependencies]
rand = "0.8.3"

이게 끝입니다. 다시 cargo run을 하면 알아서 해당 Crate 및 의존 Crate들을 설치해줍니다.

use rand::Rng;
use std::io;

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

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

    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::Rng를 통해 Rand Crate를 사용함을 알리고, rand::thread_rng().gen_range()를 통해 난수값을 취합니다. 해당 함수의 정확한 의미는 아직 모릅니다. 1..101 형태로 범위 값을 사용할 수 있는데, python 등 동적 언어에서 이전부터 지원했었고, C#도 최근 버젼에서는 지원합니다.

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..101);

    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!"),
    }
}

String.trim()parse()를 통해 문자열을 숫자로 변환합니다. 어떻게 parse()함수가 반환값을 추측할 수 있는지는 모르곘습니다. Rust의 특징으로 보이네요. => 반환값을 추측하는게 아니라 FromStr형을 반환하는데 이게 숫자형으로 보입니다.

std:cmp::Ordering 모듈을 이용해 guess.cmp()로 작거나 크거나 같음을 비교할 수 있습니다. match 키워드를 통해 상당히 세련되게 표현할 수가 있군요.

다음은 최종적인 코드 입니다.

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..101);

    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;
            }
        }
    }
}

String의 parse()의 반환형인 Result는 Ok이거나 Err일 수 있는데, 함수형 언어의 영향을 받은 것으로 보입니다. Ok일 경우 반환된 값을 그대로 사용할 수 있고 Err일 경우 에러 관련 처리를 할 수 있습니다.

이렇게 2장을 둘러보면서 Rust언어의 특징을 대략적으로 조금이나마 파악할 수 있었습니다. Rust 표준 라이브러리에서는 반환값으로 Result 형을 대부분 사용할것 같고요, 이하 기본적인 구문은 C# 프로그래머 입장에서는 예측할 만한 범위에 아직은 있는 것 같습니다.

실행화면

$ cargo run
Guess the number!
Please input your guess.
5
Guess the number!
Please input your guess.
30
You guessed: 30
Too small!
Please input your guess.
40
You guessed: 40
Too small!
Please input your guess.
50
You guessed: 50
Too small!
Please input your guess.
60
You guessed: 60
Too small!
Please input your guess.
70
You guessed: 70
Too small!
Please input your guess.
80
You guessed: 80
Too small!
Please input your guess.
90
You guessed: 90
Too big!
Please input your guess.
85
You guessed: 85
Too big!
Please input your guess.
80
You guessed: 80
Too small!
Please input your guess.
83
You guessed: 83
Too small!
Please input your guess.
84
You guessed: 84
You win!