PHpullh
심층 가이드/Rust/기초 문법

RUST · 심층 가이드

Rust 기초 문법 완전 정리

불변이 기본인 변수, 망라성을 요구하는 match, null이 없는 자리를 채우는 Option까지 Rust 문법의 기본기를 20개 예제로 다집니다.

주제 20개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

Rust에서 let은 상수 선언에 가깝습니다. 값을 바꾸려면 mut를 명시해야 하고, 같은 이름을 다시 쓰는 섀도잉은 변경이 아니라 새 바인딩을 만드는 별개의 문법입니다. 조건문과 반복문도 문(statement)이 아니라 값을 내는 식(expression)이라, if의 결과를 그대로 변수에 담습니다. 세미콜론 하나를 붙이고 떼는 것이 반환값의 유무를 가르는 언어라는 점부터 몸에 익히는 편이 빠릅니다.

숫자 변수 선언과 문자열 상수 선언에서 타입 추론의 범위를 확인한 다음 if else 분기로 넘어가면, switch 또는 match 사용에서 왜 컴파일러가 모든 갈래를 요구하는지 이해가 됩니다. 그 망라성 검사가 실제로 쓸모를 발휘하는 곳이 null 기본값 처리입니다. 값이 없을 수 있다는 사실이 타입에 적혀 있으니 빠뜨릴 수가 없습니다. 타입 변환은 이 흐름을 마무리하며 암묵적 형변환이 없는 이유를 보여줍니다.

문자열 길이 구하기에서 자주 걸립니다. len()이 돌려주는 값은 문자 수가 아니라 UTF-8 바이트 수라서 한글 한 글자가 3을 더합니다. 사람이 세는 글자 수가 필요하면 chars().count()를 쓰고, 그마저도 결합 문자까지 고려하면 정답이 아닙니다. 같은 이유로 s[0] 같은 인덱싱은 아예 막혀 있습니다. 바이트 경계를 쪼개는 슬라이싱을 하면 컴파일은 통과해도 실행 중에 패닉이 납니다.

01숫자 변수 선언

let count = 1은 타입 표기 없이 기본 정수형 i32로 추론되고, 이어지는 if가 값을 만들어 label에 넣습니다. Rust에서 if는 문이 아니라 식이라 두 갈래가 같은 타입을 내놓아야 하며 여기서는 둘 다 &str입니다. 변수는 기본이 불변이라 값을 바꾸려면 mut를 붙여야 합니다.

Rust code

// 숫자 변수 선언
fn main() {
    let count = 1;
    let label = if count > 5 { "big" } else { "small" };
    println!("{}", label);
}
알아두면 좋은 점

정수 리터럴은 문맥이 없으면 i32로 정해지지만, 뒤에서 u64 자리에 쓰이면 컴파일러가 그 쓰임을 보고 타입을 거꾸로 맞춰 줍니다.

자주 하는 실수

두 갈래가 각각 &str과 String처럼 다른 타입을 내놓으면 if 식 전체의 타입을 정할 수 없어 컴파일이 막힙니다.

02문자열 상수 선언

제목은 문자열 상수지만 코드는 정수 벡터를 만들어 sort로 오름차순 정렬한 뒤 {:?}로 찍습니다. 정렬이 제자리에서 일어나기 때문에 let mut가 필요하고, 반환값이 없어 결과를 다른 변수에 받을 수 없습니다. 진짜 상수는 const NAME: &str처럼 타입을 반드시 적어야 선언됩니다.

Rust code

// 문자열 상수 선언
fn main() {
    let mut values = vec![3, 1, 4, 2];
    values.sort();
    println!("{:?}", values);
}
알아두면 좋은 점

진짜 상수는 const MAX: i32 = 10;처럼 타입을 반드시 적어야 하고, 값이 쓰이는 자리마다 그대로 펼쳐지므로 주소를 가지지 않습니다.

