2편에서 환경을 준비했다면, 이제 변수와 자료형 규칙을 이해해야 본격적으로 코드를 작성할 수 있습니다. Rust는 기본적으로 모든 변수를 불변으로 취급하며, let 선언 시 mut를 명시해야만 값을 바꿀 수 있습니다. 3편에서는 불변성 감각을 잡고, 스칼라 타입과 튜플·배열 같은 복합 타입을 차근차근 익힙니다.
이번 글에서 새로 나오는 용어
- 불변성 (immutability): 변수가 생성된 뒤 값을 바꿀 수 없는 성질로, Rust에서는 기본 동작입니다.
- 스칼라 타입: 한 번에 하나의 값을 표현하는 타입(i32, f64, bool, char 등)을 가리킵니다.
- 튜플 (tuple): 서로 다른 타입의 값을 순서 있게 묶는 고정 길이 컬렉션입니다.
- 배열 (array): 같은 타입의 값을 고정 길이로 저장하는 컬렉션으로, 컴파일 타임에 길이가 정해져야 합니다.
핵심 개념
let으로 선언한 변수는 기본적으로 불변이며,let mut를 사용해야 값 변경이 가능합니다.- 같은 이름으로
let을 다시 쓰는 shadowing은 기존 변수를 수정하는 것이 아니라, 같은 이름의 새 변수를 다시 선언하는 방식입니다. - Rust는 타입 추론을 지원하지만, 복잡한 상황이나 명시성이 필요한 경우
: 타입구문으로 타입을 지정합니다. - 스칼라 타입은 정수(
i32,u64), 실수(f32,f64), 불리언(bool), 문자(char)가 중심입니다. - 튜플은 여러 값을 순서로 묶을 때 사용하며, 점(
.) 인덱싱이나 패턴 분해로 접근합니다. - 배열은 길이가 고정된 동일 타입 컬렉션이며, 슬라이스(7편)로 확장하기 전 단계에서 감각을 쌓습니다.
코드로 따라하기
1. let과 mut
fn main() {
let language = "Rust"; // 불변
let mut version = 1; // 가변
println!("{} v{}", language, version);
version += 1;
println!("다음 버전: v{}", version);
}
language는 불변이라 값을 바꾸려 하면 컴파일 오류가 발생합니다. version은 mut 덕에 증가시킬 수 있습니다.
2. 스칼라 타입 명시
fn main() {
let signed: i32 = -42;
let unsigned: u64 = 2026;
let pi: f64 = 3.14159;
let ready: bool = true;
let grade: char = 'A';
println!("signed = {signed}, unsigned = {unsigned}");
println!("pi = {pi}, ready = {ready}, grade = {grade}");
}
Rust는 리터럴에 접미사를 붙여 타입을 지정할 수도 있습니다(42u8, 1.0f32). 자주 쓰는 기본 정수는 i32, 기본 실수는 f64입니다.
3. shadowing
fn main() {
let steps = 5;
let steps = steps + 1;
println!("steps = {steps}");
}
여기서 두 번째 let steps = ...는 기존 변수를 수정한 것이 아니라, 같은 이름의 새 변수를 다시 선언한 것입니다. 그래서 shadowing은 mut와 다릅니다. mut는 같은 변수를 바꾸는 것이고, shadowing은 같은 이름으로 새 변수를 덮어쓰듯 다시 만드는 방식입니다.
4. 튜플
fn main() {
let user: (&str, u32, bool) = ("Jisoo", 3, true);
println!("이름: {}", user.0);
println!("레벨: {}", user.1);
println!("활성 여부: {}", user.2);
let (name, level, active) = user; // 패턴 분해
println!("{name} / {level} / {active}");
}
튜플은 길이가 고정되어 있고, 서로 다른 타입을 함께 묶을 수 있다는 점이 특징입니다. 처음에는 user.0, user.1처럼 점 인덱싱에 먼저 익숙해지고, 패턴 분해는 "이런 방식도 있다" 정도로 받아들이면 충분합니다.
5. 배열
fn main() {
let scores = [95, 88, 76, 100];
let zeros = [0; 5]; // 길이 5, 모든 요소 0
println!("첫 점수: {}", scores[0]);
println!("배열 길이: {}", scores.len());
for score in scores {
println!("점수: {score}");
}
}
배열 길이는 타입 일부이기 때문에 let data: [u8; 3] = [1, 2, 3];처럼 명시할 수 있고, 길이가 다른 배열끼리는 타입이 달라집니다.
튜플과 배열의 차이는 아래처럼 정리하면 쉽습니다.
- 튜플: 서로 다른 타입을 함께 묶을 수 있음
- 배열: 같은 타입만 묶을 수 있음
- 둘 다: 길이가 고정됨
왜 중요한가
- Rust의 불변 기본값은 값이 예상치 못하게 바뀌는 버그를 줄이고, 가변 상태를 의도적으로 드러내도록 강제합니다.
- 타입을 명시적으로 다루면 컴파일 오류를 읽기 쉬워지고, ownership과 borrowing을 학습할 때 변수의 생명주기를 추적하기 수월해집니다.
- 튜플과 배열을 이해하면 구조체, 슬라이스, 반복자 등 더 복잡한 데이터 구조를 학습할 때 쉽게 연결할 수 있습니다.
CodeSandbox로 이어서 실습하기
아래 샌드박스는 CodeSandbox의 Rust starter입니다. 이번 글의 핵심 코드를 src/main.rs에 옮기고, cargo check와 cargo run 결과를 나란히 보면서 컴파일 메시지와 실행 출력을 비교해 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요