코드 작성하기

  • 일반적인 개념을 파악하기 위해 문서 2장의 안내에 따라 간단한 숫자 맞히기 게임을 만들어 볼 예정이다. 숫자 맞히기 게임의 동작 원리는 다음과 같다.
    • 1~100 사이의 임의의 정수를 생성한다.
    • 플레이어에게 이 값이 무엇일지 예측해보라고 하고 그 예측값을 입력 받는다.
    • 플레이어에게 입력 받은 예측값과 기준값(생성된 난수)을 비교하여, 입력값이 기준값보다 큰지 작은지를 알려준다.
    • 예측값과 기준값이 일치하면 프로그램은 축하 메시지를 출력하고 종료한다.
  • 우선 cargo를 이용하여 새 프로젝트를 생성하고, 해당 디렉터리로 이동한다.
    $ cargo new guessing_game && cd guessing_game​
  • 난수를 생성하는 로직을 작성하기 전에, 플레이어에게 입력할 값을 묻고 이 입력값을 처리하는 코드를 작성해 보자. src/main.rs 파일에 아래 코드를 작성한다.
    use std::io;
    
    fn main() {
        println!("숫자를 맞혀봅시다!");
    
        println!("정답이라고 생각하는 숫자를 입력하세요.");
    
        let mut guess = String::new();
    
        io::stdin()
            .read_line(&mut guess)
            .expect("입력한 값을 읽지 못했습니다.");
    
        println!("입력한 값: {}", guess);
    }
     
    • VS Code extensions에서 Rust를 설치하면 코드 자동완성과 마우스 오버 시 코드에 대한 설명 등 유용한 기능을 이용할 수 있다.
    • 이렇게 작성한 코드를 cargo run 명령어를 통해 실행시켜보자.

 

 

코드 살펴보기

