패턴 매칭과 제어 흐름 심화 (match, if let, while let)

Rust는 패턴 매칭을 통해 다양한 값 구조를 해체하고 조건에 따라 분기할 수 있도록 지원합니다. match, if let과 while let은 이를 위해 자주 사용하는 문법입니다.


1. 복습: match 표현식

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}
  • enum은 Coin의 종류를 열거하고 있습니다.
  • match는 모든 경우를 명시해야 합니다. 위 경우 Coin의 종류 4개를 모두 다루고 있습니다.
  • 컴파일러가 exhaustiveness(완전성 – 모든 경우를 다룸)을 검사합니다.
  • 함수에서 coin을 인수로 받는데, type은 Coin 열거형이고, 반환 type은 u8로 0과 양수입니다.
  • 열거형을 분기할 때 열거형의 이름에 variant(변형)을 ::으로 연결하며,
    반환 값은 => 다음에 적고, 각각의 경우를 ,로 구분합니다.

위 코드를 실행하려면 main 함수가 아래와 같이 필요합니다.

fn main() {
    let coin1 = Coin::Penny;
    println!("{} 센트", value_in_cents(coin1));
}

Coin의 종류를 Coin:: 다음에 입력하면 value_in_cents 함수에 의해 해당하는 센트를 표시합니다.

{}가 값이 출력될 위치(placeholder)이고, 값은 value_in_cents(coin1)으로 구합니다.


2. 패턴 바인딩

enum Message {
    Hello { id: i32 },
}

fn main() {
    let msg = Message::Hello { id: 42 };

    match msg {
        Message::Hello { id: 1 } => println!("ID는 1"),
        Message::Hello { id } => println!("다른 ID: {}", id),
    }
}
  • Message 열거형은 Hello 구조체를 변형으로 갖으므로 id에 해당하는 정수를 값으로 받습니다.
  • let msg = Message::Hello { id: 42 };
    : Message 열거형으로 Hello 구조체를 만드는데 id는 42입니다.
  • 이 패턴은 Hello { id: i32 }에 매칭되며, id 값을 꺼내서 변수 id에 바인딩합니다.
  • 따라서, 두번째 분기만 있어도 되는데, 1인 경우를 별도의 경우로 빼낸 것입니다.
    다시 말해, 특정 값 비교와 변수 추출을 동시에 할 수 있습니다.

3. 와일드카드와 _, |, .., ..=

fn main() {
    let x = 7;

    match x {
        1 => println!("하나"),
        2 | 3 => println!("둘 또는 셋"),
        4..=6 => println!("4에서 6 사이"),
        _ => println!("기타"),
    }
}
  • |: 여러 패턴 매칭(or).
    위 경우 2 | 3은 2또는 3인 경우가 됩니다.
  • ..=6: 범위 매칭(=은 포함, 없으면 미만).
    위 경우 4..=6은 4이상 6이하가 되며, 4..6은 4이상 6미만이므로 4와 5만 해당됩니다.
  • _: 나머지 모든 경우 (wildcard, else)
  • x의 데이터 형식은 정수이므로 i32가 돼서 부호있는 32비트 정수이므로 -2^31 (약 -21억)부터 2^31 – 1 (약 21억)까지의 값을 표현 가능
  • 위 코드를 실행하면 x가 7이므로 _에 해당되어 “기타”가 출력됩니다.

4. if let 표현식

match가 너무 장황할 때 if let을 사용해 간단히 표현할 수 있습니다.

let some_value = Some(5);

if let Some(x) = some_value {
    println!("값은 {}", x);
} else {
    println!("값이 없습니다");
}
  • Some은 Option enum의 variant중 하나로서, if let을 이용해서 match의 단일 분기를 간단히 표현한 것이며,
    match 연산자를 이용하면 아래와 같이 해야 됩니다.
    match some_value {
        Some(x) => println!("값은 {}", x),
        None => println!("값이 없습니다"),
    }
  • if let Some(x) = some_value는 “x가 5라면”이 되므로, “값은 5″가 출력됩니다.
  • match 연산자의 경우는 None을 사용하는데, if 제어문이라 else를 사용했는데 없어도 됩니다.

5. while let 반복문

let mut stack = vec![1, 2, 3];

while let Some(top) = stack.pop() {
    println!("꺼낸 값: {}", top);
}
  • vec은 아직 설명하지 않았는데, array가 크기가 고정되어 있다면 vec은 가변 길이의 배열입니다.
  • while let을 이용하면 값이 존재하는 동안 반복합니다.
  • Option, Result와 같이 반복 가능한 열거형에 유용
  • 위 코드를 main 함수에 넣고 실행하면 아래와 같이 출력됩니다.

🧠 요약

문법설명
match여러 경우를 완전하게 분기
바인딩패턴 내부에서 변수에 값 대입 가능
_기타 모든 경우 처리
if let한 가지 매칭에 적합한 축약형
while let값이 남아 있는 동안 반복

열거형 (Enums)

Rust는 복잡한 데이터와 다양한 상태를 안전하게 표현할 수 있는 강력한 도구인 열거형(enum)을 제공합니다.
enum은 단순한 값 목록이 아닌, 각 변형(variant)에 고유한 데이터를 담을 수 있어 패턴 매칭(match)과 결합해 매우 유용하게 사용됩니다.


1. 기본 열거형의 정의와 사용

열거형은 여러 개의 이름 있는 변형을 정의하는 타입입니다.
enum 다음에 이름을 입력하고 중괄호 안에 variant(변형)를 입력합니다.

enum Direction {
    North,
    South,
    East,
    West,
}

아래와 같이 함수에 열거형을 사용할 때는 함수명을 적고 인수를 입력하는데, 인수의 형식은 열거형이 됩니다. 그리고, match 흐름 제어 연산자를 사용하는데, match 다음에 인수명을 기재하고, 분기(arm)를 정의하는데 열거형의 이름 다음에 ::을 추가하며, =>을 사용해 실행 코드를 지정합니다.

fn move_to(dir: Direction) {
    match dir {
        Direction::North => println!("북쪽으로 이동"),
        Direction::South => println!("남쪽으로 이동"),
        Direction::East  => println!("동쪽으로 이동"),
        Direction::West  => println!("서쪽으로 이동"),
    }
}

main 함수는 아래와 같이 let을 이용해 direction 변수에 열거형 이름::변형을 입력하고, 위 함수 move_to의 인수로 direction 변수를 입력하면 “북쪽으로 이동”이란 글자가 화면에 표시됩니다.

fn main () {
    let direction = Direction::North;
    move_to(direction); 
}

North를 달리하면 출력 결과가 달라지며, 변형에 없는 값, 아래에서는 North2를 입력하면 “Direction에서 (North2) variant가 발견되지 않는다”라는 에러 메시지가 표시되므로 정확한 입력을 보장할 수 있습니다.


2. 열거형 변형에 데이터 저장

열거형은 각 변형마다 다른 타입의 데이터를 가질 수 있습니다. 이 점이 Rust enum의 강력한 특징입니다.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

각 변형은 정해진 구조의 데이터를 저장할 수 있습니다:

  • Quit은 데이터 없음
  • Move는 구조체 형태(중괄호 사용)
  • Write는 문자열
  • ChangeColor는 튜플 형태(소괄호 사용)
fn process(msg: Message) {
    match msg {
        Message::Quit => println!("종료"),
        Message::Move { x, y } => println!("이동: ({}, {})", x, y),
        Message::Write(text) => println!("메시지: {}", text),
        Message::ChangeColor(r, g, b) => println!("색상 변경: {}, {}, {}", r, g, b),
    }
}