자주 하는 실수

values.sort()는 정렬된 새 벡터가 아니라 ()를 반환하므로, let sorted = values.sort();로 받으면 손에 남는 것은 빈 튜플뿐입니다.

03불리언 플래그 만들기

count > 5라는 비교 결과를 곧바로 if에 넣어 문자열 두 개 중 하나를 고릅니다. count가 3이라 작은 쪽이 나옵니다. 플래그로 쓰려면 비교식을 bool 변수에 담으면 되고, Rust는 정수나 빈 문자열을 참·거짓으로 자동 해석하지 않아 조건 자리에는 오직 bool만 올 수 있습니다.

Rust code

// 불리언 플래그 만들기
fn main() {
    let count = 3;
    let label = if count > 5 { "big" } else { "small" };
    println!("{}", label);
}
알아두면 좋은 점

비교 결과 자체가 bool 값이라 let is_big = count > 5;처럼 이름을 붙여 두면 같은 판정을 여러 곳에서 재사용할 수 있습니다.

자주 하는 실수

다른 언어처럼 if count라고 적으면 정수를 참·거짓으로 해석해 주지 않아 타입 오류가 나고, 조건 자리에는 오직 bool만 올 수 있습니다.

04문자열 길이 구하기

문자열 길이와는 무관하게 vec![3, 1, 4, 4]를 정렬해 중복된 4가 나란히 붙은 결과를 출력합니다. sort는 안정 정렬이라 같은 값끼리 원래 순서가 유지되며, 그 대가로 임시 버퍼를 씁니다. 순서 보존이 필요 없다면 sort_unstable이 추가 할당 없이 처리합니다.

Rust code

// 문자열 길이 구하기
fn main() {
    let mut values = vec![3, 1, 4, 4];
    values.sort();
    println!("{:?}", values);
}
알아두면 좋은 점

sort는 안정 정렬이라 값이 같은 원소끼리 원래 순서가 유지되므로, 두 기준으로 차례차례 정렬해 다중 키 정렬을 만들 수 있습니다.

자주 하는 실수

속도만 보고 sort_unstable로 바꾸면 같은 값끼리의 순서가 보장되지 않아, 앞서 맞춰 둔 두 번째 기준이 조용히 흐트러집니다.

05문자열 정리하기

count가 정확히 5라서 > 비교가 거짓이 되고 작은 쪽 라벨이 출력됩니다. 경계값이 어느 편에 붙는지 확인할 때 이렇게 값을 바꿔 가며 돌려 보게 됩니다. 문자열을 다듬는 trim은 원본을 건드리지 않고 잘라 낸 &str를 돌려주므로, 결과를 쓰려면 따로 받아 두어야 합니다.

Rust code

// 문자열 정리하기
fn main() {
    let count = 5;
    let label = if count > 5 { "big" } else { "small" };
    println!("{}", label);
}
알아두면 좋은 점

trim은 원본을 건드리지 않고 앞뒤 공백을 뺀 &str 조각을 돌려주므로, 새 할당 없이 잘라 낸 부분만 가리키는 참조가 생깁니다.

자주 하는 실수

경계를 >로 적으면 5는 작은 쪽에 붙는데, 사양이 5 이상을 크다고 정의했다면 >=여야 하고 이 한 글자가 흔한 경계 버그가 됩니다.

06숫자 합계 계산

합계를 구하는 대신 네 개짜리 벡터를 정렬해 출력합니다. 실제 합은 values.iter().sum::<i32>()로 얻는데, 결과 타입을 적어 주지 않으면 컴파일러가 추론에 실패합니다. 릴리스 빌드에서는 정수 덧셈이 넘쳐도 패닉 없이 값이 뒤집히므로, 큰 값을 더할 때는 checked_add가 안전합니다.

Rust code

// 숫자 합계 계산
fn main() {
    let mut values = vec![3, 1, 4, 6];
    values.sort();
    println!("{:?}", values);
}
알아두면 좋은 점

