메인 콘텐츠로 건너뛰기

Rust 기본 문법 완벽 정리

요약

개요

Rust(러스트)는 메모리 안전성과 성능을 동시에 추구하는 시스템 프로그래밍 언어로, C/C++ 수준의 성능과 현대적인 문법, 강력한 컴파일 타임 검사 기능을 제공한다.

Generated Image

이 언어의 가장 큰 특징은 소유권(ownership)과 빌림(borrowing) 시스템을 통해 가비지 컬렉터 없이도 메모리 오류와 데이터 경쟁을 방지한다는 점이며, 이 덕분에 안정적인 네이티브 애플리케이션, 서버, 임베디드 소프트웨어 개발에 널리 쓰인다.

Rust의 기본 문법은 다른 언어 경험이 있다면 크게 어렵지 않지만, 소유권과 참조 규칙 등 Rust 고유의 개념에 익숙해지는 과정이 필요하다.


기본 구조와 "Hello, world!"

Rust 프로그램은 보통 main 함수부터 시작하며, println! 매크로를 사용해 화면에 출력한다.

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

fn 키워드는 함수를 정의한다는 뜻이고, 느낌표가 붙은 println!은 함수가 아니라 매크로라는 것을 나타낸다.

Rust 파일의 확장자는 .rs이며, 보통 cargo라는 빌드 도구(패키지 매니저)를 사용해 프로젝트를 관리한다.


변수와 불변성

Rust에서 변수는 기본적으로 불변(immutable)이다. 즉, 한 번 값을 대입하면 바꿀 수 없다.

fn main() {
    let x = 5;
    // x = 6; // 컴파일 에러: 불변 변수는 변경 불가
}

값을 바꾸고 싶다면 mut 키워드를 사용해 가변(mutable) 변수로 선언해야 한다.

fn main() {
    let mut x = 5;
    x = 6; // OK
    println!("x = {}", x);
}

Rust의 불변성이 기본값인 이유는, 의도치 않은 상태 변경을 줄이고 코드의 예측 가능성과 안전성을 높이기 위해서이다.


변수 섀도잉(Shadowing)

Rust에서는 같은 이름의 변수를 다시 선언해 기존 변수를 가리는(shadowing) 문법을 제공한다.

fn main() {
    let x = 5;
    let x = x + 1;   // 새로운 x
    let x = "문자열"; // 또 다른 새로운 x
    println!("x = {}", x); // "문자열"
}

섀도잉은 타입을 바꾸거나, 한 번 계산된 값을 새로운 이름처럼 쓰고 싶을 때 유용하며, mut와 달리 "새 변수를 새로 만든다"는 의미를 가진다.


기본 자료형 (스칼라 타입)

Rust에는 정수, 부동소수점, 불리언, 문자 등 여러 기본 타입이 있다.

  • 정수: i8, i16, i32, i64, i128, isize (부호 있음) / u8, u16, u32, u64, u128, usize (부호 없음)

  • 실수: f32, f64

  • 불리언: bool (true, false)

  • 문자: char (유니코드 스칼라 값 하나)

fn main() {
    let a: i32 = 10;
    let b: u64 = 20;
    let pi: f64 = 3.14;
    let is_rust_fun: bool = true;
    let ch: char = '한';
}

타입 표시는 대부분의 경우 생략 가능하지만, 모호할 때는 명시하는 편이 좋다.


복합 자료형: 튜플과 배열

Rust의 기본 복합 타입으로는 튜플(tuple)과 배열(array)이 있다.

튜플은 서로 다른 타입의 값을 고정된 개수만큼 묶을 수 있다.

fn main() {
    let tup: (i32, f64, &str) = (1, 2.0, "three");
    let (x, y, z) = tup; // 구조 분해
    println!("x = {}, y = {}, z = {}", x, y, z);

    println!("tup.0 = {}", tup.0); // 인덱스로 접근
}

배열은 같은 타입의 값을 고정된 길이로 저장한다.

fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    let repeated = [0; 5]; // [0, 0, 0, 0, 0]

    println!("첫 번째 원소 = {}", arr[0]);
}

배열은 크기가 컴파일 타임에 고정되는 반면, 벡터(Vec<T>)는 런타임에 크기가 바뀌는 동적 배열이다.