fn main() {
    let msg1 = Message::Quit;
    let msg2 = Message::Move { x: 10, y: 20 };
    let msg3 = Message::Write(String::from("안녕하세요"));
    let msg4 = Message::ChangeColor(255, 0, 0);

    process(msg1);
    process(msg2);
    process(msg3);
    process(msg4);
}

위 코드를 실행하면 아래와 같이 분기에 따라 실행 코드가 화면에 출력됩니다.

종료
이동: (10, 20)
메시지: 안녕하세요
색상 변경: 255, 0, 0

3. match 표현식

enum과 함께 가장 강력하게 사용되는 문법이 match입니다. 모든 경우를 exhaustively(빠짐없이) 처리하도록 강제되어, 안전한 분기 로직을 작성할 수 있습니다.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(String),
}

fn main() {
    
    let coin = Coin::Quarter("New York".to_string());

    match coin {
        Coin::Penny => println!("1원!"),
        Coin::Nickel => println!("5원!"),
        Coin::Dime => println!("10원!"),
        Coin::Quarter(state) => println!("25원 from {:?}", state),
    }
}

  • enum Coin이 네 가지가 있으므로 match 연산자는 네 가지 분기를 모두 작성해야 합니다. 위 코드에서 match 분기의 하나를 주석 처리하고 실행하면

“모든 경우를 소진하지 않았다(none-exaustive patterns)”고 하면서 ‘Coin:Dime” not covered라고 “Dime 코인을 커버하지 않았다”고 합니다.

  • 모든 경우를 망라하기 어렵다면 _를 이용해 위를 제외한 다른 경우는 모두 이에 해당하는 실행 코드를 적용하도록 할 수 있습니다.
    match coin {
        Coin::Penny => println!("1원!"),
        Coin::Nickel => println!("5원!"),
        // Coin::Dime => println!("10원!"),
        // Coin::Quarter(state) => println!("25원 from {:?}", state),
        _ => println!("기타 동전!"),
    }

4. if let 표현식

단일 패턴만 확인하고 나머지는 무시하고 싶을 때는 if let 구문을 이용해 더 간결하게 처리할 수 있습니다.

    let coin = Coin::Penny;
    if let Coin::Penny = coin {
        println!("1원!");
    }

if 문의 내용이 “같다면”인데 let 문이므로 ==이 아니라 =를 사용했다는 것, 그리고 Coin::Penny가 앞에 왔다는 점을 주의해야 합니다. 위 코드를 실행하면 “1원!”가 화면에 출력됩니다.

if let coin = Coin::Penny 이라고 순서를 바꿔 표시하면 에러는 나지 않는데 coin 변수를 사용하지 않았으므로 _coin으로 변수명을 바꾸라는 제안을 합니다.

match보다 간단하지만, 나머지 경우는 무시되므로 사용에 주의가 필요합니다.


5. 열거형은 메서드도 가질 수 있다

열거형도 구조체처럼 impl 블록을 통해 메서드를 정의할 수 있습니다.

enum Status {
    Ready,
    Waiting,
    Error(i32),
}

impl Status {
    fn print(&self) {
        match self {
            Status::Ready => println!("준비 완료"),
            Status::Waiting => println!("대기 중"),
            Status::Error(code) => println!("에러 코드: {}", code),
        }
    }
}
fn main() {
    let status1 = Status::Ready;
    let status2 = Status::Waiting;
    let status3 = Status::Error(404);

    status1.print();
    status2.print();
    status3.print();
}

위 코드를 실행하면 아래와 같이 화면에 표시됩니다.

준비 완료
대기 중
에러 코드: 404

6. Option 열거형

Rust 표준 라이브러리에는 매우 자주 쓰이는 열거형인 Option이있습니다. 이는 값이 있을 수도 있고, 없을 수도 있다는 개념을 타입 시스템으로 안전하게 표현합니다.

enum Option<T> {
    Some(T),
    None,
}

예시:

let some_number = Some(5);
let no_number: Option<i32> = None;

이 방식은 null을 사용하지 않고도 안전하게 결측 값을 표현할 수 있게 해줍니다.

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(n) => Some(n + 1),
        None => None,
    }
}

위 함수 plus_one은 인수 x가 정수(i32)라면 +1을 하고, None이라면 None을 반환하라는 의미인데, Option Enum이라 정수를 그냥 입력하면 안되고, Some 괄호 안에 입력해야 합니다.

fn main() {
    let num1 = Some(5);
    let num2 = None;

    println!("Result 1: {:?}", plus_one(num1)); // Result 1: Some(6)
    println!("Result 2: {:?}", plus_one(num2)); // Result 2: None
}

위와 같이 main 함수를 만들어 실행하면 숫자 5가 Some(5)라고 입력되면 Some(6)이 반환되고, None이 입력되면 None이 반환됩니다.

Rust는 기존의 사고 방식을 모두 바꿔버리니 적응하기 어렵습니다.


마무리

Rust의 열거형은 단순한 열거 상수를 넘어서 다양한 형태의 상태를 표현하는 강력한 수단입니다.특히 Option, Result, match와 결합하면 null, 에러, 상태 관리 등의 문제를 컴파일 타임에 안전하게 해결할 수 있습니다.

구조체 (Structs)

Rust에서 복잡한 데이터를 다루기 위해 사용하는 기본 단위가 바로 구조체(struct)입니다. 구조체는 여러 개의 관련된 데이터를 하나의 타입으로 묶어 표현할 수 있도록 해줍니다. Rust는 구조체와 메서드(impl 블록)를 통해 모듈화, 캡슐화, 데이터 모델링이 가능합니다.


1. 기본 구조체 정의 및 사용

가장 기본적인 구조체는 struct 다음에 구조체 이름을 쓰고, 중괄호 안에 필드의 이름과 타입을 :으로 연결해 선언합니다.

Rust의 구조체 이름 규칙은 대문자 카멜 케이스(Camel case)입니다. 예를 들어 User, MySruct와 같이 단어 시작을 대문자로 하고, _를 사용할 수 있으며 숫자로 시작할 수 없고, 공백이 포함되면 안됩니다.

struct User {
    username: String,
    email: String,
    active: bool,
}

사용할땐 struct의 인스턴스를 생성합니다.

일반 변수 선언할 때와 마찬가지로 let 키워드를 사용하고, 그 다음에 인스턴스 이름을 적고, = 구조체 이름을 적은 다음 중괄호안에 필드의 이름과 값을, key: value 쌍으로 아래와 같이 입력합니다. 구조체는 모든 필드의 타입이 명확해야 합니다.

fn main() {
    let user1 = User {
        username: String::from("alice"),
        email: String::from("alice@example.com"),
        active: true,
    };

    println!("username: {}", user1.username);
}
  • 문자열(String)은 “alice”와 같이 큰따옴표 안에 입력한다고 되는 것이 아니며, String::from(“alice”)라고 하거나, “alice”.to_string()으로 입력해야 합니다.
  • bool(논리값)도 true, false와 같이 모두 소문자로 표기합니다.
  • 필드 값을 끝까지 입력하고, 쉼표가 있어도 문제가 없습니다.
  • 구조체 인스턴스는 tuple과 마찬가지로 . 연산자(notation)로 접근할 수 있습니다.
  • Rust는 사용하는 기호도 여러가지가 사용돼서 복잡합니다.

