Rust의 상수와 복합 타입 (Compound Types)

Scalar Types에 대해서는 아래 글에서 살펴봤는데, 이번에는 상수와 복합 타입(Compound Types) 튜플과 배열을 살펴보겠습니다. 상수는 변하지 않는 값이며, 튜플은 다양한 타입의 값을 하나의 그룹으로 묶은 것이고, 배열은 동일한 타입의 값을 고정된 크기로 저장합니다.


🔷 상수 (Constants)

상수는 프로그램 전체에서 변하지 않는 값을 나타냅니다.

const MAX_POINTS: u32 = 100_000;

fn main() {
    println!("최대 점수: {}", MAX_POINTS);
}
  • 변수는 let 키워드를 사용하는데, 상수는 const 키워드를 사용하며 함수 외부/내부 모두 선언 가능
  • 타입을 반드시 명시해야 하며(추론 적용되지 않음), 대문자+언더스코어로 표기하는 것이 관례입니다.
  • 위 코드 중 100_000에서 _는 천 단위 구분 기호인 ,를 대체하는 기호입니다.
  • const는 컴파일 타임에 값이 결정됨

참고: let로 선언한 변수는 mut로 변경 가능하지만, const는 절대 변경되지 않습니다.

fn main() {
    const MAX_POINTS: u32 = 100_000;
    MAX_POINTS += 10; // 상수는 변경할 수 없음, 이 줄은 오류를 발생시킴
    println!("최대 점수: {}", MAX_POINTS);
}

MAX_POINTS를 변경하려고 하면 값을 할당할 수 없다는 에러가 발생합니다.


🔷 튜플 (Tuples)

튜플은 다양한 타입의 값을 하나의 그룹으로 묶습니다.

fn main() {
    let person: (&str, u32) = ("Alice", 30);
    let (name, age) = person;

    println!("이름: {}, 나이: {}", name, age);
    println!("튜플 직접 접근: {}", person.0);
}
  • 괄호 안에 콤마로 구분되는 값들의 목록을 작성하여 튜플을 만듭니다.
  • 고정된 길이와 순서를 가지며, 서로 다른 타입 허용
    위 예에서는 &str, 다시 말해 스트링 슬라이스와 u32 정수 타입이 섞여 있습니다.
    타입을 입력하지 않으면 추론되는데, 정수는 i32가 기본 타입이므로 i32로 추론됩니다.
  • . 문법으로 인덱스로 튜플의 요소에 접근 가능
    위 예에서 person.0은 첫번째 값인 이름을 가리키고, .1을 하면 나이를 가리키게 됩니다.

튜플의 구조해체(destructuring)

튜플의 속성인 그룹을 해체하여 각각의 값을 개별 변수에 할당하는 것을 말합니다.

위 예에서 let (name, age) = person; 란 구문을 사용했는데,
person이란 튜플의 첫번째 요소는 name에, 두번째 요소는 age 변수에 할당하는 것입니다.
다시 말해 튜플은 구조해체 또는 .인덱스를 이용해 요소에 접근할 수 있습니다.


🔷 배열 (Arrays)

배열은 동일한 타입의 값고정된 크기로 저장합니다.

fn main() {
    let scores: [i32; 3] = [90, 85, 78];

    println!("첫 번째 점수: {}", scores[0]);

    for score in scores.iter() {
        println!("점수: {}", score);
    }
}
  • 대괄호 안에 값들을 콤마로 구분하여 나열해서 배열을 만듭니다.
  • [i32; 3]와 같이 타입 뒤에 ;(:이 아님)을 붙이고 숫자를 쓰면, i32 타입 3개의 배열 의미
  • let scores = [30; 10]; 이라고 입력하면 scores 배열에 정수 30을 10개 입력한 것이 됩니다.
  • scores[0]처럼 대괄호안에 인덱스를 입력하여 배열의 요소에 접근 가능
  • for와 .iter()를 이용해서 반복 가능