함수 기본 문법과 반환값

Rust에서 함수는 fn 키워드로 정의하며, 매개변수와 반환 타입을 명시한다.

fn add(a: i32, b: i32) -> i32 {
    a + b  // 마지막 표현식이 반환값 (세미콜론 없음)
}

fn main() {
    let result = add(3, 4);
    println!("3 + 4 = {}", result);
}

Rust 함수에서 반환값은 return을 써도 되지만, 보통 마지막 표현식의 값이 암묵적으로 반환되며, 이때 끝에 세미콜론이 없어야 한다.


표현식과 문장

Rust는 "표현식 기반" 언어에 가깝다. 대부분의 구성요소가 값을 가지는 표현식이다.

  • 문장(statement): 값을 반환하지 않는 실행 단위 (예: let x = 5;)

  • 표현식(expression): 값을 가지는 코드 조각 (예: 5, x + 1, if 표현식 등)

fn main() {
    let x = {
        let a = 1;
        let b = 2;
        a + b // 블록 전체가 표현식, 값은 3
    };
    println!("x = {}", x);
}

이 패턴은 초기화 로직이 복잡할 때 유용하다.


조건문: if 표현식

if는 조건문이면서 동시에 표현식이기 때문에, 값으로 사용할 수 있다.

fn main() {
    let n = 10;

    if n > 0 {
        println!("양수입니다");
    } else if n == 0 {
        println!("0입니다");
    } else {
        println!("음수입니다");
    }

    let size = if n > 5 { "big" } else { "small" };
    println!("size = {}", size);
}

if 표현식의 각 분기에서 반환하는 타입은 모두 같아야 한다. 하나는 문자열, 다른 하나는 숫자처럼 섞어 쓸 수 없다.


반복문: loop, while, for

Rust는 세 가지 기본 반복문을 제공한다.

무한 루프: loop

fn main() {
    let mut count = 0;

    loop {
        count += 1;
        if count == 3 {
            break; // 루프 종료
        }
        println!("count = {}", count);
    }
}

loop는 항상 반복되므로, breakreturn으로 빠져나와야 한다.

조건 루프: while

fn main() {
    let mut n = 3;

    while n > 0 {
        println!("n = {}", n);
        n -= 1;
    }
}

조건이 false가 되면 루프가 종료된다.

컬렉션 순회: for

for는 범위(range)나 컬렉션을 순회할 때 사용하며, 가장 많이 쓰이는 루프 형태이다.

fn main() {
    for i in 0..5 { // 0,1,2,3,4
        println!("i = {}", i);
    }

    let arr = [10, 20, 30];
    for v in arr.iter() {
        println!("v = {}", v);
    }
}

0..5는 0 이상 5 미만, 0..=5는 0 이상 5 이하를 의미한다.


소유권(Ownership)의 기본 개념

Rust의 핵심은 소유권 시스템으로, 메모리 안전성을 보장하는 기초가 된다.

기본 규칙은 다음과 같다.

  1. 각 값은 정확히 하나의 소유자(owner)를 가진다.

  2. 소유자가 스코프를 벗어나면 값은 자동으로 드롭(drop)된다.

  3. 소유권은 이동(move)할 수 있지만, 동시에 여러 소유자를 가질 수 없다(일반적인 경우).

fn main() {
    let s = String::from("hello"); // s가 String의 소유자
    let t = s;                     // 소유권 이동: s → t

    // println!("{}", s);          // 컴파일 에러: s는 더 이상 유효하지 않음
    println!("{}", t);
}

이 규칙 덕분에, Rust는 가비지 컬렉터 없이도 언제 메모리를 해제해야 하는지 컴파일 타임에 정확히 알 수 있다.


빌림(Borrowing)과 참조(Reference)

소유권을 완전히 넘기지 않고, "잠깐 빌려 쓰는" 개념이 참조(reference)이다.

불변 참조

fn main() {
    let s = String::from("hello");
    let len = length(&s); // &s: s를 빌려서 참조

    println!("'{}'의 길이는 {}", s, len); // s는 여전히 유효
}

fn length(s: &String) -> usize {
    s.len()
}