지금까지 나온 것이
변수의 형식은 : 다음에 표시하고,
println다음에 !를 붙여야 하며,
match 패턴의 경우 => 을 사용해서 실행 코드를 지정하고, else를 _로 표시하며,
숫자 입력시 천단위 구분 기호로 _를 사용하고,
char를 입력할 때는 작은 따옴표, String을 입력할 때는 큰따옴표,
반환 값의 타입을 지정할 때는 ->,
loop label은 ‘로 시작하며,
참조를 표시할 때는 &를 사용하고,
튜플과 구조체의 값을 지정할 때는 .을 사용합니다.


2. 구조체는 소유권을 가진다

Rust에서 구조체는 일반 변수처럼 소유권을 가집니다. 즉, 구조체를 다른 변수로 이동시키면 원래 변수는 더 이상 사용할 수 없습니다.

let user2 = user1; // user1의 소유권이 user2로 이동
// println!("{}", user1.email); // 오류!

필드 하나만 이동하는 경우도 마찬가지입니다.

    let username = user1.username; // 소유권 이동 (user1.username에 대한 소유권은 종료됨)

    // user1.username은 더 이상 유효하지 않음, username 변수가 소유권을 갖게 됨

    println!("username: {}", username);

일부 필드를 참조로 처리하거나 클론(clone)을 사용해야 합니다.

let username = &user1.username;
또는
let username = user1.username.clone();

3. 기존 구조체 인스턴스로 새 구조체 인스턴스 생성하기

구조체 인스턴스를 만들 때 기존 구조체를 기반으로 일부 필드만 바꾸고 싶은 경우, 다음과 같이 .. 문법을 사용하여 나머지는 (user2와) 동일하다고 할 수 있습니다:

let user3 = User {
    email: String::from("bob@example.com"),
    ..user2
};

단, user2는 이후 더 이상 사용할 수 없습니다. 그 이유는 username, email과 active 필드의 소유권이 user3에게로 넘어갔기 때문입니다.

또한 ..user2라고 나머지 필드는 똑같다고 할 때 맨 뒤에 ,를 붙이면 안됩니다. 구조체 정의할 때는 ,로 끝나도 되는 것과 구분됩니다.


4. 튜플 구조체 (Tuple Struct)

필드의 이름이 없고 형식만 있는 구조체도 정의할 수 있습니다. 이를 튜플 구조체라고 하며, 단순한 데이터 묶음에 유용합니다. 구조체 이름 다음이 중괄호가 아니라 소괄호인 것도 다릅니다.

struct Color(i32, i32, i32);

fn main() {
    let red = Color(255, 0, 0);
    println!("Red: {}, {}, {}", red.0, red.1, red.2);
}


5. 유사 유닛 구조체 (Unit-like Struct)

필드가 없는 구조체도 정의할 수 있습니다. 이를 유닛 구조체라고 하며, 마치 빈 enum처럼 동작하거나 타입 태깅 등에 사용됩니다.

struct Marker;

fn main() {
    let _m = Marker;
}

이런 구조체는 메모리를 차지하지 않으며, 값 자체보다 타입에 의미를 둘 때 사용됩니다.


6. 구조체에 메서드 구현

Rust는 구조체에 메서드(method)를 추가할 수 있습니다. impl 블록을 통해 구조체에 동작을 부여할 수 있습니다.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

&self는 해당 메서드가 구조체 인스턴스를 참조로 빌려서 사용한다는 뜻입니다.

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("면적: {}", rect.area());
}

impl 블록 안에는 여러 메서드(함수)를 정의할 수 있으며, 정적 메서드(fn new, 생성자 역할)는 다음처럼 작성합니다:

impl Rectangle {
    fn new(width: u32, height: u32) -> Self {
        Self { width, height }
    }
}

위와 같이 생성자를 선언한 경우 아래와 같이 Rectangle::new 다음의 괄호 안에 필드 이름을 입력할 필요 없이 너비와 높이만을 입력해서 인스턴스를 만들 수 있으며 , 면적을 계산하는 것은 같습니다.

    let rect1 = Rectangle::new(10, 20);
    println!("rect1 면적: {}", rect1.area());

7. 디버깅을위한 #[derive(Debug)]

구조체를 println!으로 출력하려면 Debug 트레이트를 구현해야 합니다.

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 3, y: 7 };
    println!("{:?}", p);
}

위에서 {:?} 포맷은 Debug 형식 출력을 의미하며, 결과는 Point { x: 3, y: 7 }처럼 구조체의 필드 이름과 값을 포함한 형태로 출력됩니다.

그러나, 아래와 같이 #[derive(Debug)]를 주석 처리하고 실행하면 “Point가 {:?}를 사용해서 포맷될 수 없다”는 에러 메시지가 표시됩니다.


마무리

Rust의 구조체는 단순한 데이터 묶음을 넘어서, 로직과 상태를 함께 표현할 수 있는 강력한 도구입니다. 구조체를 메서드와 함께 사용하면 객체지향적 모델도 자연스럽게 구현할 수 있으며, 안전하고 구조화된 데이터 설계가 가능합니다.

참조와 빌림 (References & Borrowing)

Rust의 핵심 개념 중 하나는 메모리 안전성을 컴파일 타임에 보장하는 것으로서 소유권 개념을 기반으로 한 참조(reference)와 빌림(borrowing)을 통해 변수의 소유권을 넘기지 않고도 다른 함수나 변수에서 데이터를 읽거나 수정할 수 있게 해줍니다.

Rust의 참조와 빌림은 처음엔 까다로워 보일 수 있습니다. 하지만 이 시스템 덕분에 Rust는 런타임 오류 없이 안전한 코드 작성이 가능합니다. 메모리 안전성을 수동으로 관리할 필요 없이, 컴파일러가 우리의 실수를 미리 잡아줍니다.


1. 불변 참조 (Immutable Reference)

가장 기본적인 형태는 불변 참조입니다. 값의 소유권을 넘기지 않으면서 읽기 권한만 주는 방식입니다.

fn main() {
    let s = String::from("hello");
    print_str(&s);
    println!("main: {}", s);
}

fn print_str(s: &String) {
    println!("func: {}", s);
}

(소유권이 이동(move)된 경우)
위 글에서 살펴본 바와 같이 main 함수의 print_str의 인수를 &s가 아니라 s로 바꾸고, fn print_str의 인수 s를 &String이 아니라 String으로 바꾸고 실행하면 s의 소유권이 함수로 넘어갔고, 함수 종료와 함께 메모리에서 해제됐기때문에 main함수의 println!문에서 에러가 발생합니다.

(참조한(빌린) 경우)
그래서 s를 함수 실행이후에도 사용하려면 &s라고 표현해서 s를 참조(borrow)해야 합니다. print_str 함수는 s의 복사본(clone)을 받는 것이 아니라 참조된 주소값을 받아 사용합니다. 중요한 점은 빌렸기때문에 print_str 함수 실행이 끝난 뒤에도 s는 여전히 main 함수에서 유효하게 사용 가능하다는 점입니다.

주의해야 할 점은 print_str함수로 인수를 전달할 때도 &s라고 &를 붙이고, print_str함수에도 인수의 타입을 지정할 때도 &String이라고 &표시를 해야한다는 것입니다.

핵심 포인트:
불변 참조는 소유권을 넘기지 않으며, 데이터를 읽기만 할 수 있다.

Rust는 동시에 여러 개의 불변 참조를 허용합니다. 읽기만 하는 작업이므로 충돌이 없기 때문입니다.

fn main() {
    let s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    println!("{}, {}", r1, r2); // OK
}

위 코드를 실행하면 hello, hello라고 화면에 표시됩니다.


2. 가변 참조 (Mutable Reference)

