Rust 기초 강좌 #2 변수와 타입: let과 mut, 섀도잉, 함수는 표현식으로 끝난다
1편에서 let으로 선언한 변수에 재대입하면 컴파일 에러가 나는 것을 봤습니다. 이번 편은 그 지점부터 시작합니다. 변수, 타입, 함수라는 기본기지만, Rust는 세 가지 모두에서 다른 언어와 다른 선택을 했고 그 선택이 이후 소유권 이해의 바탕이 됩니다.
let과 mut: 불변이 기본값인 이유 #
let count = 1; // 불변. 재대입 불가
let mut total = 0; // 가변. 재대입 가능
total += count;다른 언어 대부분은 “변수는 바뀌는 것이 기본, 상수만 특별 표시"입니다. Rust는 반대로 바뀌지 않는 것이 기본이고, 바뀌는 변수에만 mut를 붙입니다. 코드를 읽을 때 mut가 없는 변수는 선언 지점의 값이 끝까지 유지된다고 확신할 수 있고, 값이 바뀔 수 있는 지점만 추적하면 되므로 읽어야 할 범위가 줄어듭니다. 4편에서 다룰 빌림 규칙도 이 불변·가변 구분 위에 세워져 있어서, mut는 편의 표시가 아니라 언어 전체를 관통하는 축입니다.
const는 별도로 있습니다. 컴파일 타임에 값이 확정되는 진짜 상수로, 타입 표기가 필수이고 관례상 대문자로 씁니다.
const MAX_RETRIES: u32 = 3;섀도잉: 같은 이름을 다시 선언하기 #
mut 없이도 값을 바꾸는 것처럼 보이는 방법이 하나 있습니다. let으로 같은 이름을 다시 선언하는 섀도잉(shadowing)입니다.
let input = "42"; // &str 타입
let input: i32 = input.parse().unwrap(); // 같은 이름, 이제 i32 타입
재대입과 달리 새 변수를 만들어 이전 변수를 가리는 것이라서 타입까지 바꿀 수 있습니다. 문자열로 받은 입력을 파싱해 숫자로 쓰는 위 패턴이 대표적입니다. input_str, input_num처럼 이름을 늘리는 대신 같은 이름을 유지하는 것이 Rust의 관용구입니다. 변환 전의 값을 이후에 쓸 일이 없다는 것을 이름 차원에서 보장하는 효과도 있습니다.
스칼라 타입: 정수는 크기를 고른다 #
| 분류 | 타입 | 비고 |
|---|---|---|
| 정수 | i8〜i128, u8〜u128, isize, usize | 기본 추론은 i32 |
| 부동소수점 | f32, f64 | 기본 추론은 f64 |
| 불리언 | bool | true, false |
| 문자 | char | 4바이트 유니코드 스칼라 |
정수 타입이 많아 보이지만 실무 감각은 단순합니다. 특별한 이유가 없으면 i32, 컬렉션 인덱스와 길이는 usize(포인터 크기 부호 없는 정수), 바이트 데이터는 u8입니다. char가 1바이트가 아니라 4바이트 유니코드라는 점은 문자열을 다루는 7편에서 다시 만나게 됩니다.
오버플로 동작은 알아 둘 가치가 있습니다. 디버그 빌드에서는 정수 오버플로가 나면 즉시 패닉으로 죽고, 릴리스 빌드에서는 감싸서(wrap) 계속 돌아갑니다. 조용히 이상한 값이 되는 것을 개발 단계에서 잡겠다는 설계입니다. 의도적으로 감싸거나 포화시키려면 wrapping_add, saturating_add 같은 명시적 메서드를 씁니다.
타입 추론이 강해서 표기는 생략할 때가 많지만, 추론이 안 되는 지점(위의 parse 결과 등)에서는 명시가 필수입니다.
튜플과 배열: 고정 크기 묶음 #
let point: (f64, f64) = (3.0, 4.0);
let x = point.0; // 위치로 접근
let (px, py) = point; // 구조 분해
let days: [u32; 3] = [31, 28, 31]; // 타입과 길이가 함께 타입
let jan = days[0];튜플은 서로 다른 타입의 고정 묶음, 배열은 같은 타입의 고정 길이 묶음입니다. 둘 다 길이가 타입의 일부라서 컴파일 타임에 크기가 확정됩니다. 실무에서 훨씬 자주 쓰는 가변 길이 컬렉션 Vec은 소유권을 배운 뒤 7편에서 다룹니다. 배열 인덱스가 범위를 벗어나면 C처럼 옆 메모리를 읽는 것이 아니라 패닉으로 즉시 멈춥니다. 이것도 “조용한 오동작보다 시끄러운 실패"라는 Rust의 일관된 태도입니다.
함수: 마지막 표현식이 반환값 #
fn add(a: i32, b: i32) -> i32 {
a + b
}매개변수 타입과 반환 타입(->)은 추론 없이 항상 명시합니다. 함수 시그니처가 곧 문서라는 설계입니다. 눈에 띄는 것은 return이 없다는 점인데, 오타가 아닙니다. Rust에서 블록의 마지막 표현식에 세미콜론을 붙이지 않으면 그 값이 블록의 값이 됩니다. a + b 뒤에 세미콜론을 붙이면 값을 버리는 문장이 되어 “()를 반환했는데 i32가 필요하다"는 컴파일 에러가 납니다. help 줄이 “세미콜론을 지우라"고 알려 주므로 당황할 필요는 없습니다.
if도 표현식이라서 값을 만듭니다. 삼항 연산자가 따로 없는 이유입니다.
let price = if is_member { 800 } else { 1000 };return은 함수 중간에서 일찍 빠져나올 때만 씁니다. 이 “거의 모든 것이 표현식"이라는 성질은 5편의 match에서 진가를 발휘합니다.
정리 #
- 변수는 불변이 기본이고 바뀌는 것에만
mut를 붙입니다. 읽는 사람이 추적할 범위를 줄이는 설계이고, 이후 빌림 규칙의 바탕이 됩니다. - 섀도잉은 같은 이름을
let으로 다시 선언해 타입까지 바꾸는 관용구입니다. 파싱·변환 코드에서 이름 오염을 막아 줍니다. - 정수는 기본
i32, 인덱스는usize, 바이트는u8이 실무 감각입니다. 오버플로는 디버그에서 패닉, 릴리스에서 랩핑입니다. - 블록의 마지막 표현식이 곧 값입니다. 세미콜론 유무가 문장과 표현식을 가르고,
if도 값을 만듭니다. - 다음 편은 이 강좌의 고비이자 심장인 소유권입니다. GC 없이 메모리가 정리되는 원리를 정면으로 다루겠습니다.