values.iter().sum::<i32>()처럼 터보피시로 결과 타입을 적어 주면, 받는 변수에 타입을 표기하지 않아도 어느 정수형으로 모을지 정해집니다.

자주 하는 실수

합이 i32 범위를 넘으면 디버그 빌드는 패닉하지만 릴리스 빌드는 조용히 감싸 돌아, 개발 중에는 멀쩡하던 코드가 배포본에서만 틀린 수를 냅니다.

07나머지 연산 확인

count 7과 5를 비교해 큰 쪽 라벨을 고르는 흐름이며 나머지 연산자는 쓰이지 않습니다. Rust의 %는 피제수 부호를 따라가서 -7 % 3이 -1이 되므로, 부호가 섞인 값으로 짝수를 판정할 때는 rem_euclid를 씁니다. 0으로 나누면 컴파일은 통과해도 실행 중 패닉이 납니다.

Rust code

// 나머지 연산 확인
fn main() {
    let count = 7;
    let label = if count > 5 { "big" } else { "small" };
    println!("{}", label);
}
알아두면 좋은 점

항상 0 이상인 나머지가 필요하면 % 대신 rem_euclid를 쓰면 되고, 음수 입력에서도 결과가 양수로 나옵니다.

자주 하는 실수

n % 2 == 1로 홀수를 판정하면 음수에서는 나머지가 -1이라 조건이 거짓이 되므로, 부호가 섞인 값에는 != 0으로 비교해야 합니다.

08if else 분기

분기문 대신 벡터 정렬이 실행되어 1, 3, 4, 8 순서가 출력됩니다. sort는 원소가 Ord를 구현해야 호출할 수 있어서 f64 벡터에는 그대로 쓸 수 없고, sort_by에 partial_cmp를 넘겨야 합니다. NaN 때문에 부동소수점이 전순서를 이루지 못하는 탓입니다.

Rust code

// if else 분기
fn main() {
    let mut values = vec![3, 1, 4, 8];
    values.sort();
    println!("{:?}", values);
}
알아두면 좋은 점

sort는 원소가 Ord를 구현해야 부를 수 있고, 실수 벡터는 sort_by에 partial_cmp를 넘겨 비교 방법을 직접 알려 주어야 합니다.

자주 하는 실수

그 partial_cmp 뒤에 unwrap을 붙인 코드는 데이터에 NaN이 하나라도 섞이면 그 자리에서 패닉으로 멈춥니다.

09switch 또는 match 사용

match 대신 if와 else 한 쌍으로 두 갈래를 나눕니다. 같은 판정을 match count { 0..=5 => ..., _ => ... }처럼 범위 패턴으로 옮길 수 있습니다. match는 모든 경우를 덮지 않으면 컴파일이 거부되기 때문에, 값의 종류가 늘어날 때 빠뜨린 갈래를 컴파일러가 대신 찾아 줍니다.

Rust code

// switch 또는 match 사용
fn main() {
    let count = 9;
    let label = if count > 5 { "big" } else { "small" };
    println!("{}", label);
}
알아두면 좋은 점

match에 0..=5 같은 범위 패턴을 쓰면 조건이 늘어나도 else if 사슬이 길어지지 않고 갈래가 나란히 보입니다.

자주 하는 실수

마지막에 _를 습관적으로 붙여 두면 나중에 열거형에 변형을 추가해도 컴파일러가 누락을 알려 주지 못하고 새 값이 기본 갈래로 흘러갑니다.

10null 기본값 처리

널 처리와는 상관없이 벡터를 정렬해 찍는 코드입니다. Rust에는 널이 없고 값이 없을 수 있는 자리는 Option으로 표현하며, 기본값은 unwrap_or나 unwrap_or_else로 채웁니다. 앞쪽은 인자를 항상 미리 계산하므로 기본값 만들기가 비싸면 클로저를 받는 뒤쪽을 골라야 합니다.