값을 변경하고 싶다면, 가변 참조(mutable reference)를 사용해야 합니다. 불변 참조와 달리, 가변 참조는 값을 수정할 수 있는 권한을 갖습니다.

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("changed: {}", s);
}

fn change(s: &mut String) {
    s.push_str(" world");
}

이 코드에서 &mut s는 가변 참조입니다. change 함수는 s의 실제 데이터를 수정할 수 있습니다. s를 변경할 수 있어야 하기때문에 let으로 s 변수를 선언할 때도 mut를 붙였습니다.

s.push_str(” world”);는 s라는 변수에 ” world”를 추가하는 것입니다.
따라서, 위 코드를 실행하면 화면에 changed: hello world라고 표시됩니다.

신기한 것은 change 함수에서 s의 참조값을 변경하고 값을 반환하지 않았는데도 main 함수의 s값이 변했다는 것입니다. 가변 참조라 main 함수의 s에 영향을 줍니다.

하지만 Rust는 여기서 아주 엄격한 규칙을 적용합니다:

  • 가변 참조는 동시에 하나만 존재 가능
  • 가변 참조와 불변 참조는 동시에 존재할 수 없음
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s; // 오류: 두 개의 가변 참조!
    println!("{}, {}", r1, r2);
}

이 규칙은 데이터 경쟁(data race)을 방지하기 위함입니다. 여러 스레드나 함수에서 동시에 하나의 값을 변경하게 되면 런타임 오류가 발생할 수 있는데, Rust는 컴파일 타임에 이를 미리 막아줍니다.


3. 불변과 가변 참조는 함께 못 쓴다

다음 코드를 보겠습니다.

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &mut s; // 오류: 두 개의 가변 참조!
    println!("{}, {}", r1, r2);
}

불변 참조가 존재하는 동안에는 어떤 가변 참조도 허용되지 않습니다. 이는 데이터 일관성을 지키기 위한 Rust의 철칙입니다.

위 코드를 실행하면 아래와 같이 “불변 참조로 빌렸기때문에 가변 참조로 빌릴 수 없다”는 에러메시지가 표시됩니다.


4. 스코프와 참조

참조가 유효한 범위(scope)는 매우 중요합니다.

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
        r1.push_str("!");
        println!("r1: {r1}"); // r1의 유효 범위 안에서 사용
    } // r1의 유효 범위가 끝났으므로...


    let r2 = &mut s; // 가능!
    r2.push_str(" again");
    println!("r2: {r2}"); // r2의 유효 범위 안에서 사용
}

Rust는 스코프가 겹치지 않는 한, 가변 참조를 연속적으로 허용합니다. 위 예제를 보면 r1의 유효 범위가 끝났기 때문에 가변 참조 r2가 생성될 수 있습니다.

위 코드를 실행하면 r1.push_str(“!”);으로 s 변수에 !가 추가되고, r2.push_str(” again”);로 s에 ” again”이 추가되어
r1: hello!
r2: hello! again
이 출력됩니다.


5. 참조는 기본적으로 일시적

참조는 값의 복사본이 아니라 원본을 일시적으로 사용하는 방식입니다. 다시 말해 원본보다 오래 살 수 없습니다.

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

dangle 함수는 컴파일 에러가 발생합니다. 왜냐하면 dangle 함수 실행결과 &s를 반환하는데, 함수가 끝나면서 s가 메모리에서 해제되기 때문에 참조할 변수 s가 없기 때문입니다. Rust는 이런 dangling reference를 허용하지 않습니다.

따라서, 위 코드를 s를 직접 반환하도록 아래와 같이 dangle 함수를 수정해야 합니다.

fn dangle() -> String {
    let s = String::from("hello");
    s
}

반환 값의 형식에서 &를 제거하고, 반환값에서도 &를 제거하는 것입니다.


6. 요약: 빌림 규칙 정리

  • 불변 참조 &T: 여러 개 가능, 읽기 전용
  • 가변 참조 &mut T: 한번에 하나만 가능, 읽기 + 쓰기
  • 불변 참조와 가변 참조는 동시에 사용할 수 없음
  • 참조는 항상 원본(소유자)보다 먼저 소멸해야 함(dangling reference)

소유권(Ownership)

Rust의 가장 중요한 개념 중 하나는 소유권(Ownership)입니다. 이 개념은 메모리 안전성을 보장하면서도 가비지 컬렉션 없이 성능을 유지하는 핵심 메커니즘입니다. 소유권 개념은 힙에 저장된 데이터에 대해서만 적용되며, 힙의 데이터는 다른 변수에 대입하거나 함수의 인수로 전달 시 이동이 원칙입니다.


스택(Stack)과 힙(heap)

구분스택(Stack)힙(Heap)
저장 방식선입후출 (LIFO)필요할 때 동적으로 저장
속도빠름느림
용도크기가 고정된 데이터 (ex. 정수, 포인터 등)크기가 가변적인 데이터 (ex. String, Vec)
할당 방식컴파일 타임에 크기 결정런타임에 크기 결정
메모리 해제자동 (블록 종료 시)직접 해제 필요 (Rust는 drop)

🔹 기본 규칙

  1. 러스트의 각각의 값은 해당값의 오너(owner)라고 불리우는 변수를 갖고 있다.
  2. 한번에 딱 하나의 오너만 존재할 수 있다.
  3. 오너가 스코프 밖으로 벗어나는 때, 값은 버려진다(dropped).

🔸 문자열 이동(Move)

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}", s1); // 에러! s1은 더 이상 유효하지 않음
}
  • String은 힙(heap)에 저장되는 데이터입니다.
  • s2 = s1은 복사가 아니고, 소유권이 s1에서 s2로 이동(move)됩니다.
  • 따라서, s1은 더 이상 유효하지 않으며, 컴파일 에러가 발생합니다. 그러나, s2를 화면 출력하면 문제 없습니다.
String 타입은 복사가 아니라 이동이 되기 때문에 "이동 후 빌림"오류가 발행합니다.

빨간 색으로 표시된 error만 문제가 되며, 노란 색으로 표시된 warning은 실행에 영향이 없기에 무시하거나 참고만해도 됩니다.


✅ 클론(Clone) 사용

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1: {}, s2: {}", s1, s2);
}
  • .clone()을 사용하면 깊은 복사가 수행됩니다.
  • 깊은 복사는 값(포인터, 길이, 용량)뿐만 아니라 (포인터가 가리키는) 참조된 데이터까지 복사하여 완전히 독립적인 객체를 생성하는 것입니다.

🔸 스택 값은 복사(Copy)

fn main() {
    let x = 5;
    let y = x;

    println!("x: {}, y: {}", x, y);
}
  • 정수형(i32 등)은 스택에 저장되며, Copy 트레잇이 적용되어 자동 복사됩니다.
  • String은 Copy가 아니라 Clone이 필요합니다.

🛠️ 함수와 소유권

fn main() {
    let s = String::from("Rust");
    take_ownership(s);

    // println!("{}", s); // 에러! 소유권이 함수로 이동
}

fn take_ownership(s: String) {
    println!("받은 문자열: {}", s);
}
  • 함수로 전달하면 소유권이 이동되어 원래 변수는 더 이상 사용할 수 없습니다.
  • 위에서 println! 앞의 주석을 ctrl + / 키를 눌러 해제한 후 실행하면 소유권 이동으로 컴파일 오류가 발생합니다.

소유권을 되돌리기

fn main() {
    let s1 = String::from("hi");
    let s2 = return_ownership(s1);
    println!("{}", s2);
}

