[Rust 시리즈 3편] 변수, 불변성, 기본 자료형

English version

2편에서 환경을 준비했다면, 이제 변수와 자료형 규칙을 이해해야 본격적으로 코드를 작성할 수 있습니다. Rust는 기본적으로 모든 변수를 불변으로 취급하며, let 선언 시 mut를 명시해야만 값을 바꿀 수 있습니다. 3편에서는 불변성 감각을 잡고, 스칼라 타입과 튜플·배열 같은 복합 타입을 차근차근 익힙니다.

이번 글에서 새로 나오는 용어

  1. 불변성 (immutability): 변수가 생성된 뒤 값을 바꿀 수 없는 성질로, Rust에서는 기본 동작입니다.
  2. 스칼라 타입: 한 번에 하나의 값을 표현하는 타입(i32, f64, bool, char 등)을 가리킵니다.
  3. 튜플 (tuple): 서로 다른 타입의 값을 순서 있게 묶는 고정 길이 컬렉션입니다.
  4. 배열 (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는 불변이라 값을 바꾸려 하면 컴파일 오류가 발생합니다. versionmut 덕에 증가시킬 수 있습니다.

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을 학습할 때 변수의 생명주기를 추적하기 수월해집니다.
  • 튜플과 배열을 이해하면 구조체, 슬라이스, 반복자 등 더 복잡한 데이터 구조를 학습할 때 쉽게 연결할 수 있습니다.

실습

  1. cargo new data-playground --bin을 만든 뒤, main.rs에서 불변 변수에 값을 다시 대입해 보고 컴파일 오류 메시지를 캡처합니다.
  2. 정수 배열에서 최댓값을 찾는 간단한 루프를 작성해 보고, mut 없이도 합계를 구할 수 있는지 실험합니다(힌트: iter().sum::<i32>()는 아직 다루지 않으므로 직접 누적 변수를 써도 됩니다).
  3. 튜플을 반환하는 작은 함수를 작성해 (sum, average) 같은 계산 결과를 돌려보며 패턴 분해를 연습합니다.
  4. let shadowed = 5; let shadowed = shadowed + 1;처럼 같은 이름을 다시 선언(shadowing)해, 기존 변수를 수정한 것이 아니라 새 변수를 선언한 패턴임을 설명해 봅니다.

마무리

불변성, 스칼라 타입, 튜플, 배열은 Rust 문법의 기초 체온을 결정합니다. 이 감각이 잡혀 있어야 함수와 제어 흐름을 다룰 때 변수의 범위와 상태를 자연스럽게 추적할 수 있습니다. 4편에서는 fn으로 함수를 정의하고, if와 반복문으로 흐름을 제어하는 방법을 이어서 살펴봅니다.

💬 댓글

이 글에 대한 의견을 남겨주세요