Rust code

// null 기본값 처리
fn main() {
    let mut values = vec![3, 1, 4, 10];
    values.sort();
    println!("{:?}", values);
}
알아두면 좋은 점

Option은 값이 없을 수 있다는 사실을 타입에 적어 두므로, 확인을 빠뜨리면 실행 중 오류가 아니라 컴파일 오류로 먼저 드러납니다.

자주 하는 실수

unwrap_or는 값이 있든 없든 인자를 먼저 계산하므로, 기본값 만들기가 비싸다면 unwrap_or_else로 미뤄야 헛일을 줄일 수 있습니다.

11삼항 연산자 사용

Rust에는 ?: 문법이 없어서, 이 코드처럼 값을 내놓는 if 식이 삼항 연산자 자리를 대신합니다. 조건에 따라 짧은 문자열 하나를 고르는 흔한 형태입니다. else를 생략하면 전체 타입이 ()가 되어 대입이 막히므로, 식으로 쓸 때는 양쪽 갈래가 반드시 있어야 합니다.

Rust code

// 삼항 연산자 사용
fn main() {
    let count = 11;
    let label = if count > 5 { "big" } else { "small" };
    println!("{}", label);
}
알아두면 좋은 점

if가 식이라 결과를 그대로 대입할 수 있고, 각 블록의 마지막 식이 그 갈래의 값이 되므로 별도의 대입문이 필요 없습니다.

자주 하는 실수

갈래 끝에 세미콜론을 붙이면 그 블록의 값이 ()가 되어 반대쪽 &str과 타입이 어긋나고, 원인을 알기 어려운 타입 오류가 납니다.

12for 반복문

반복문 없이 sort 한 번으로 순서를 정리합니다. 직접 for value in &values로 돌면 원소를 빌려 읽기만 하고, &를 빼면 벡터가 통째로 소비되어 뒤에서 다시 쓸 수 없습니다. 정렬은 원소 수에 로그를 곱한 만큼 비교하므로, 최댓값 하나만 필요하다면 iter().max()가 더 싸게 끝납니다.

Rust code

// for 반복문
fn main() {
    let mut values = vec![3, 1, 4, 12];
    values.sort();
    println!("{:?}", values);
}
알아두면 좋은 점

for v in &values는 원소를 빌려 읽기만 하므로 반복이 끝난 뒤에도 벡터를 그대로 쓸 수 있고, 인덱스가 필요하면 enumerate를 덧붙이면 됩니다.

자주 하는 실수

for v in values처럼 &를 빼면 벡터가 반복문에 소비되어, 그 뒤에서 같은 변수를 다시 쓰려 할 때 이동됨 오류가 납니다.

13while 누적 계산

누적 변수 없이 정수 하나를 임계값과 비교해 라벨을 정합니다. 합을 쌓는 while로 바꾸려면 카운터와 누적값 모두 mut여야 하고, 조건을 갱신하는 줄을 빠뜨리면 그대로 무한 루프가 됩니다. 조건 없이 도는 반복에는 loop를 쓰는데, break에 값을 실어 밖으로 돌려줄 수 있는 것은 loop뿐입니다.

Rust code

// while 누적 계산
fn main() {
    let count = 13;
    let label = if count > 5 { "big" } else { "small" };
    println!("{}", label);
}
알아두면 좋은 점

조건 없이 도는 반복에는 loop를 쓰며, break value;로 값을 실어 밖으로 내보낼 수 있는 것은 세 반복문 중 loop뿐입니다.

자주 하는 실수

while 조건에 쓰이는 변수를 본문에서 갱신하는 줄을 빠뜨리면 조건이 영원히 참으로 남아, 컴파일은 통과한 채 프로그램만 멈춰 있게 됩니다.

14break 와 continue