fn return_ownership(s: String) -> String {
    s
}
  • 위와 같이 하면 return_ownership 함수가 실행 후 String type으로 s를 반환하고, 그 값을 s2가 받기때문에 문제없이 s2를 화면에 출력할 수 있습니다.

🛠️ 스코프와 Drop

스코프를 벗어나면 스택이든 힙이든 저장된 데이터가 메모리에서 해제(Drop)됩니다.

> 힙(heap)

fn main() {
    let s = String::from("Hello");  // s는 문자열 "Hello"를 소유합니다.

    {
        let s1 = s; // s의 소유권이 s1으로 이동합니다. s는 더 이상 값을 소유하지 않습니다.
        println!("s1 = {}", s1);
    } // s1이 스코프를 벗어나고, 값이 해제됩니다.

    println!("s = {}", s); // 에러 발생: s, s1은 더 이상 유효하지 않습니다.
    // println!("s1 = {}", s1); // 에러 발생: s, s1은 더 이상 유효하지 않습니다.
}
  • 중괄호로 둘어쌓인 영역인 Scope입니다. 위 main 함수는 바깥쪽에 스코프가 하나 있고, 그 안에 스코프가 있는 이중 구조입니다. 바깥쪽 스코프의 변수는 안쪽 스코프에도 유효하게 적용되지만, 안쪽 스코프의 변수는 바깥쪽 스코프에서 유효하지 않습니다.
  • 힙의 데이터는 다른 변수에 대입하면 이동됩니다.
  • 안쪽 스코프에서 s1으로 데이터가 이동되었으므로 바깥쪽 스코프의 println!문에 주석을 하고 실행하면 안쪽 println!문은 정상적으로 출력됩니다.
  • 안쪽 스코프를 벗어나면 s1의 데이터가 메모리에서 해제되므로 s1은 더 이상 존재하지 않게 되므로, println!(“s1 = {}”, s1);에서 에러가 발생합니다.
  • 또한 s는 s1으로 이동되었으므로 println!(“s = {}”, s); 실행 시 에러가 발생합니다.

> 스택(stack)

  • 그러나, 스택 데이터인 정수를 s에 대입하면 이동이 아니라 s1으로 복사되고, s1은 스크프를 벗어나면 메모리에서 해제되므로 바깥쪽 스코프의 println!(“s1 = {}”, s1);이 오류가 발생하는 것은 힙 데이터인 String일 때와 같습니다.


그러나, s는 바깥쪽 스코프의 변수이므로 스코프를 벗어나도 해제되지 않고 s를 출력하는 안쪽, 바깥쪽 println!문 모두 정상적으로 작동합니다. 이 때 에러가 발생하는 s1 출력문은 주석으로 처리해야 합니다.

🧠 요약

개념설명
소유권값의 유일한 소유자가 존재
Move소유권이 다른 변수로 이동
Clone깊은 복사 (스택과 힙 데이터를 모두 복사)
Copy스택의 값은 자동 복사 가능
함수 전달인자로 주면 소유권도 함께 전달됨

반복문: loop, while, for

Rust에서는 loop, while, for라는 3가지 반복문을 제공합니다: . 사용법은 다른 언어와 비슷한데, Rust에만 특이한 것은 loop에 라벨을 붙일 수 있고, break 다음에 값을 적어 값을 반환할 수 있으며, for문에서 반복하기 위한 iter 메소드가 있습니다.


🔁 무한 루프: loop

fn main() {
    let mut count = 0;

    loop {
        println!("count: {}", count);
        count += 1;

        if count == 3 {
            break;
        }
    }
}
  • loop는 무한히 반복합니다.
  • break를 통해 루프를 종료할 수 있습니다.
    위 코드를 실행하면 count값이 0에서 1씩 증가하다가 3이면 끝나는데, 화면 출력은 그 전에 되므로 0, 1, 2까지만 출력됩니다.
  • break가 없다면 무한 반복하므로 강제로 종료하려면 Ctrl + C를 눌러야 합니다.

📦 값을 반환하는 loop

fn main() {
    let result = loop {
        let num = 5;
        break num * 2;
    };
    println!("결과: {}", result); // 10
}
  • break 뒤에 값을 적으면 루프가 해당 값을 반환하고 종료합니다.
  • 따라서, 위 코드를 실행하면 화면에 결과: 10이 출력됩니다.

Loop 라벨

여러 개의 loop 사이의 모호함을 제거하기 위해 loop에 라벨을 붙일 수 있습니다.

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}
  • loop 라벨은 ‘로 시작하고, 이름을 붙인 다음 :으로 끝납니다.
  • 그리고 이동할 라벨을 지정할 때는 break 또는 continue 다음에 ‘라벨명; 이라고 적습니다.
    위 코드를 보면 첫번째 break 문에서는 loop 라벨을 지정하지 않았으므로 안쪽 loop를 벗어나서 count += 1;로 이동하는데,
    두번째 break 문은 ‘counting_up이라고 loop 라벨을 지정했으므로 바깥쪽 루프를 벗어나서 println!(“count = {count}”);을 실행하게 됩니다.
  • 따라서, 위 코드를 실행하면
    바깥쪽 loop에서 count 값 0과 remaining 값 10을 출력하고,
    안쪽 loop에서 remaining값을 1 감소시켜 9가 되며 9를 출력한 다음, remaining이 9이므로 break문으로 안쪽 loop를 벗어난 다음,
    바깥쪽 loop에서 count값을 1 증가시켜 1을 만든 다음 1을 출력하고, remaining을 10으로 만들고 .
    안쪽 loop로 들어가 remaining 값 10을 출력하고, remaing을 1감소시켜 9를 만들고 9를 출력한 다음 remaining이 9이므로 다시 안쪽 loop를 벗어나
    바깥쪽 loop에서 count값을 1 증가시켜 2를 만든 다음 2를 출력하고, remaining을 10으로 만들고 .
    안쪽 loop로 다시 들어가 remaining 값 10을 출력하고, count값이 2이므로 break ‘counting_up 문이 실행되어 ‘counting_up에 해당하는 바깥쪽 loop를 끝내고,
    count값 2를 출력하고 실행을 마칩니다.


🔄 조건반복: while

fn main() {
    let mut n = 0;

    while n < 5 {
        println!("n: {}", n);
        n += 1;
    }
}
  • 조건이 true인 동안 반복합니다.
  • 일반적인 조건 반복에 사용됩니다.

🔄 조건 반복: while 2

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}
  • 대괄호로 표시된 [10, 20, 30, 40, 50]은 배열이며 요소들을 불러낼 때는 index를 대괄호 안에 넣으며, index는 0부터 시작합니다.
  • 따라서, 위 코드는 배열 a의 요소를 하나씩 꺼내서 화면에 출력하는 것입니다.

🔂 컬렉션 반복: for

  • 배열, 벡터 등 반복 가능한 값들을 순회할 때 사용합니다.
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

위 ‘while 2’와 달리 index를 사용하지 않고 출력하고 있습니다.

fn main() {
    let numbers = [10, 20, 30];

    for num in numbers.iter() {
        println!("num: {}", num);
    }
}

.iter()를 사용해서 출력할 수도 있습니다.
위 코드를 실행하면 numbers 배열의 값을 하나씩 화면에 출력합니다.

📦 범위 반복

fn main() {
    for i in 1..5 {
        println!("{}", i);
    }

    for i in 1..=5 {
        println!("(inclusive) {}", i);
    }
}
  • rust에서 범위는 ..을 사용해서 표현합니다.
    1..5: 1 이상 5 미만이고,
    1..=5: 5를 포함할 때는 =을 앞에 붙이면 됩니다. 따라서, 1..=5는 1 이상 5 이하입니다.