배열(Array)과 벡터(vector)

배열이 유용할 때는 항상 고정된 크기의 요소를 갖는데 비해서 벡터 타입은 유사 집합체로 표준 라이브러리에서 제공되며 크기를 확장 또는 축소가 가능합니다. 배열이나 벡터 중에 뭘 선택해야 할지 확실하지 않은 상황이라면 벡터를 사용하라고 합니다.

유효하지 않은 배열 요소에 대한 접근

아래에서 a배열의 가장 큰 인덱스가 4인데, 10으로 지정하고 cargo run을 하면

fn main() {
    let a = [1, 2, 3, 4, 5];

    let element = a[10];

    println!("The value of element is: {}", element);
}

아래와 같이 길이가 5인데, 인덱스가 10이라는 경계를 벗어난 인덱스 에러가 발행합니다.


🧠 요약

항목설명
const변경 불가능한 상수, 타입 명시 필수
튜플다양한 타입을 그룹화, 순서 중요
배열동일한 타입, 고정된 크기, 인덱스로 접근

변수, 불변성, 데이터 타입

Rust는 안전성을 위해 변수는 기본적으로 불변(immutable) 입니다. 따라서, 필요한 경우 변수의 값을 변경하려면 명시적으로 가변(mut)으로 선언해야 합니다. 변수 선언 시 let을 사용하고, 스칼라 타입은 숫자, 불리언, 문자가 있으며, 타입을 지정해야 하는 정적 타입 언어이나 타입 추론이 가능합니다.


🔹 준비

Code의 터미널 창에서 D:\rust-practice 폴더로 이동한 다음
cargo new day2라고 입력해서 day2 패키지를 만들고
cd day2라고 입력해서 day2 폴더로 이동합니다.

main.rs는 day2 아래 src 폴더에 있습니다.

🔹 변수 선언

fn main() {
let x = 5;
println!("x의 값은: {}", x);
}
  • let 키워드는 변수를 선언합니다.
  • x는 기본적으로 불변 변수입니다. 변수인데 불변이라고 하니 Rust는 참 독특한 개념의 언어입니다.
  • {}는 print, 다시 말해 출력 시 변수가 위치할 자리(placeholder)입니다.

실행

위 코드를 복사해서 에디터의 main.rs의 내용을 덮어쓰고 저장한 다음
cargo run을 실행하면
출력되는 값은 “x의 값은: 5″입니다.

출력 구문 변경

중괄호안에 변수를 넣어
println!(“x의 값은: {x}”);
과 같이 구문을 바꿀 수 있습니다.

main.rs 위의 run 실행

Code를 닫았다가 열면 rust-analyzer가 다시 실행되면서 아래와 같이 main.rs위에 Run|Debug가 생기므로

main.rs 위에 run 명령어가 보이고, 이를 눌러 rust 패키지를 실행할 수 있다.
흰색 동그라미는 파일이 저장되지 않았다는 의미

run을 눌러 실행할 수 있습니다.

cargo run을 할 때는 먼저 파일을 저장해야 하는데 위 run을 누르면 자동으로 파일을 저장하고, cargo run을 실행하므로 너무 편리합니다.

🔁 값을 바꾸려면?

fn main() {
let x = 5;
x = 6; // 에러! 기본 변수는 값 변경 불가
}

오류 메시지: cannot assign twice to immutable variable x
(불편 변수인 x에 값을 두 번 할당할 수 없다)

//는 주석 표시입니다. 다시 말해 // 다음의 문장은 프로그램 실행 시 제외됩니다.

기존 구문을 주석 처리하고 위 구문을 추가할 때
해당하는 2줄을 마우스로 끌어서 선택하고 Ctrl + /키를 누르면 맨 앞에 // 표시가 추가됩니다.

fn main() {
    // let x = 5;
    // println!("x의 값은: {x}");

    let x = 5;
    x = 6; // 에러! 기본 변수는 값 변경 불가    
}