&T는 불변 참조로, 읽기만 가능하다. 불변 참조는 여러 개가 동시에 존재해도 된다.

가변 참조

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

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

&mut T는 가변 참조로, 값을 변경할 수 있다. 그러나 특정 시점에는 오직 하나의 가변 참조만 존재할 수 있으며, 불변 참조와 가변 참조를 동시에 둘 수 없다.

이 제약은 데이터 경쟁(data race)을 컴파일 단계에서 차단하기 위한 것이다.


문자열: String&str

Rust에는 크게 두 가지 문자열 타입이 있다.

  • &str: 문자열 슬라이스, 보통 리터럴 "hello"의 타입

  • String: 소유권을 가진 가변 문자열, 힙에 저장

fn main() {
    let s1 = "hello";                // &str
    let mut s2 = String::from("hi"); // String

    s2.push_str(", rust!");
    println!("{}", s2);
}

String은 메모리를 소유하므로 크기를 변경하거나 내용 추가가 가능하고, &str은 보통 읽기 전용으로 사용된다.


구조체(Struct) 기본

구조체는 여러 필드를 묶어 하나의 타입으로 정의하는 기능이다.

struct User {
    name: String,
    age: u32,
    active: bool,
}

fn main() {
    let user = User {
        name: String::from("Alice"),
        age: 30,
        active: true,
    };

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

Rust 구조체는 기본적으로 불변이며, 변경하고 싶다면 변수 선언에 mut를 붙여야 한다.

fn main() {
    let mut user = User {
        name: String::from("Bob"),
        age: 25,
        active: false,
    };

    user.age = 26;
}

열거형(Enum) 기본

열거형은 여러 가능한 "케이스"를 하나의 타입으로 표현한다.

enum Message {
    Quit,
    Move { x: i32, y: i32 }, // 구조체 형태 필드
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let m = Message::Write(String::from("hello"));
}

열거형은 패턴 매칭(match)과 함께 사용할 때 매우 강력하다.


패턴 매칭: match 표현식

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

fn main() {
    let c = Coin::Dime;
    println!("{}", value_in_cents(c));
}

match는 모든 가능한 경우를 반드시 다 처리해야 하므로, 빠뜨린 경우를 컴파일러가 잡아줄 수 있다.


모듈과 크레이트 개념

Rust에서는 코드를 모듈(module) 단위로 나누고, 여러 파일과 디렉터리로 구조화할 수 있다.

  • 크레이트(crate): 컴파일 단위, 하나의 라이브러리 또는 실행 파일

  • 모듈(module): 크레이트 내부의 논리적 코드 묶음

간단한 예:

// main.rs
mod math; // math.rs 또는 math/mod.rs를 찾음

fn main() {
    println!("{}", math::add(2, 3));
}
// math.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub 키워드는 다른 모듈에서 접근할 수 있도록 공개(public) 범위를 지정한다.


에러 처리: Result와 Option 기초

Rust는 예외(exception)를 사용하지 않고, ResultOption 타입을 통해 에러를 표현한다.

Option

값이 있을 수도 있고 없을 수도 있는 경우를 표현한다.

fn divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        None
    } else {
        Some(a / b)
    }
}

Result

성공(T) 또는 실패(E)를 표현한다.

use std::fs::File;
use std::io::Error;

fn try_open() -> Result<File, Error> {
    File::open("test.txt")
}

match, ? 연산자 등을 사용해 이 타입들을 다루며, 이를 통해 예외 없는 명시적인 에러 처리가 가능하다.


마무리 및 학습 방향

Rust의 기본 문법은 변수, 자료형, 제어 흐름, 함수, 구조체·열거형, 모듈, 소유권·참조 정도로 요약할 수 있으며, 이들을 이해하면 대부분의 기초 코드를 읽고 쓸 수 있다.

다음 단계로는 제너릭, 트레이트(trait), 수명(lifetime), 비동기 프로그래밍(async/await), 표준 라이브러리 컬렉션(Vec, HashMap 등)을 학습하면 실무 수준의 Rust 개발에 크게 가까워질 수 있다.

#러스트#시스템 프로그래밍#메모리 안전#소유권#기본 문법

이 노트는 요약·비평·학습 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.