⏹️ 루프 제어: break, continue

fn main() {
    for i in 0..5 {
        if i == 2 {
            continue; // 2는 건너뜀
        }
        if i == 4 {
            break;    // 4에서 종료
        }
        println!("i: {}", i);
    }
}
  • continue는 이후의 구문을 실행하지 않고 건너뛰는 것이며, break는 반복문은 종료하는 것입니다.
  • 따라서, 위 코드를 실행하면 0, 1, 3만 출력됩니다.

🧠 요약

반복문설명
loop무한 루프, break 필요
while조건 기반 루프
for컬렉션/범위 순회, 안전하고 권장됨

Rust의 상수와 복합 타입 (Compound Types)

Scalar Types에 대해서는 아래 글에서 살펴봤는데, 이번에는 상수와 복합 타입(Compound Types) 튜플과 배열을 살펴보겠습니다. 상수는 변하지 않는 값이며, 튜플은 다양한 타입의 값을 하나의 그룹으로 묶은 것이고, 배열은 동일한 타입의 값을 고정된 크기로 저장합니다.


🔷 상수 (Constants)

상수는 프로그램 전체에서 변하지 않는 값을 나타냅니다.

const MAX_POINTS: u32 = 100_000;

fn main() {
    println!("최대 점수: {}", MAX_POINTS);
}
  • 변수는 let 키워드를 사용하는데, 상수는 const 키워드를 사용하며 함수 외부/내부 모두 선언 가능
  • 타입을 반드시 명시해야 하며(추론 적용되지 않음), 대문자+언더스코어로 표기하는 것이 관례입니다.
  • 위 코드 중 100_000에서 _는 천 단위 구분 기호인 ,를 대체하는 기호입니다.
  • const는 컴파일 타임에 값이 결정됨

참고: let로 선언한 변수는 mut로 변경 가능하지만, const는 절대 변경되지 않습니다.

fn main() {
    const MAX_POINTS: u32 = 100_000;
    MAX_POINTS += 10; // 상수는 변경할 수 없음, 이 줄은 오류를 발생시킴
    println!("최대 점수: {}", MAX_POINTS);
}

MAX_POINTS를 변경하려고 하면 값을 할당할 수 없다는 에러가 발생합니다.


🔷 튜플 (Tuples)

튜플은 다양한 타입의 값을 하나의 그룹으로 묶습니다.

fn main() {
    let person: (&str, u32) = ("Alice", 30);
    let (name, age) = person;

    println!("이름: {}, 나이: {}", name, age);
    println!("튜플 직접 접근: {}", person.0);
}
  • 괄호 안에 콤마로 구분되는 값들의 목록을 작성하여 튜플을 만듭니다.
  • 고정된 길이와 순서를 가지며, 서로 다른 타입 허용
    위 예에서는 &str, 다시 말해 스트링 슬라이스와 u32 정수 타입이 섞여 있습니다.
    타입을 입력하지 않으면 추론되는데, 정수는 i32가 기본 타입이므로 i32로 추론됩니다.
  • . 문법으로 인덱스로 튜플의 요소에 접근 가능
    위 예에서 person.0은 첫번째 값인 이름을 가리키고, .1을 하면 나이를 가리키게 됩니다.

튜플의 구조해체(destructuring)

튜플의 속성인 그룹을 해체하여 각각의 값을 개별 변수에 할당하는 것을 말합니다.

위 예에서 let (name, age) = person; 란 구문을 사용했는데,
person이란 튜플의 첫번째 요소는 name에, 두번째 요소는 age 변수에 할당하는 것입니다.
다시 말해 튜플은 구조해체 또는 .인덱스를 이용해 요소에 접근할 수 있습니다.


🔷 배열 (Arrays)

배열은 동일한 타입의 값고정된 크기로 저장합니다.

fn main() {
    let scores: [i32; 3] = [90, 85, 78];

    println!("첫 번째 점수: {}", scores[0]);

    for score in scores.iter() {
        println!("점수: {}", score);
    }
}
  • 대괄호 안에 값들을 콤마로 구분하여 나열해서 배열을 만듭니다.
  • [i32; 3]와 같이 타입 뒤에 ;(:이 아님)을 붙이고 숫자를 쓰면, i32 타입 3개의 배열 의미
  • let scores = [30; 10]; 이라고 입력하면 scores 배열에 정수 30을 10개 입력한 것이 됩니다.
  • scores[0]처럼 대괄호안에 인덱스를 입력하여 배열의 요소에 접근 가능
  • for와 .iter()를 이용해서 반복 가능

배열(Array)과 벡터(vector)

배열이 유용할 때는 항상 고정된 크기의 요소를 갖는데 비해서 벡터 타입은 유사 집합체로 표준 라이브러리에서 제공되며 크기를 확장 또는 축소가 가능합니다. 배열이나 벡터 중에 뭘 선택해야 할지 확실하지 않은 상황이라면 벡터를 사용하라고 합니다.

유효하지 않은 배열 요소에 대한 접근

아래에서 a배열의 가장 큰 인덱스가 4인데, 10으로 지정하고 cargo run을 하면

fn main() {
    let a = [1, 2, 3, 4, 5];

    let element = a[10];

    println!("The value of element is: {}", element);
}

아래와 같이 길이가 5인데, 인덱스가 10이라는 경계를 벗어난 인덱스 에러가 발행합니다.


🧠 요약

항목설명
const변경 불가능한 상수, 타입 명시 필수
튜플다양한 타입을 그룹화, 순서 중요
배열동일한 타입, 고정된 크기, 인덱스로 접근

함수, if와 match 표현식

함수는 코드의 재사용과 구조화를 위한 기본 단위로서 매개변수와 반환값이 있을 수 있습니다. 또한 if와 match는 중요한 제어 흐름 도구로서, let과 결합하여 변수에 값을 대입하는 표현식도 됩니다. match의 경우 모든 경우를 망라하기 위해 _를 사용하는 것이 특이합니다.


🔧 함수 정의

fn main() {
greet("Rust");
}

fn greet(name: &str) {
println!("Hello, {}!", name);
}
  • fn 키워드로 함수를 정의합니다.
  • 함수는 매개변수와 반환 타입을 명시할 수 있습니다. 그러나, 매개변수나 반환 값이 있다면 반드시 형식(타입)을 지정해야 합니다. 위에서 main 함수에는 매개변수가 없고, greet에는 매개변수 name이 있으므로 형식을 &str로 지정했습니다.
  • &str은 문자열 슬라이스(문자열 참조)입니다.
  • main함수에서 greet 함수를 호출하고, greet 함수의 name 매개변수로 Rust를 전달하고 있으므로, 위 코드를 실행하면 아래 화면과 같이 Hello, Rust!라고 화면에 표시됩니다.
Run을 실행한 결과 Hello, Rust!가 화면에 출력된 화면입니다.

위 화면은 D:\rust-practice 폴더에서 cargo new day3를 실행한 다음 위 코드로 대체하고 실행한 화면입니다.

name 다음의 형식을 제거하고 실행(Run) 하면 아래와 같이 복잡한 에러 메시지가 표시되는데, name에 대한 형식을 지정하라는 의미입니다.

name 다음에 형식 지정이 없어서 지정하라는 에러 화면입니다.

위 화면에서 name 다음에 :을 입력하면 &str이 제시되므로 tab키를 눌러 제안을 수용하면 쉽게 코드를 완성할 수 있습니다.


🔁 반환값이 있는 함수

