Page

관련 포스팅

2023년 9월 9일 토요일

Week01 - Rust 기초 문법 정리1 (cargo, rustup, rustc, ; statement와 return 문)


이번 포스팅에서는 수업시간에 배웠던 Rust의 기초 문법들에 대해서 정리해 보겠다.

우선 Rust 기초 문법에 들어가기 앞서, 이 언어를 코딩하고 사용하기 위해서 알아야 할 것들에 대해서 잡고 가보겠다.


Rust의 공식 패키지 및 빌드 도구: Cargo


cargo는 Rust 프로그래밍 언어의 공식 패키지 관리자이자 빌드 도구이다. cargo를 사용하면 Rust 프로젝트의 빌드, 테스트, 문서화, 배포 등 다양한 작업을 쉽게 수행할 수 있다.

이를 다른 언어의 도구와 비교하자면, Java의 Maven이나 Gradle, Python의 pip와 비슷한 역할을 한다.

Java의 Maven/Gradle과 비교

  • Maven과 Gradle은 Java 프로젝트를 빌드하고, 의존성을 관리하는 도구다.
  • pom.xml 또는 build.gradle 파일에서 의존성을 명시적으로 관리한다.
  • mvn compile, mvn package 또는 gradle build 등의 명령어로 프로젝트를 빌드한다.

Python의 pip과 비교

  • pip은 Python 패키지를 설치하고 관리하는 도구다. requirements.txt 파일에서 의존성을 명시할 수 있다.
  • pip 자체는 빌드 도구가 아니지만, Python 패키지를 설치할 때 필요한 빌드 작업을 수행한다.

공통점과 차이점

공통점

  • 의존성 관리: 모든 도구들은 프로젝트의 의존성을 관리해준다.
  • 프로젝트 빌드: cargo, Maven, Gradle은 명령어 하나로 소스 코드를 빌드할 수 있다.
  • 테스트 지원: 테스트 코드를 쉽게 실행할 수 있는 기능을 제공한다.

차이점

  • 언어
    • 각 도구는 해당하는 언어에 특화되어 있다. (cargo는 Rust, Maven/Gradle은 Java, pip는 Python)
  • 표준화
    • cargo는 Rust의 공식 도구로, 거의 모든 Rust 프로젝트에서 사용된다. 반면에 Java나 Python은 여러 빌드 도구나 패키지 관리자를 선택할 수 있다.
  • 기능 범위
    • Maven과 Gradle은 빌드 라이프사이클을 세밀하게 제어할 수 있는 반면, cargo는 Rust 생태계에 특화된 간결하고 직관적인 기능을 제공한다.
이렇듯 cargo, Maven, Gradle의 컨셉은 비슷하면서도 각 언어와 생태계에 따라 특화된 기능과 사용성을 제공한다고 볼 수 있다.

cargo의 주요 기능

  • 빌드
    • cargo build 명령어를 이용해 소스 코드를 컴파일할 수 있다. 릴리스 모드에서 빌드하려면 --release 플래그를 추가한다.
  • 의존성 관리
    • Cargo.toml 파일에서 프로젝트의 의존성을 관리한다. cargo는 자동으로 의존성과 함께 해당 패키지를 빌드한다.
  • 테스트
    • cargo test 명령어를 사용해 프로젝트의 테스트 케이스를 실행할 수 있다.
  • 문서화
    • cargo doc 명령어로 프로젝트와 의존성에 대한 문서를 생성할 수 있다.
  • 배포
    • cargo publish 명령어를 통해 패키지를 crates.io에 배포할 수 있다.
  • 새 프로젝트 생성
    • cargo new 명령어를 사용해 새 Rust 프로젝트를 생성할 수 있다.

예시

새 프로젝트 생성

cargo new my_project



프로젝트 빌드


cd my_project
cargo build
cargo build --release // 릴리즈 버전으로 빌



프로그램 실행: 컴파일을 하고 실행한다.


cargo run



테스트 실행


cargo test


의존성 추가 (Cargo.toml에서)


[dependencies]
serde = "1.0"


이러한 기능들은 Rust 개발을 더욱 편리하고, 안정적으로 만들어준다. 특히 의존성 관리와 빌드를 자동화하는 것은 다른 언어의 패키지 관리 도구에서도 흔히 볼 수 있는데, Rust에서는 이러한 도구가 표준으로 제공되기 때문에 개발 생산성이 크게 향상된다.


Rustup: Rust의 인스톨러 및 버전 관리 도구