제어 흐름 키워드는 나오지 않고 벡터 정렬 결과만 출력됩니다. break와 continue는 중첩 루프에서 'outer 같은 라벨을 붙여 어느 루프를 벗어날지 지정할 수 있습니다. 정렬된 데이터에서 값을 찾는다면 루프를 처음부터 도는 대신 binary_search로 비교 횟수를 크게 줄일 수 있습니다.

Rust code

// break 와 continue
fn main() {
    let mut values = vec![3, 1, 4, 14];
    values.sort();
    println!("{:?}", values);
}
알아두면 좋은 점

중첩 루프에서는 'outer: for ...처럼 이름을 붙여 break 'outer로 바깥까지 한 번에 빠져나갈 수 있어 탈출용 플래그 변수가 필요 없습니다.

자주 하는 실수

while 안에서 continue를 쓰면 뒤에 있는 증가 문장을 건너뛰어 무한 루프가 되므로, 갱신은 continue보다 앞에 두어야 합니다.

15배열 생성과 순회

배열 대신 정수 변수 하나와 조건식이 전부입니다. Rust의 배열 [i32; 4]는 길이가 타입에 박혀 있어 컴파일 시점에 크기가 정해지고, 길이가 변하는 자료는 Vec을 씁니다. 범위를 벗어난 인덱스 접근은 실행 중 패닉이므로, 실패를 값으로 받고 싶으면 get이 돌려주는 Option을 씁니다.

Rust code

// 배열 생성과 순회
fn main() {
    let count = 15;
    let label = if count > 5 { "big" } else { "small" };
    println!("{}", label);
}
알아두면 좋은 점

배열은 길이가 [i32; 4]처럼 타입에 들어 있어 스택에 놓이고, 길이가 실행 중에 달라져야 하는 자료는 Vec으로 가야 합니다.

자주 하는 실수

values[i]는 범위를 벗어나면 그 자리에서 패닉하므로, 인덱스가 외부 입력에서 왔다면 get(i)가 돌려주는 Option으로 받아야 합니다.

16맵 또는 객체 생성

맵을 만들지 않고 벡터를 정렬합니다. 키와 값의 짝은 HashMap이 맡는데, 삽입 순서를 보존하지 않아 순회 순서가 실행마다 달라질 수 있습니다. 순서가 필요하면 BTreeMap이 키 순으로 정렬해 주지만 조회 비용이 상수 시간 대신 키 개수의 로그에 비례합니다.

Rust code

// 맵 또는 객체 생성
fn main() {
    let mut values = vec![3, 1, 4, 16];
    values.sort();
    println!("{:?}", values);
}
알아두면 좋은 점

HashMap은 조회가 평균 상수 시간이지만 순회 순서가 정해져 있지 않고, 키 순서가 필요하면 BTreeMap이 정렬된 순서로 돌려줍니다.

자주 하는 실수

HashMap 순회 결과를 그대로 출력에 쓰면 실행할 때마다 줄 순서가 달라져, 그 출력을 비교하는 테스트가 이유 없이 실패하곤 합니다.

17정렬 기초

제목은 정렬이지만 코드는 값 하나를 기준과 비교해 문자열을 고르는 조건식입니다. 비교 자체가 정렬의 재료라서, sort_by에 넘기는 클로저도 이런 비교를 Ordering으로 돌려주는 함수입니다. 그 클로저가 일관되지 않은 순서를 반환하면 결과가 뒤죽박죽이 되거나 패닉으로 끝날 수 있습니다.

Rust code

// 정렬 기초
fn main() {
    let count = 17;
    let label = if count > 5 { "big" } else { "small" };
    println!("{}", label);
}
알아두면 좋은 점

sort_by에 넘기는 클로저는 a.cmp(b)처럼 Ordering을 돌려주며, 앞뒤를 바꿔 b.cmp(a)로 적으면 그대로 내림차순이 됩니다.

자주 하는 실수