fn add(a: i32, b: i32) -> i32 {
a + b // 세미콜론 없음 → 반환값
}
  • 함수의 마지막 표현식(Expression)이 반환값입니다. 여기서는 a + b 입니다.
  • -> 다음의 i32가 반환 값의 형식을 지정하는 것입니다.
  • 세미콜론(;)이 붙으면 실행문(Statement)으로 값이 반환되지 않습니다.
  • return키워드를 사용할 수도 있지만, 마지막 줄에 return 없이 값을 놓는 것이 일반적입니다.
fn add(a: i32, b: i32) -> i32{
    return a + b
}
  • 위 함수는 출력문이 없으므로 화면에 어떠한 값도 출력하지 않습니다.
    값을 출력하려면 println! 매크로를 사용해야 합니다.
fn main() {
    let sum = add(5, 10);
    println!("5와 10의 합은: {sum}"); // 15
}

fn add(a: i32, b: i32) -> i32{
    a + b
}

위 코드는 main함수에서 add 함수에 5와 10을 전달하고 a + b의 값을 반환받아 값 15를 sum 변수에 대입한 후 println!를 이용해 “5와 10의 합은: 15″라고 화면에 출력하는 것입니다.


🔸 if 표현식

Rust에서 if는 표현식이며, 값으로 사용할 수 있습니다. 다시 말해 let 예약어를 이용해 변수에 if 표현식으로 결정되는 값을 변수에 대입할 수 있습니다.

fn main() {
let score = 85;
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else {
"C"
};
println!("성적: {grade}");
}
  • if는 블록의 결과를 반환합니다.
  • 각 분기의 결과는 같은 타입이어야 합니다.
  • 위 코드를 실행하면 score가 90보다 작고, 80보다 크므로 “성적: B”가 화면에 출력됩니다.

🔶 match 표현식

match는 패턴 매칭을 제공하는 강력한 제어문입니다.

fn main() {
let number = 3;

match number {
1 => println!("하나"),
2 => println!("둘"),
3 => println!("셋"),
_ => println!("기타"),
}
}
  • _는 위에 해당하지 않는 모든 경우를 의미하는 와일드카드입니다. ‘아무거나(any value)’라고 이해하면 편합니다.
  • 각 분기(arm)에는 =>로 실행 코드를 지정합니다(The => operator that separates the pattern and the code to run).
  • println!를 사용했는데도 ;을 붙이지 않는 점을 주의해야 합니다.
  • 위 코드를 실행하면 “셋”이라고 화면에 표시됩니다.
  • match는 반드시 모든 경우를 처리해야 합니다.
    다시 말해 _가 없으면 “i32 형식에 해당하는 수 중 1,2,3만 처리해서 i32의 최소값부터 0까지와 4부터 i32의 최대값은 커버하지 못했다”고 하는 non-exaustive patterns(총망라 하지 않은 패턴) 에러가 표시됩니다.

또한 아래와 같이 _를 맨 위에 놓으면 ‘모든 경우’가 되므로, number의 값이 1이거나 2 또는 3이더라도 “기타”를 출력하게 됩니다. 1,2,3이 아닌 4인 경우 “기타”를 출력하는 것은 너무나 당연합니다.

fn main() {
    let number = 3;

    match number {
        _ => println!("기타"),
        1 => println!("하나"),
        2 => println!("둘"),
        3 => println!("셋"),        
    }
}

📌 match를 값으로 사용하기

fn main() {
let day = 3;
let weekday = match day {
1 => "월요일",
2 => "화요일",
3 => "수요일",
_ => "기타",
};
println!("요일: {}", weekday);
}
  • match는 if와 마찬가지로 표현식이므로 변수에 바로 match 표현식의 결과 값을 할당할 수 있습니다.
  • 이전 예에서는 => 다음에 println!를 사용했는데, 여기서는 “화요일” 등의 반환값을 지정한 점이 다릅니다.
  • 위 코드를 실행하면 “요일: 수요일”이 출력됩니다.


🧠 요약

  • 함수는 fn으로 정의하며, 매개변수와 반환 타입 지정 가능
  • if와 match는 모두 표현식으로, 값을 반환할 수 있음
  • match는 매우 강력한 패턴 매칭 도구이며, 모든 경우를 반드시 다뤄야 함

변수, 불변성, 데이터 타입

Rust는 안전성을 위해 변수는 기본적으로 불변(immutable) 입니다. 따라서, 필요한 경우 변수의 값을 변경하려면 명시적으로 가변(mut)으로 선언해야 합니다. 변수 선언 시 let을 사용하고, 스칼라 타입은 숫자, 불리언, 문자가 있으며, 타입을 지정해야 하는 정적 타입 언어이나 타입 추론이 가능합니다.


🔹 준비

Code의 터미널 창에서 D:\rust-practice 폴더로 이동한 다음
cargo new day2라고 입력해서 day2 패키지를 만들고
cd day2라고 입력해서 day2 폴더로 이동합니다.

main.rs는 day2 아래 src 폴더에 있습니다.

🔹 변수 선언

fn main() {
let x = 5;
println!("x의 값은: {}", x);
}
  • let 키워드는 변수를 선언합니다.
  • x는 기본적으로 불변 변수입니다. 변수인데 불변이라고 하니 Rust는 참 독특한 개념의 언어입니다.
  • {}는 print, 다시 말해 출력 시 변수가 위치할 자리(placeholder)입니다.

실행

위 코드를 복사해서 에디터의 main.rs의 내용을 덮어쓰고 저장한 다음
cargo run을 실행하면
출력되는 값은 “x의 값은: 5″입니다.

출력 구문 변경

중괄호안에 변수를 넣어
println!(“x의 값은: {x}”);
과 같이 구문을 바꿀 수 있습니다.

main.rs 위의 run 실행

Code를 닫았다가 열면 rust-analyzer가 다시 실행되면서 아래와 같이 main.rs위에 Run|Debug가 생기므로

main.rs 위에 run 명령어가 보이고, 이를 눌러 rust 패키지를 실행할 수 있다.
흰색 동그라미는 파일이 저장되지 않았다는 의미

run을 눌러 실행할 수 있습니다.

cargo run을 할 때는 먼저 파일을 저장해야 하는데 위 run을 누르면 자동으로 파일을 저장하고, cargo run을 실행하므로 너무 편리합니다.

🔁 값을 바꾸려면?

fn main() {
let x = 5;
x = 6; // 에러! 기본 변수는 값 변경 불가
}

오류 메시지: cannot assign twice to immutable variable x
(불편 변수인 x에 값을 두 번 할당할 수 없다)

//는 주석 표시입니다. 다시 말해 // 다음의 문장은 프로그램 실행 시 제외됩니다.

기존 구문을 주석 처리하고 위 구문을 추가할 때
해당하는 2줄을 마우스로 끌어서 선택하고 Ctrl + /키를 누르면 맨 앞에 // 표시가 추가됩니다.

fn main() {
    // let x = 5;
    // println!("x의 값은: {x}");

    let x = 5;
    x = 6; // 에러! 기본 변수는 값 변경 불가    
}

🔧 가변 변수 (mutable)

fn main() {
let mut x = 5;
println!("처음 x: {x}");
x = 6;
println!("바뀐 x: {x}");
}
  • mut 키워드를 사용하면 값 변경이 가능합니다.

실행

실행하면 화면에 아래와 같이 출력됩니다.
처음 x: 5
바뀐 x: 6


🧮 스칼라 타입(Scalar Types)

스칼라(scalar)는 하나의 값으로 표현되는 타입입니다. Rust는 정수(integer), 부동소수형 숫자(floating-point numbers), boolean(논리형), 그리고 문자(char)라는 네 가지 타입을 보유하고 있습니다. Rust는 정적 타입 언어이며, 대부분 타입은 명시하지 않아도 추론됩니다.