rustup은 Rust의 인스톨러 및 버전 관리 도구다. rustup을 사용하면 Rust의 여러 버전을 쉽게 설치하거나 변경할 수 있고, 컴파일러 도구체인을 관리할 수 있다.

주요 기능

  • Rust 설치: rustup을 사용해 Rust를 처음 설치할 수 있다.
  • 버전 관리: 여러 버전의 Rust를 설치하고, 특정 프로젝트에 적합한 버전을 사용할 수 있다.
  • 도구체인 설치: nightly, beta, stable 등 다양한 릴리스 채널에서 도구체인을 설치할 수 있다.
  • 타겟 추가: 다양한 플랫폼을 대상으로하는 크로스 컴파일을 지원한다.
  • 업데이트와 제거: 설치된 Rust 버전을 업데이트하거나 제거할 수 있다.

이렇게 rustup을 사용하면 Rust 관련 작업을 훨씬 쉽고 효율적으로 수행할 수 있다. 특히 여러 버전의 Rust를 쉽게 관리할 수 있어서, 프로젝트의 요구 사항에 따라 적절한 버전을 선택하는 것이 용이하다.

그리고 rustup의 또 다른 유용한 기능 중 하나가 바로 Rust 언어와 관련된 문서를 로컬에서 쉽게 볼 수 있다는 것이다. 이 기능은 Rust를 공부하거나 레퍼런스를 빠르게 찾을 때 매우 유용하다. 다음과 같은 명령어로 Rust 문서를 로컬에서 열 수 있다:

rustup doc


이 명령을 실행하면 웹 브라우저가 자동으로 열리고, Rust의 공식 문서를 볼 수 있다. 또한 특정 라이브러리나 API에 대한 문서도 검색할 수 있다.

특정 항목을 바로 찾고 싶다면?
특정 항목에 대한 문서를 바로 보려면 다음과 같이 명령어를 사용할 수 있다.

특정 라이브러리 문서 보기

rustup doc --std


특정 API 문서 보기

rustup doc
rustup doc --book
rustup doc std::collections::vec::Vec

위 명령어를 통해 관련된 문서를 바로 열어주게 되어 보다 빠르게 공부를 하는데 도움을 준.

이렇게 rustup doc 명령어를 잘 활용하면, 인터넷 연결이 없는 환경에서도 문서를 쉽게 참고할 수 있다. 따라서 Rust를 효율적으로 공부하거나 개발할 때 이 명령어는 매우 유용하다.


rustc: 컴파일러


rustc는 Rust 프로그래밍 언어의 공식 컴파일러다. 이 컴파일러는 Rust 소스 코드를 바이너리나 라이브러리 파일로 변환한다. rustc는 기본적으로 LLVM을 백엔드로 사용해, 높은 수준의 최적화와 다양한 타겟 플랫폼을 지원한다.

사용 방법

가장 기본적인 사용 방법은 명령 행에서 다음과 같이 입력하는 것이다.

rustc hello.rs

이 명령은 hello.rs라는 Rust 소스 파일을 컴파일하고, 실행 가능한 바이너리 파일 hello 또는 hello.exe (Windows의 경우)를 생성한다.

주요 옵션

  • -o
    • 출력 파일의 이름을 지정한다. 
    • 예를 들어, rustc hello.rs -o my_program 명령은 my_program이라는 이름의 바이너리를 생성한다.
  • --edition
    • Rust의 에디션(2015, 2018, 2021 등)을 지정한다.
  • --crate-type
    • 라이브러리로 컴파일할 경우 어떤 타입의 라이브러리를 생성할지 지정한다. 
    • 예를 들어, lib, staticlib, dylib 등이 있다.

특징

  • 안전성 검증
    • rustc는 컴파일 시점에 메모리 관련 오류와 데이터 경쟁 등을 체크한다.
  • 최적화
    • LLVM 백엔드를 활용해 높은 수준의 최적화를 수행한다.
  • 다양한 타겟 지원
    • 다양한 플랫폼과 아키텍처를 지원하기 때문에 크로스 컴파일이 가능하다.
  • 에러 메시지
    • Rust 컴파일러는 에러 발생 시 사용자에게 친절한 에러 메시지를 제공한다.

대부분의 Rust 개발자들은 cargo라는 빌드 툴을 사용해 프로젝트를 관리하기 때문에, 일반적으로 rustc를 직접 사용하는 경우는 드물다. 그러나 내부 동작을 이해하거나 빌드 과정을 세밀하게 제어하려는 경우에는 rustc가 유용하게 사용된다.