비교 클로저가 일관되지 않은 순서를 돌려주면, 예를 들어 항상 Less를 내면 결과가 뒤죽박죽이 되거나 표준 정렬이 패닉으로 끝날 수 있습니다.

18튜플 또는 페어 사용

튜플 대신 단일 정수 벡터를 정렬해 출력합니다. 페어를 정렬하면 (i32, &str)처럼 튜플이 사전식으로 비교되어 첫 요소가 같을 때만 두 번째를 봅니다. 튜플은 필드에 이름이 없어 .0, .1로 접근하는데, 원소가 서너 개를 넘어가면 구조체로 옮기는 편이 읽기 쉽습니다.

Rust code

// 튜플 또는 페어 사용
fn main() {
    let mut values = vec![3, 1, 4, 18];
    values.sort();
    println!("{:?}", values);
}
알아두면 좋은 점

튜플은 사전식으로 비교되어 점수와 이름을 순서대로 담으면 점수가 같을 때만 이름을 보게 되므로, 필드 순서만 바꿔 다중 키 정렬을 만들 수 있습니다.

자주 하는 실수

.0과 .1은 무엇을 담았는지 알려 주지 않아 원소가 서너 개를 넘으면 헷갈리기 쉬우니, 그 시점에는 이름 있는 구조체로 옮기는 편이 낫습니다.

19타입 변환

변환 없이 i32 값 하나를 그대로 비교합니다. Rust는 정수끼리도 자동 변환을 해 주지 않아 as나 try_into로 명시해야 합니다. as는 값이 목표 타입에 안 들어가면 조용히 잘라 내지만 try_into는 Result로 실패를 알려 주므로, 범위를 장담할 수 없을 때는 뒤쪽을 씁니다.

Rust code

// 타입 변환
fn main() {
    let count = 19;
    let label = if count > 5 { "big" } else { "small" };
    println!("{}", label);
}
알아두면 좋은 점

i64에서 i32처럼 좁아지는 변환은 try_into()가 Result로 실패를 알려 주므로 범위를 장담할 수 없을 때 안전합니다.

자주 하는 실수

as는 값이 목표 타입에 들어가지 않으면 경고 없이 상위 비트를 잘라 내기 때문에, 큰 수가 갑자기 음수로 바뀌어도 아무 신호가 남지 않습니다.

20날짜 문자열 만들기

날짜와는 무관하게 벡터를 정렬해 {:?}로 출력합니다. 표준 라이브러리에는 달력 개념이 없어 SystemTime은 기준 시각으로부터의 경과만 주고, 사람이 읽는 날짜 형식은 별도 크레이트가 필요합니다. 시간 간격을 잴 목적이라면 시스템 시계가 바뀌어도 역행하지 않는 Instant가 맞습니다.

Rust code

// 날짜 문자열 만들기
fn main() {
    let mut values = vec![3, 1, 4, 20];
    values.sort();
    println!("{:?}", values);
}
알아두면 좋은 점

표준 라이브러리는 달력을 다루지 않고 SystemTime이 기준 시점부터의 경과만 주므로, 사람이 읽는 날짜 형식은 외부 크레이트에 맡기게 됩니다.

자주 하는 실수

시간 간격을 SystemTime으로 재면 시스템 시계가 조정될 때 값이 뒤로 갈 수 있어, 소요 시간 측정에는 단조 증가하는 Instant를 써야 합니다.

정리하며

  • 변경이 필요한 변수에만 mut를 붙이고 나머지는 불변으로 둡니다
  • match는 모든 갈래를 쓰고, _ 와일드카드는 정말 필요할 때만 씁니다
  • 문자열 길이는 목적에 따라 len()과 chars().count()를 구분해 씁니다
  • 정수 오버플로는 디버그 빌드에서 패닉이므로 릴리스 동작과 함께 확인합니다

더 깊이 들어가고 싶다면 Rust 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.