정적 타입 언어는 컴파일 시 변수 타입이 결정되는 언어이고, 동적 타입 언어는 런타임 시 변수 타입이 결정되는 언어, 다시 말해, 코드를 실행할 때 알아서 변수 타입을 판단해주는 언어입니다. 그러나 Rust는 정적 타입 언어임에도 변수 타입을 프로그램에서 지정하지 않더라도 변수 타입을 추론한다는 것이 다른 점입니다.

숫자형

정수형은 소수점이 없는 숫자이고, 부동소수형은 소수점이 있는 숫자입니다. 3을 부동소수형으로 표현할 경우 소수점이하가 0이더라도 3.0이라고 써야지 3으로 쓰면 안되며, 최소한 3.이라고 소수점을 찍어야 합니다.

위 코드에서 let a = 10;이라고 변수에 정수를 대입하면 : i32를 입력하지 않아도 rust analyzer가 : i32를 알아서 추가해서 표시합니다. 3.14도 마찬가지로 : f64라는 타입을 자동으로 표시합니다. 이것이 추론입니다.

: i32 또는 : f64가 없어도 프로그램 실행에 전혀 문제가 없으며, 직접 입력하려면 : i32라고 입력해야 합니다. 아래와 같이 Code에서 타입을 입력한 것은 녹색으로 보이고, 추론한 것은 회색으로 보입니다.

숫자형의 종류

불리언 & 문자

let t: bool = true;
let c: char = 'R'; // 유니코드 문자

마찬가지로 : bool을 입려하지 않아도 값이 true이므로 boolean으로 추론되고, 문자 하나를 입력했으므로 : char로 자동으로 표시됩니다. 이때 문자는 반드시 작은따옴표 안에 입력해야 하며, “R”로 바꾸면 에러가 발생합니다.

파이썬은 자유롭게 작은따옴표 또는 큰따옴표를 사용할 수 있는 것과 다릅니다.


※ 문자 한 개(char)가 아니라 “I am a boy”과 같은 문자열은 String 타입으로 구분되므로 나중에 별도로 다루도록 하겠습니다.


📌 변수 섀도잉(Shadowing)

Rust에서는 같은 이름의 변수로 새로운 값을 선언할 수 있습니다.

fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("x의 값: {x}"); // 12
}
  • let을 반복 사용하면 기존 변수를 덮어쓰는(shadowing) 것처럼 새 변수 선언이 가능합니다.
  • 타입을 변경하는 것도 허용됩니다. 아래는 x가 i32 형식이었다가 char 형식으로 변경된 것을 보여줍니다.

‘R’; 다음의 주석 “// 변수 재정의 가능”도 입력한 것이 아니고 rust analyzer가 알아서 입력한 것입니다. 참 똑똑합니다.

위 코드의 출력 값은
x의 값: 12 (x는 5 -> 6 -> 12가 됨)


🧠 정리

  • 변수는 기본 불변 → 변경하려면 mut 사용
  • 정적 타입 → 타입을 명시하는 것이 원칙이지만 타입 추론 가능
  • Shadowing으로 가변처럼 활용 가능하나, 메모리 안정성을 유의해서 사용하지 않는 것이 바람직함

Rust 생태계와 Hello, World! 출력하기

🔸 Rust 생태계

러스트의 생태계는 여러가지 도구들로 구성되어 있으며, 그 중 중요한 것은 rustc, cargo, rustup입니다. 그러나, Visual Studio Code가 있기 때문에 cargo new를 이용해 간단하게 패키지를 만들고, cargo run을 이용해 실행할 수 있습니다.

  • rustc : rust의 확장자는 .rs이며, 이 파일을 바이너리 또는 다른 중간 형식으로 변환해주는 역활을 하는 컴파일러입니다. 그러나 Visual Studio Code(이하 code)를 사용하기때문에 거의 사용하지 않습니다.
  • cargo : rust 실행에 필요한 라이브러리를 관리하고 또한 프로젝트를 관리, 빌드, 테스트하는 도구입니다.
  • rustup : 첫번째 개발환경 세팅에서 언급한 바와 같이 rust를 업데이트해주고, toolchain을 설치하는 역활을 합니다. 여러 개의 rust 버전이 설치되어 있을 때 필요한 것을 선택할 수 있도록 합니다.

🔸 폴더 만들고 Code 실행하기

명령 프롬프트 창을 열고 D드라이브 루트인 상태에서 md rust-practice 라고 입력해서 rust-practice 폴더를 만들고

루트에서 cd rust-practice 폴더로 이동한 다음 code . 이라고 입력하면 rust-practice 폴더에서 code가 실행됩니다.

폴더를 만들고 그 폴더로 이동한 다음 visual studio code를 실행함

code 화면에 terminal이 실행되어 있지 않다면 ctrl + `(키보드 1 왼쪽 키)를 눌러 terminal을 엽니다.

🔸 cargo new

그리고, cargo new day1 이라고 입력하고 엔터 키를 누르면

code 터미널 창에서 cargo new 명령을 이용해 새로운 패키지를 만듭니다.

그러면 왼쪽 탐색기 창을 보면 rust-practice 아래에 preview 폴더가 생기고 오른쪽 터미널 창을 보면 preview라는 바이너리 패키지를 생성하고 있다는 메시지와 Cargo.toml 키를 더 알아보고, 사이트를 참고하라고 합니다.

cargo enw day1이라고 입력해서 day1이라는 패키지를 생성한 화면

다시 왼쪽 탐색기 창의 preview를 열어보면 preview 폴더 아래에 src 폴더가 있고, 그 아래 main.rc 파일이 있고, src 폴더와 동급으로 .gitignore, Cargo.toml 파일이 있습니다.

day1 폴더 아래에 src 폴더가 있고, 그 아래 main.rc 파일이 있으며, src 폴더와 동급으로 .gitignore, Cargo.toml 파일이 있습니다.

Cargo new 패키지명을 입력하면 기본적으로 이런 형식으로 폴더와 파일이 만들어집니다.

🔸 main.rs의 내용

이제 main.rs를 클릭하면 오른쪽 에디터 창에 아래와 같이 표시됩니다.

기본적인 main.rs의 내용은 "Hello, world!를 화면에 출력하라"는 것입니다.

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

fn은 function이라는 의미이고, main은 함수명이며 괄호 안에 인수를 넣는데 괄호만 있으므로 인수가 없는 것입니다.

그리고, 중괄호안에 실행할 내용이 들어가는데,

println!(“Hello, world!”);란 화면에 “Hello, world!를 출력하라”는 의미입니다.

🔸 cargo run

cargo run은 컴파일하고, 패키지를 실행하는 명령어입니다.

터미널에서 day1 폴더로 이동해야 하므로 ‘cd day1’이라고 입력하고 엔터키를 입력한 다음 ‘cargo run’이라고 입력하고 엔터키를 누르면

Compiling과 Finished, Running이라는 글자가 보이고, 화면에 “Hello, world!가 출력됐습니다.

Finished 이전에는 Building이라고 표시되다가 Finished로 바뀌었습니다.

실행 파일은 target 폴더 아래 debug 폴더에 day1.exe란 이름으로 생겼습니다. 실행 파일의 사이즈가 궁금하면 탑색기에서 살펴보기 바랍니다. 그러나 실행은 명령 프롬프트 창에서 해야 합니다.

이렇게 rust 프로그램을 만들고 실행하는 법을 전체적으로 살펴봤습니다.