; statement


;는 Rust에서 문(statement)의 끝을 나타내는 기호다. 이는 C, C++, Java와 유사하다. 여러 문을 같은 스코프 또는 블록 안에 작성할 때, 각 문을 구분하기 위해 ;을 사용한다. 예를 들어:

let x = 5;
let y = 10;
let z = x + y;


위 코드에서 ;는 각 변수 선언과 초기화가 끝났음을 나타낸다.

반환 값이 없는 함수

;은 반환 값이 없는 함수를 호출할 때도 자주 사용된다. 예를 들어,


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

fn main() {
    print_hello();  // 여기에서 `;` 사용
}



Rust에서 함수를 호출할 때, 그 함수가 값을 반환하든 반환하지 않든, 호출 자체는 문(statement)이 될 수 있다. Rust에서는 () 타입을 사용해 "값이 없음"을 나타낸다. 이것을 "유닛 타입(unit type)"이라고 부른다.

반환 값이 없는 함수는 사실상 () 타입을 반환하는 함수로 볼 수 있다. 예를 들어 println! 함수는 아무것도 반환하지 않는다고 말할 수 있는데, 정확하게는 ()를 반환한다.

fn print_hello() {
    println!("Hello, world!");  // 이 함수는 `()`를 반환한다고 볼 수 있다.
}


이제 이 함수를 호출해보자.

fn main() {
    print_hello();  // 여기서 `;`를 사용한다.
}


여기서 ;가 있는 이유는 print_hello() 함수 호출이 문(statement)이기 때문이다. 문은 보통 값을 반환하지 않는 동작을 나타내고, ;는 그러한 동작이 끝났음을 명시한다. 따라서 print_hello() 함수가 끝나고 나면, 다음 동작으로 넘어갈 준비가 되었다는 것을 ;를 통해 명시한다.

간단히 말하면, ;는 "이 작업이 끝났고 다음 작업을 시작해도 좋다"라는 것을 코드에 명시해주는 역할을 한다. 이런 식으로 Rust에서는 반환 값이 없는 함수 호출 후에 ;를 붙여, 그 동작이 끝났음을 표시한다.

;가 없는 경우


Rust에서는 블록의 마지막 표현식에 ;를 붙이지 않으면 그 표현식의 값이 블록의 반환값이 된다. 이런 경우에는 ;를 생략한다.

즉, ; 가 없는 경우는 두 가지이다.
  1. 함수에서 반환값을 명시하려 할 때
  2. 조건문에서 값을 반환하려 할 때

1. 함수에서 반환값을 명시하려 할 때

fn add_one(x: i32) -> i32 {
    x + 1  // 여기에 ;를 붙이지 않는다. 그래서 이 표현식의 값이 함수의 반환값이 된다.
}


이 함수는 x + 1이라는 표현식의 값이 함수의 반환값이 되도록 설계되었다. ;를 붙이지 않음으로써 이를 명시하고 있다.

fn give_me_five() -> i32 {
    5  // 여기에는 세미콜론이 없다. 그래서 5가 반환되는 값이 된다.
}

즉, 함수 내부에서 마지막 줄에 있는 코드는 그 함수의 반환값이 될 수 있다. 이때는 ;를 생략하면 그 줄의 계산 결과가 함수의 반환값이 된다.


2. 조건문에서 값을 반환하려 할 때의 예시는 아래와 같다.

let x = if true {
    1  // 여기에 ;를 붙이지 않으면, 이 값이 if-else 표현식의 값이 된다.
} else {
    0  // 여기에도 ;를 붙이지 않는다.
};


여기에서 if true { 1 } else { 0 } 전체가 표현식이고, 그 값은 1 또는 0이 된다. 마지막에 ;를 붙이지 않아서 이 값을 x에 할당할 수 있다.

let x = if true {
    10  // 세미콜론이 없어서, 이 값 10이 x에 저장되게 된다.
} else {
    0
};

이처럼 Rust의 if-else문도 값을 반환할 수있다. 이 경우에도 마지막 줄에 ;를 생략하면 그 값이 if-else 전체의 반환값이 된다.

보다 쉬운 이해를 위해 추가적인 if-else의 예시를 더 살펴보겠다.

1. 짝수와 홀수 판별하기
let number = 7;
let is_even = if number % 2 == 0 {
    true  // 여기에는 세미콜론이 없어서 이 값이 is_even에 저장된다.
} else {
    false
};


