Post
Rust 기본 문법 빠르게 훑기
변수, 함수, 조건문, 반복문, 소유권까지 Rust 입문에서 바로 마주치는 기본 문법 정리.
2026년 3월 15일
Rust를 처음 볼 때 가장 낯선 부분은 문법 자체보다도 ownership과 borrowing 개념입니다. 그래도 시작은 평범합니다. 변수, 함수, 조건문, 반복문부터 익히면 읽는 속도가 금방 붙습니다.
변수와 상수
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에서는 enum과 match를 매우 자주 씁니다.
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 순서로 넘어가면 흐름이 좋습니다.