작성한 코드를 한 줄씩 살펴보자.

  • use std::io;
    • use
      • 다른 크레이트(라이브러리)나 모듈에서 항목(들)을 가져온다.
      • as 키워드를 사용하여, 가져온 항목의 이름을 해당 파일에서 사용될 로컬 이름으로 바인딩 해줄 수도 있다. (예: use p::q::r as x; )
    • std
    • ::
      • :: 기호는 (자바스크립트는 아니지만) 다른 언어에서는 scope resolution operator로 사용된다고 하는데 러스트에서는 정확히 이러한 용도로 쓰이는지는 모르겠다. 위키백과에 따르면 scope resolution operator는 '네임 스페이스를 지정하여 식별자가 참조하는 컨텍스트를 식별'한다고 한다. 위와 같이 std에서 io 라이브러리를 가져올 때엔 이런 용도로 사용한 게 맞는 것 같은데, 문서에 따르면 8번째 줄에서 사용된 ::는 new 함수가 String 타입의 '연관 함수(associated function)'이라는 점을 의미한다고 한다. 이러한 '연관 항목(associated items)에 대해 이 문서에 따르면, '연관 항목'이란 특정 인스턴스가 아니라 어떤 타입(예: String) 자체에 구현된 메서드 또는 상수를 가리키며, 다른 언어(예: 자바스크립트)에서 이러한 연관 함수와 대응되는 개념이 '정적 메서드(static method)'라고 한다.
      • Why double colon rather that dot ← 이 레딧 글에 달린 댓글 중 lookmeat이라는 유저가 작성한 댓글을 보면 러스트에서 마침표 연산자와 :: 연산자가 어떻게 다른지에 대해 정적 메서드/정적 프로퍼티의 개념과 연관지어서 생각해볼 수 있다.
    • io
      • 플레이어의 입력값을 읽어오고 출력하려면 io 라이브러리를 가져와야 한다. (io는 input/output(입력/출력)을 의미한다.)
  • fn main() {
    • fn
      • 새로운 함수를 선언한다.
    • main
      • 프로그램에 진입하는 함수다.
    • ()
      • 괄호가 비어있으므로 매개변수가 없음을 의미한다.
    • {
      • 함수 본문의 시작을 의미한다.
  • println! : string을 화면에 출력한다.
  • let mut guess = String::new();
    • let
      • 변수를 생성하는 키워드
    • mut
      • 러스트에서 변수는 기본적으로 immutable하나, mut 키워드를 사용하면 가변변수로 만들 수 있다.
    • String::new()
      • new 함수는 String 타입의 새로운 인스턴스를 생성하는 associated function이다. 이때 String 타입은 str 타입과는 구분되는 개념으로, 길이 조절이 가능한 UTF-8 인코딩의 문자열 타입이라고 한다.
      • String 타입과 str 타입이 나누어져 있는 이유는 메모리 관리, 그리고 문서의 4장에서 살펴볼 '소유권(Ownership)'과 관련이 있는 듯 하다. 스택 오버플로우에서 둘의 차이점에 대한 설명들을 읽어보았으나 매니지드 언어인 자바스크립트만 경험해 본 나로서는 아직 이해가 잘 가지 않으므로 이 부분은 우선 북마크해두고 스킵!
    • 결론적으로 이 구문은, guess라는 mutable한 변수에 String 타입의 인스턴스를 할당하는 구문임을 알 수 있다.
  • io::stdin().read_line(&mut guess).expect("입력한 값을 읽지 못했습니다.");
    • 이 코드는 포매터가 3줄로 나누어 버렸고 가독성 측면에서 그런 식으로 나누는 게 좋지만 이렇게 한 줄로 작성하는 것도 가능은 하다.
    • 먼저 io의 associated function인 stdin 함수를 호출한다. 툴팁에 뜨는 설명에 따르면 이 stdin 함수는 현재 프로세스의 표준 입력에 대한 새로운 '핸들'을 생성한다고 한다.

      • 핸들(handle)은 러스트에만 있는 개념은 아닌 것 같다. (왜냐하면 'rust' 붙이고 구글링하면 자료가 별로 안 나온다 ㅋㅋ) 위키백과에 따르면 '컴퓨터 프로그래밍에서 핸들(handle)은 응용 소프트웨어가 메모리 블록이나 데이터베이스나 운영 체제와 같은 다른 시스템에 의해 관리되는 객체를 참조할 때 사용되는 리소스에 대한 추상적인 참조'라고 한다. 포인터(pointer)는 포인터가 가리키는 대상이 위치하고 있는 주소를 담고 있는 반면, 핸들은 외부에서 관리되는 참조의 '추상화라고 한다.
      • '현재 프로세스의 표준 입력'은 터미널에서 사용자의 입력으로 볼 수 있고, stdin() 함수는 이 입력에 대한 핸들을 표현하는 std::io::Stdin 타입의 인스턴스를 반환한다.
    • 이렇게 반환된 표준 입력 핸들의 read_line() 메서드를 호출하는데 이때 &mut guess를 인자로 넘긴다.
      • read_line() 메서드는 표준 입력으로부터 사용자 입력 값을 읽고 이를 인자로 전달 받은 '가변' 문자열에 저장한다.
      • &은 참조(reference) 타입임을 나타내기 위해 사용하는데, 참조도 기본적으로는 불변이며 여기서는 가변으로 바꿔야 하기 때문에 &mut로 작성한다. 참조는 '코드의 여러 부분에서 데이터를 여러 번 메모리로 복사하지 않고 접근하기 위한 방법을 제공'한다는데 아직은 무슨 이야기인지 잘 모르겠다 헤헷. 4장에서 자세하게 알려준다고 한다.
    • read_line() 메서드는 io::Result 타입의 값을 반환한다. io::Result 타입은 I/O 동작 전용 Result 타입이라고 한다. (러스트의 표준 라이브러리에는 이와 같이 서브 모듈 전용의 여러 Result 타입들이 정의되어 있다.)
      • Result 타입은 성공(Ok) 또는 실패(Err)를 나타내는 타입이다.
      • Result 타입은 열거형(enums)으로서, '정해진 값'들만 가질 수 있다. 이때 '정해진 값'들을 variants라고 하는데, Result의 variants로는 OkErr가 있다.
      • Ok는 성공적으로 생성된 결과값을 가지고 있고, Err는 처리가 실패한 사유에 대한 정보를 가지고 있다.
      • Result 타입은 에러 처리를 위한 정보를 나타내는 데 쓰인다.
      • io::Result 인스턴스는 expect 메서드를 갖는다. expect 메서드를 호출하지 않으면 컴파일은 되지만 다음과 같이 'ResultErr variant일 수 있으므로 해당 에러에 대한 처리가 필요하다'는 경고 메시지가 뜬다.

    • expect 메서드를 호출하면, io::Result 인스턴스의 값에 따라 다음과 같이 동작한다.
      • io::Result 인스턴스의 값이 Ok일 경우: expect 메서드는 Ok 값이 갖고 있는 결과값을 반환한다.
      • io::Result 인스턴스의 값이 Err일 경우: expect 메서드는 프로그램을 종료하고, 인자로 전달 받은 메시지(본 예제에서는 '입력한 값을 읽지 못했습니다.')를 출력한다.
  • println!("입력한 값: {}", guess);
    • 여기서 중괄호는 placeholder로서, 두 번째 인자로 전달한 값이 이 자리에 표시된다.

 

2편에선 난수를 생성하는 방법에 대해 알아보자.

 


참고

https://rinthel.github.io/rust-lang-book-ko/ch02-00-guessing-game-tutorial.html

복사했습니다!