2. 최대값 찾기
let a = 5;
let b = 10;
let max_value = if a > b {
    a  // 세미콜론이 없으니, a가 더 크면 max_value는 a가 된다.
} else {
    b  // b가 더 크면 max_value는 b가 된다.
};


3. 문자열 길이 비교
let str1 = "apple";
let str2 = "banana";
let longer_string = if str1.len() > str2.len() {
    "apple"  // 여기에도 세미콜론이 없으니, "apple"이 longer_string에 저장되게 된다.
} else {
    "banana"
};


결론적으로, ;를 생략하는 것이 의미 있는 값이 전달되어야 하는 경우에 사용된다. 이렇게 해서 함수나 블록, 또는 표현식이 값을 반환할 수 있게 한다.

아직은 이 개념이 생소해서 그런지 명확하게 와닿지 않아서 조금 더 쉬운 문장으로 정리를 해보면 이렇다.

Rust에서는 코드를 작성할 때 문장 마지막에 점(.)을 찍는 것처럼 세미콜론(;)을 붙이는 경우가 많다. 이렇게 하면 "이 작업은 끝났어!"라고 컴퓨터에게 알려줄 수 있다.

하지만 때로는 점 대신 느낌표(!)나 물음표(?)를 쓰는 것처럼, 코드에서도 세미콜론을 안 붙일 수 있다. 세미콜론을 안 붙이면 "이 작업이 끝나고 난 값이 중요해! 그 값을 다른 곳에 써야 해!"라는 의미가 된다.

예를 들어, "나는 5살이야"라는 문장이 있으면, 그 뒤에 점을 찍으면 이렇게 된다. "나는 5살이야." 그런데 "너 몇 살이야"라고 물어볼 경우, 마침표 대신에 물음표를 찍는 것이 자연스럽다. "너 몇 살이야?" 이렇게 상황에 따라 다른 기호를 쓸 수 있는 것이다. 이처럼, Rust에서도 중요한 값을 다른 용도로 사용하고 싶을 때는 세미콜론을 안 붙이게 되는 것이다.


;를 넣을 자리에 ;를 생략하면 결과가 달라진다.

앞서서 살펴본대로 세미콜론을 붙이면 그 문장은 '값을 반환하지 않는다'는 의미로 해석이 된다. 이를 Rust에서는 ()로 표시하며, 'unit'이라고 부르는데, 이는 아무런 정보를 가지고 있지 않는 값을 말한다.

예를 들어,

let a = 5;
let b = 10;
let max_value = if a > b {
    a;  // 여기에 세미콜론을 붙임
} else {
    b;
};


이 코드에서 max_value는 ()(unit)이 된다. a나 b 중에서 큰 값을 반환하는 대신, 아무런 정보가 없는 ()가 반환되게 되는거지.

세미콜론을 붙이면 그 줄의 결과값은 무시되고, 아무런 정보가 없는 ()가 반환되니까, 값을 저장하거나 반환하려는 경우에는 세미콜론을 생략해야 된다.

즉, 세미콜론 ;를 붙이면 해당 문장은 실행되지만, 그 문장에서 나오는 값은 반환되지 않는다. Rust에서는 이렇게 값을 반환하지 않는 상태를 ()(unit)이라고 표현한다. 즉, 문장을 실행만 하고 '아무 것도 아닌' 상태를 반환하는 것이다. 따라서 값을 반환하기위해서라면 세미콜론을 생략해야 한다.


fn add(a: i32, b: i32) -> i32 {
    a + b  // 세미콜론을 생략해서 이 값이 반환되게 함
}

let result = add(5, 3);

여기서 add 함수의 마지막 줄에서 세미콜론을 생략했기 때문에 a + b의 계산 결과가 add 함수의 반환값으로 설정되고, 이 값이 result 변수에 저장되게 되는 것이다.

만약 세미콜론을 붙였다면, add 함수는 아무런 값도 반환하지 않게 되어서 ((), unit을 반환) result 변수에는 아무 값도 저장되지 않게 된다.


표현식과 문의 차이

Rust는 표현식(expression)과 문(statement)을 명확하게 구분한다. 표현식은 값을 반환하고, 문은 값을 반환하지 않는다. ;을 표현식 뒤에 붙이면 그 표현식은 문이 되고 값을 반환하지 않게 된다.

예를 들어, 아래 코드에서 let y = x + 1;는 문이다. x + 1은 표현식이지만 ;을 붙여서 문으로 만들었다.

fn main() {
    let x = 5;
    let y = x + 1;  // `x + 1`은 표현식, 전체 `let y = x + 1;`는 문
}



