okja.me icon okja.me

Post

Rust 기본 문법 빠르게 훑기

변수, 함수, 조건문, 반복문, 소유권까지 Rust 입문에서 바로 마주치는 기본 문법 정리.

2026년 3월 15일

Writing Notes
#rust#beginner

Rust를 처음 볼 때 가장 낯선 부분은 문법 자체보다도 ownershipborrowing 개념입니다. 그래도 시작은 평범합니다. 변수, 함수, 조건문, 반복문부터 익히면 읽는 속도가 금방 붙습니다.

변수와 상수

Rust의 기본 변수는 불변입니다.

fn main() {
    let name = "Ferris";
    let mut count = 1;

    count += 1;

    println!("{} {}", name, count);
}
  • let은 변수 선언입니다.
  • mut를 붙이면 변경 가능한 변수입니다.
  • 상수는 const를 사용하고 타입을 반드시 적어야 합니다.
const MAX_USERS: u32 = 100;

타입과 추론

Rust는 타입 추론을 잘하지만, 필요한 순간에는 명시하는 편이 좋습니다.

let age: u32 = 30;
let price = 9.99_f64;
let is_ready = true;
let initial = 'R';

자주 보는 기본 타입은 아래 정도입니다.

  • 정수: i32, u32, usize
  • 실수: f32, f64
  • 불리언: bool
  • 문자: char
  • 문자열 슬라이스: &str
  • 가변 문자열: String

함수

함수는 fn으로 선언하고, 반환 타입은 -> 뒤에 씁니다.

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

Rust에서는 마지막 표현식에 세미콜론을 붙이지 않으면 그 값이 반환됩니다.

fn square(x: i32) -> i32 {
    x * x
}

조건문

if는 조건식에 괄호가 필요 없고, 표현식으로도 사용할 수 있습니다.

let score = 85;

let grade = if score >= 80 { "pass" } else { "fail" };

조건식은 반드시 bool이어야 합니다. JavaScript처럼 truthy, falsy로 처리되지 않습니다.

반복문

Rust에는 주로 loop, while, for를 씁니다.

for n in 1..=3 {
    println!("{}", n);
}
let mut x = 0;

while x < 3 {
    x += 1;
}

loop는 무한 반복이지만 break와 함께 값을 반환할 수도 있습니다.

let mut n = 0;

let result = loop {
    n += 1;

    if n == 3 {
        break n * 10;
    }
};

문자열과 소유권

Rust 문자열은 처음부터 구분해서 보면 편합니다.

let a = "hello"; // &str
let b = String::from("world"); // String

&str은 보통 문자열 데이터를 빌려 보는 참조이고, String은 힙에 데이터를 저장하며 그 값을 직접 소유합니다. Rust의 핵심은 “하나의 값에는 어느 시점에 누가 책임을 지는가”를 컴파일 타임에 분명히 하는 데 있습니다.

소유권 규칙은 아주 단순하게 시작할 수 있습니다.

  • 각 값은 하나의 owner를 가집니다.
  • owner가 scope를 벗어나면 값은 정리됩니다.
  • owner가 바뀌면 이전 owner는 그 값을 더 이상 사용할 수 없습니다.
fn main() {
    let s = String::from("rust");

    {
        let inner = s;
        println!("{}", inner);
    }

    // println!("{}", s); // 이미 소유권이 이동해서 사용할 수 없음
}

위 예제에서 s의 소유권은 inner로 이동합니다. 그리고 inner가 scope를 벗어날 때 문자열 메모리도 함께 정리됩니다. 이 방식 덕분에 같은 메모리를 두 번 해제하는 실수를 줄일 수 있습니다.

이동과 복사

모든 대입이 소유권 이동을 의미하는 것은 아닙니다. 스택에 저장되는 단순한 값은 Copy가 되어 복사됩니다.

let x = 10;
let y = x;

println!("{} {}", x, y); // 둘 다 사용 가능

반면 String처럼 힙 데이터를 관리하는 타입은 기본적으로 이동합니다.

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

// println!("{}", s1); // move 발생
println!("{}", s2);