🔧 가변 변수 (mutable)

fn main() {
let mut x = 5;
println!("처음 x: {x}");
x = 6;
println!("바뀐 x: {x}");
}
  • mut 키워드를 사용하면 값 변경이 가능합니다.

실행

실행하면 화면에 아래와 같이 출력됩니다.
처음 x: 5
바뀐 x: 6


🧮 스칼라 타입(Scalar Types)

스칼라(scalar)는 하나의 값으로 표현되는 타입입니다. Rust는 정수(integer), 부동소수형 숫자(floating-point numbers), boolean(논리형), 그리고 문자(char)라는 네 가지 타입을 보유하고 있습니다. Rust는 정적 타입 언어이며, 대부분 타입은 명시하지 않아도 추론됩니다.

정적 타입 언어는 컴파일 시 변수 타입이 결정되는 언어이고, 동적 타입 언어는 런타임 시 변수 타입이 결정되는 언어, 다시 말해, 코드를 실행할 때 알아서 변수 타입을 판단해주는 언어입니다. 그러나 Rust는 정적 타입 언어임에도 변수 타입을 프로그램에서 지정하지 않더라도 변수 타입을 추론한다는 것이 다른 점입니다.

숫자형

정수형은 소수점이 없는 숫자이고, 부동소수형은 소수점이 있는 숫자입니다. 3을 부동소수형으로 표현할 경우 소수점이하가 0이더라도 3.0이라고 써야지 3으로 쓰면 안되며, 최소한 3.이라고 소수점을 찍어야 합니다.

위 코드에서 let a = 10;이라고 변수에 정수를 대입하면 : i32를 입력하지 않아도 rust analyzer가 : i32를 알아서 추가해서 표시합니다. 3.14도 마찬가지로 : f64라는 타입을 자동으로 표시합니다. 이것이 추론입니다.

: i32 또는 : f64가 없어도 프로그램 실행에 전혀 문제가 없으며, 직접 입력하려면 : i32라고 입력해야 합니다. 아래와 같이 Code에서 타입을 입력한 것은 녹색으로 보이고, 추론한 것은 회색으로 보입니다.

숫자형의 종류

불리언 & 문자

let t: bool = true;
let c: char = 'R'; // 유니코드 문자

마찬가지로 : bool을 입려하지 않아도 값이 true이므로 boolean으로 추론되고, 문자 하나를 입력했으므로 : char로 자동으로 표시됩니다. 이때 문자는 반드시 작은따옴표 안에 입력해야 하며, “R”로 바꾸면 에러가 발생합니다.

파이썬은 자유롭게 작은따옴표 또는 큰따옴표를 사용할 수 있는 것과 다릅니다.


※ 문자 한 개(char)가 아니라 “I am a boy”과 같은 문자열은 String 타입으로 구분되므로 나중에 별도로 다루도록 하겠습니다.


📌 변수 섀도잉(Shadowing)

Rust에서는 같은 이름의 변수로 새로운 값을 선언할 수 있습니다.

fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("x의 값: {x}"); // 12
}
  • let을 반복 사용하면 기존 변수를 덮어쓰는(shadowing) 것처럼 새 변수 선언이 가능합니다.
  • 타입을 변경하는 것도 허용됩니다. 아래는 x가 i32 형식이었다가 char 형식으로 변경된 것을 보여줍니다.

‘R’; 다음의 주석 “// 변수 재정의 가능”도 입력한 것이 아니고 rust analyzer가 알아서 입력한 것입니다. 참 똑똑합니다.

위 코드의 출력 값은
x의 값: 12 (x는 5 -> 6 -> 12가 됨)


🧠 정리

  • 변수는 기본 불변 → 변경하려면 mut 사용
  • 정적 타입 → 타입을 명시하는 것이 원칙이지만 타입 추론 가능
  • Shadowing으로 가변처럼 활용 가능하나, 메모리 안정성을 유의해서 사용하지 않는 것이 바람직함