블록의 마지막 표현식

블록도 표현식이 될 수 있다. 블록의 마지막 표현식은 블록의 값을 결정한다. 이때 마지막 표현식 뒤에 ;를 붙이지 않는다.

let x = {
    let y = 5;
    y + 2  // 블록의 마지막 표현식에는 `;`를 붙이지 않는다
};

이 예에서 x는 7이 된다. 만약 y + 2;라고 쓰면, 블록의 값은 ()가 되고 x도 ()가 된다.


rust에서는 return문이 없는 걸까?


아니, Rust에서는 return 문이 있어. return 문을 사용하면 함수에서 값을 명시적으로 반환할 수 있다. 그런데 Rust에서는 보통 함수의 마지막 표현식의 값이 자동으로 반환되기 때문에, return 문 없이도 값이 반환되곤 한다.

예를 들어, 아래 두 함수는 동일하게 작동한다.

// return 문을 사용하는 경우
fn add_with_return(a: i32, b: i32) -> i32 {
    return a + b;
}

// return 문을 생략하는 경우
fn add_without_return(a: i32, b: i32) -> i32 {
    a + b  // 마지막 표현식의 값이 자동으로 반환됨
}


return 문을 사용할 때는 세미콜론 ;을 붙이는 것이 일반적이고, return 문 없이 마지막 표현식의 값이 반환될 때는 세미콜론을 생략해야 된다.

return 문이 유용한 경우도 있다. 예를 들면, 조건문에서 조건에 따라 함수를 중간에 빠져나와야 할 때 return 문을 사용할 수 있다.

그렇다면 return 문이 있는데, 왜 굳이 햇갈리게 ;를 생략하는 방법을 사용하는 걸까?

;를 생략하는 방식이 처음에는 햇갈릴 수 있지만, 일단 익숙해지면 코드를 더 간결하고 읽기 쉽게 만든다. 그리고 return을 생략할 수 있으면 함수의 마지막 표현식이 반환값이 되는 것이 명확해져서 코드를 더 직관적으로 이해할 수 있다.

예를 들어, 아래 두 코드를 비교해 보자.

// return을 사용한 경우
fn add_with_return(a: i32, b: i32) -> i32 {
    return a + b;
}

// return을 생략한 경우
fn add_without_return(a: i32, b: i32) -> i32 {
    a + b
}

두 번째 함수에서는 return이 없기 때문에 코드가 좀 더 간단하다. 그리고 이 간결성은 복잡한 로직이나 여러 개의 조건문, 반복문이 있는 함수에서 더 크게 빛을 발한다. 일반적으로 코드가 간결하면 유지보수도 더 쉽고, 버그 발생 확률도 낮아지는 것도 그 이유 중 하나다.

물론, return 문이 필요한 상황도 있다. 예를 들면, 조건에 따라 함수를 중간에 빠져나와야 하는 경우나 여러 반환 경로가 있는 복잡한 함수에서는 return 문을 사용하게 된다. 그래서 Rust는 두 가지 방식을 모두 제공해서 프로그래머가 상황에 따라 선택할 수 있게 하고 있는 것이다.

변수 앞에 _를 넣는 경우

Rust에서 변수 앞에 _를 붙이면 그 변수가 사용되지 않았을 때에도 경고를 발생시키지 않게 해준다. 일반적으로 변수를 선언하고 사용하지 않으면 Rust 컴파일러는 "unused variable"이라는 경고를 출력한다. 따라서 _를 붙여서 이러한 경고를 회피할 수 있다.

fn main() {
    let x = 42;
}


이 코드를 컴파일하면 x가 사용되지 않았다는 경고가 발생할 것이다. 그런데, 이렇게 하면 경고가 발생하지 않는다.

이 기능은 특히 패턴 매칭이나 열거형을 사용할 때 유용하다. 예를 들어, Result를 사용할 때 성공(Ok)은 처리하고 실패(Err)는 무시하고 싶다면 다음과 같이 할 수 있겠다.

if let Ok(value) = some_function_that_returns_result() {
    // do something with value
}


이 경우 Err는 무시되고 경고가 발생하지 않음. 이렇게 _를 사용하면 코드의 의도가 더 명확해져서 읽기 좋은 코드를 작성할 수 있다.



포스팅이 너무 길어지는 관계로 일단 여기서 끊고 다음 포스팅에서 이어가도록 하겠다.

댓글 없음:

댓글 쓰기

관련 포스팅