둘 다 유지하고 싶다면 명시적으로 복제해야 합니다.

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

println!("{} {}", s1, s2);

clone()은 실제 데이터 복사를 수행하므로 비용이 있습니다. Rust가 자동 복사를 기본값으로 두지 않는 이유도 이 비용과 소유권 책임을 명확하게 드러내기 위해서입니다.

함수 호출과 소유권

함수에 값을 넘길 때도 같은 규칙이 적용됩니다.

fn take(text: String) {
    println!("{}", text);
}

fn main() {
    let name = String::from("Ferris");
    take(name);

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

함수에서 값을 계속 쓰고 싶다면 참조를 넘기면 됩니다.

fn print_len(text: &String) {
    println!("{}", text.len());
}

fn main() {
    let name = String::from("Ferris");
    print_len(&name);
    println!("{}", name); // 여전히 사용 가능
}

실무에서는 &String보다 &str를 더 자주 받습니다. 더 유연해서 문자열 슬라이스와 String 모두에서 받을 수 있기 때문입니다.

fn print_text(text: &str) {
    println!("{}", text);
}

Borrowing과 참조 규칙

참조는 ownership을 넘기지 않고 잠깐 빌려 쓰는 방식입니다. 다만 Rust는 빌리는 방식에도 규칙을 둡니다.

  • 같은 시점에 여러 개의 불변 참조는 허용됩니다.
  • 같은 시점에 가변 참조는 하나만 허용됩니다.
  • 불변 참조와 가변 참조는 동시에 공존할 수 없습니다.
let s = String::from("hello");
let r1 = &s;
let r2 = &s;

println!("{} {}", r1, r2);

위 코드는 읽기 전용 참조만 있으므로 안전합니다. 하지만 아래는 허용되지 않습니다.

let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;

// println!("{} {}", r1, r2); // 동시에 존재하면 컴파일 에러

읽는 쪽과 쓰는 쪽이 동시에 존재하면 데이터 경쟁 가능성이 생기기 때문입니다. Rust는 이 문제를 런타임이 아니라 컴파일 단계에서 차단합니다.

가변 참조

값을 수정하려면 변수 자체도 mut여야 하고, 참조도 &mut여야 합니다.

fn append_world(text: &mut String) {
    text.push_str(" world");
}

fn main() {
    let mut greeting = String::from("hello");
    append_world(&mut greeting);
    println!("{}", greeting);
}

가변 참조가 하나만 허용되는 이유는 “누가 지금 이 값을 바꾸는가”를 단 하나로 제한하기 위해서입니다. 처음엔 엄격해 보이지만, 상태 변경 흐름을 따라가기가 훨씬 쉬워집니다.

Dangling Reference를 막는 방식

Rust는 이미 사라진 값을 가리키는 참조도 허용하지 않습니다.

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

이 코드는 함수가 끝나면 s가 제거되므로 안전하지 않습니다. 대신 소유권을 반환해야 합니다.

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

이런 제약 덕분에 null 포인터나 use-after-free 계열 버그를 초반부터 줄일 수 있습니다.

구조체

관련 있는 데이터를 묶을 때는 구조체를 사용합니다.

struct User {
    name: String,
    age: u8,
}

fn main() {
    let user = User {
        name: String::from("Okja"),
        age: 28,
    };

    println!("{}", user.name);
}

enum과 match

Rust에서는 enummatch를 매우 자주 씁니다.

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

fn describe(dir: Direction) -> &'static str {
    match dir {
        Direction::Up => "up",
        Direction::Down => "down",
        Direction::Left => "left",
        Direction::Right => "right",
    }
}

match는 가능한 경우를 빠짐없이 처리해야 해서 분기 로직이 안전해집니다.

마무리

처음에는 문법보다 소유권 규칙이 더 크게 느껴질 수 있습니다. 하지만 변수 선언, 함수, 제어 흐름, struct, enum, 참조 개념까지 익히면 Rust 코드를 읽는 데 필요한 최소 기반은 갖춘 셈입니다.

다음 단계에서는 Result, Option, Vec, slice, trait 순서로 넘어가면 흐름이 좋습니다.