3. 변수, 타입, 함수, 제어 흐름

이 장에서 배우는 것
Rust를 처음 배울 때 가장 먼저 손에 익혀야 하는 것은 거창한 개념이 아니라, 코드를 이루는 가장 기본적인 문법입니다. 값을 저장하는 변수, 값의 모양을 정하는 타입, 코드를 묶는 함수, 그리고 프로그램의 진행 방향을 바꾸는 제어 흐름이 바로 그 바닥입니다. Rust는 변수 기본값을 불변으로 두고, 타입을 컴파일 시점에 엄격하게 확인하며, 표현식 중심으로 코드를 쓰게 만드는 언어입니다. 이 성격을 이해하면 다음 장의 소유권도 훨씬 덜 무섭게 느껴집니다.12
이 장의 목표는 간단합니다. Rust의 기본 문법을 읽고, 직접 작은 프로그램으로 조립해 보는 것입니다. 마치 레고 상자를 열고 기본 블록부터 맞춰 보는 느낌으로 가면 됩니다.
변수: Rust는 왜 기본값을 "안 바뀜"으로 둘까
Rust에서 변수는 let으로 선언합니다.
fn main() {
let x = 10;
println!("x = {}", x);
}여기서 중요한 점은 x가 기본적으로 불변이라는 것입니다. 한 번 값을 넣으면 다시 바꿀 수 없습니다.1
fn main() {
let x = 10;
// x = 20; // 컴파일 에러
}처음에는 조금 답답해 보일 수 있습니다. 하지만 실제로는 큰 장점이 있습니다. 값이 바뀌는 곳을 줄이면 버그도 줄고, 코드를 읽는 사람도 "이 값은 중간에 안 바뀌는구나" 하고 안심할 수 있습니다.
정말 값을 바꿔야 한다면 mut를 붙입니다.
fn main() {
let mut score = 0;
score = 10;
score = 20;
println!("score = {}", score);
}이제 score는 가변 변수입니다. Rust는 이렇게 "바뀌는 값"을 눈에 띄게 표시하게 만듭니다. 초보자에게는 살짝 엄격해 보이지만, 나중에는 오히려 친절하게 느껴집니다. 코드를 읽을 때 숨은 반전이 적기 때문입니다.
섀도잉: 같은 이름이지만 새 변수
Rust에는 mut 말고도 자주 쓰는 도구가 하나 더 있습니다. 바로 섀도잉(shadowing) 입니다. 같은 이름으로 새 변수를 다시 선언하는 방식입니다.1
fn main() {
let user_input = "42";
let user_input = user_input.trim();
let user_input: i32 = user_input.parse().unwrap();
println!("{}", user_input);
}겉으로는 같은 이름을 계속 쓰는 것 같지만, 실제로는 매번 새 변수를 만드는 것입니다. 이 방식의 좋은 점은 값이 변형되는 흐름을 자연스럽게 보여 줄 수 있다는 데 있습니다. 문자열을 정리하고, 숫자로 바꾸고, 그 결과를 같은 이름으로 이어서 다룰 수 있죠.
mut는 같은 변수를 수정하는 것이고, 섀도잉은 새 변수를 같은 이름으로 다시 만드는 것입니다. 이 차이는 나중에 소유권과 타입을 배울 때 꽤 중요해집니다.
상수는 변수와 다르다
항상 같은 값을 써야 할 때는 const를 사용합니다.1
const MAX_USERS: u32 = 100;
fn main() {
println!("최대 사용자 수: {}", MAX_USERS);
}상수는 몇 가지 특징이 있습니다.
반드시 타입을 써야 합니다.
값을 바꿀 수 없습니다.
프로그램 전반에서 고정된 의미를 갖는 값에 어울립니다.
이름은 보통 대문자와 밑줄로 씁니다.
예를 들어 세율, 최대 점수, 게임의 초기 생명 수처럼 "의미상 절대 안 바뀌는 값"은 상수로 두면 읽기 좋습니다.
타입: 값의 정체를 분명히 하자
Rust의 모든 값은 타입을 가집니다. 그리고 Rust는 정적 타입 언어라서, 컴파일할 때 각 값의 타입을 알아야 합니다.2 다행히 매번 다 손으로 적을 필요는 없습니다. Rust 컴파일러는 꽤 똑똑해서 많은 경우 타입을 추론해 줍니다.
fn main() {
let age = 20; // i32로 추론
let height = 170.5; // f64로 추론
let active = true; // bool로 추론
}하지만 항상 추론이 가능한 것은 아닙니다. 특히 문자열을 숫자로 바꿀 때는 힌트가 필요합니다.2
fn main() {
let n: i32 = "42".parse().unwrap();
println!("{}", n);
}여기서 parse()는 "어떤 숫자로 바꿔야 하지?"라고 묻고 있습니다. i32인지 u64인지 f64인지 스스로 확신할 수 없기 때문입니다. 그래서 우리가 타입을 알려 줘야 합니다.
자주 쓰는 기본 타입
초보자 단계에서 가장 많이 보는 타입은 다음 네 가지입니다.2
fn main() {
let a: i32 = -10; // 정수
let b: u32 = 10; // 부호 없는 정수
let c: f64 = 3.14; // 실수
let d: bool = true; // 불리언
let e: char = 'R'; // 문자
println!("{} {} {} {} {}", a, b, c, d, e);
}정수는 i32, u32, i64, usize처럼 종류가 많습니다. 처음에는 이렇게 생각하면 충분합니다.
음수가 필요하면
i로 시작하는 정수 타입을 쓴다.음수가 필요 없으면
u로 시작하는 정수 타입을 쓴다.특별한 이유가 없으면 정수는
i32, 실수는f64를 기본값처럼 생각해도 된다.인덱스나 길이에는
usize가 자주 등장한다.
숫자 리터럴은 읽기 좋게 쓸 수도 있습니다.
fn main() {
let million = 1_000_000;
let hex = 0xff;
let bin = 0b1010;
println!("{} {} {}", million, hex, bin);
}참고로 정수 타입은 표현 가능한 범위를 넘기면 문제가 생깁니다. 특히 디버그 빌드에서는 오버플로우를 잡아 주기도 하니, 숫자 타입은 "크기도 의미의 일부"라고 생각하는 습관이 좋습니다.2
튜플과 배열
Rust에는 값을 묶는 기본적인 복합 타입도 있습니다.2
먼저 튜플(tuple) 은 서로 다른 타입의 값을 한 덩어리로 묶습니다.
fn main() {
let person: (&str, i32, bool) = ("Alice", 30, true);
let (name, age, employed) = person;
println!("{} / {} / {}", name, age, employed);
}튜플은 "이름, 나이, 취업 여부"처럼 서로 성격이 다른 값들을 임시로 한 번에 다루고 싶을 때 편리합니다.
다음은 배열(array) 입니다. 같은 타입의 값을 고정 길이로 저장합니다.2
fn main() {
let scores: [i32; 5] = [90, 80, 70, 60, 50];
println!("첫 번째 점수: {}", scores[0]);
}배열은 길이가 고정되어 있습니다. "학생 5명의 점수", "7일치 온도"처럼 개수가 정해져 있을 때 어울립니다. 길이가 바뀌는 자료구조는 나중에 벡터(Vec)를 배우면서 만나게 됩니다.
함수: 코드를 잘게 나누는 가장 쉬운 방법
함수는 반복되는 코드를 묶고, 이름을 붙이고, 재사용할 수 있게 해 줍니다. Rust에서 함수는 fn으로 선언합니다.3
fn greet(name: &str) {
println!("안녕하세요, {}!", name);
}
fn main() {
greet("Rust");
}함수의 매개변수는 타입을 반드시 적어야 합니다.3 이 점은 귀찮아 보여도 장점이 큽니다. 함수를 보는 순간 "무슨 값을 받는지"가 분명해지기 때문입니다.
반환값이 있는 함수
반환값이 있으면 -> 타입을 씁니다.
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let result = add(3, 5);
println!("{}", result);
}여기서 눈여겨볼 부분은 마지막 줄의 a + b 입니다. 세미콜론이 없습니다. Rust에서는 마지막 표현식이 함수의 반환값이 될 수 있습니다.3
세미콜론 하나로 의미가 바뀐다
Rust 초보자가 정말 자주 만나는 포인트입니다.
fn plus_one(x: i32) -> i32 {
x + 1
}이 함수는 잘 동작합니다. 하지만 아래처럼 쓰면 에러가 납니다.
fn plus_one(x: i32) -> i32 {
x + 1;
}마지막에 세미콜론을 붙이면 그 줄은 값을 돌려주는 표현식이 아니라, 그냥 일을 하고 끝나는 문장이 됩니다. 그러면 함수는 사실상 ()를 반환하려고 하게 되고, 선언한 i32와 맞지 않아 오류가 납니다.3
처음엔 까다로워 보이지만, 익숙해지면 Rust 코드가 왜 이렇게 간결한지 이해하게 됩니다. "마지막 줄이 곧 결과"라는 규칙이 꽤 예쁩니다.
제어 흐름: 프로그램이 스스로 판단하게 만들기
이제 프로그램에 "생각하는 척"을 시켜 봅시다. 조건에 따라 다른 코드를 실행하거나, 같은 일을 여러 번 반복하면 프로그램이 훨씬 살아 움직이는 것처럼 보입니다.
if: 조건에 따라 갈림길 만들기
Rust의 if는 단순한 문장이 아니라 표현식이기도 합니다.4 즉, 값을 만들어 낼 수 있습니다.
fn main() {
let temperature = 23;
if temperature >= 25 {
println!("조금 덥네요");
} else {
println!("괜찮은 날씨네요");
}
}중요한 규칙이 하나 있습니다. if의 조건은 반드시 bool이어야 합니다.4
fn main() {
let number = 3;
// if number { } // 에러: 정수는 bool이 아님
if number != 0 {
println!("0이 아닙니다");
}
}다른 언어에서는 0이 아니면 참으로 취급하기도 하지만, Rust는 그런 암묵적인 변환을 허용하지 않습니다. 덕분에 코드를 읽을 때 덜 헷갈립니다.
if는 값을 반환할 수 있다
이게 Rust다운 포인트입니다.
fn main() {
let score = 85;
let status = if score >= 60 { "합격" } else { "불합격" };
println!("{}", status);
}다만 양쪽 결과 타입은 같아야 합니다.4 한쪽은 문자열, 다른 한쪽은 숫자를 반환하면 컴파일러가 바로 막습니다.
반복문: 같은 일을 여러 번 하기
Rust에는 세 가지 대표 반복문이 있습니다.4
loop: 무한 반복이 기본이며,break로 빠져나옵니다.while: 조건이 참인 동안 반복합니다.for: 범위나 컬렉션을 순회할 때 가장 자주 씁니다.
loop
fn main() {
let mut count = 0;
loop {
count += 1;
if count == 3 {
break;
}
println!("count = {}", count);
}
}loop는 단순하지만 강력합니다. 심지어 break 값; 형태로 값을 돌려줄 수도 있습니다.4
fn main() {
let mut count = 0;
let result = loop {
count += 1;
if count == 5 {
break count * 2;
}
};
println!("{}", result);
}while
fn main() {
let mut n = 3;
while n > 0 {
println!("{}!", n);
n -= 1;
}
println!("발사!");
}조건이 있는 반복이라서 이해하기 쉽습니다. 다만 단순 순회에는 보통 for가 더 읽기 좋습니다.
for
fn main() {
for n in 1..=5 {
println!("{}", n);
}
}1..=5는 1부터 5까지 포함하는 범위입니다. 1..5라고 쓰면 5는 포함하지 않습니다.4
배열도 순회할 수 있습니다.
fn main() {
let scores = [90, 80, 70, 60, 50];
for score in scores {
println!("점수: {}", score);
}
}초보자 입장에서는 for를 "가장 자연스럽고 안전한 반복문"으로 기억해 두면 좋습니다.
match: Rust가 자랑하는 갈림길
match는 Rust에서 정말 자주 쓰는 도구입니다. 값의 모양에 따라 경우를 나누고, 각 경우에 다른 동작을 할 수 있습니다. 그리고 무엇보다 중요한 특징은 모든 경우를 처리해야 한다는 점입니다. 빠진 경우가 있으면 컴파일러가 알려 줍니다.5
fn main() {
let number = 3;
match number {
1 => println!("하나"),
2 => println!("둘"),
3 => println!("셋"),
_ => println!("그 외의 숫자"),
}
}여기서 _는 "나머지 전부"를 뜻하는 와일드카드 패턴입니다.
여러 값과 범위도 매칭할 수 있다
fn main() {
let score = 87;
match score {
90..=100 => println!("A"),
80..=89 => println!("B"),
70..=79 => println!("C"),
60..=69 => println!("D"),
_ => println!("F"),
}
}또는 여러 값을 한 번에 묶을 수도 있습니다.
fn main() {
let day = 6;
match day {
1 | 2 | 3 | 4 | 5 => println!("평일"),
6 | 7 => println!("주말"),
_ => println!("잘못된 입력"),
}
}if가 "조건 검사기"라면, match는 "경우의 수 정리왕"에 가깝습니다. 갈래가 많아질수록 else if 연쇄보다 match가 읽기 쉬워집니다.
초보자가 자주 만나는 실수
처음 Rust를 배우면 거의 정해진 자리에서 몇 번씩 넘어집니다. 미리 알고 있으면 덜 아픕니다.
불변 변수에 다시 대입하기:
let만 쓰면 값이 안 바뀝니다. 바꿀 일이 있으면let mut를 써야 합니다.if조건에 정수 넣기: Rust는 정수나 문자열을 자동으로 참/거짓으로 바꾸지 않습니다. 반드시bool이어야 합니다.반환식 뒤에 세미콜론 붙이기: 함수의 마지막 값은 세미콜론 없이 써야 반환됩니다.
parse()에 타입 힌트 안 주기: 어떤 숫자로 바꿀지 모호하면let n: i32 = ...처럼 타입을 알려 줘야 합니다.234
Rust 컴파일러는 이런 실수를 혼내기만 하는 존재가 아니라, 꽤 집요한 개인 과외 선생님에 가깝습니다. 처음엔 잔소리처럼 들리지만, 나중에는 "아, 그때 막아 줘서 고맙다"는 생각이 듭니다.
함께 만들어 보는 간단한 프로그램
이제 배운 내용을 한 번에 엮어 보겠습니다. 학생 점수를 보고 등급을 매기고, 합격 여부를 표시하고, 전체 합격자 수를 계산하는 프로그램입니다.
const CLASS_NAME: &str = "Rust 입문반";
fn grade(score: u32) -> &'static str {
match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
_ => "F",
}
}
fn pass_message(score: u32) -> &'static str {
if score >= 60 { "합격" } else { "재도전" }
}
fn main() {
let scores = [95, 82, 67, 48, 100];
let total_students = scores.len();
let mut pass_count = 0;
let mut total = 0;
println!("[{} 성적표]", CLASS_NAME);
for score in scores {
let letter = grade(score);
let message = pass_message(score);
if score >= 60 {
pass_count += 1;
}
total += score;
println!("점수: {:>3}, 등급: {}, 결과: {}", score, letter, message);
}
let average = total as f64 / total_students as f64;
let failed_count = total_students - pass_count;
println!("---------------------------");
println!("평균 점수: {:.1}", average);
println!("합격 인원: {}명", pass_count);
println!("불합격 인원: {}명", failed_count);
}이 코드에는 지금까지 배운 핵심이 거의 다 들어 있습니다.
먼저 CLASS_NAME은 변하지 않는 값이므로 상수로 선언했습니다. scores는 고정 길이 데이터라 배열로 만들었습니다. pass_count와 total은 반복문 안에서 값이 바뀌어야 하므로 mut를 붙였습니다. grade() 함수는 match를 사용해 점수 범위를 등급으로 바꾸고, pass_message() 함수는 if 표현식으로 합격 여부 문구를 반환합니다. 마지막으로 for 반복문이 배열을 순회하며 결과를 출력합니다.
이 프로그램을 직접 바꿔 보세요. 예를 들어 다음처럼 확장해 볼 수 있습니다.
점수 배열을 바꿔 보기
A+,A,B+처럼 등급을 더 세분화하기평균이 80점 이상이면 "우수 반"이라고 출력하기
match에 잘못된 점수 범위를 처리하는 분기 추가하기
처음에는 문법 하나하나가 따로 노는 것처럼 보이지만, 이렇게 작은 프로그램으로 묶으면 Rust 문장이 왜 그렇게 생겼는지 감이 오기 시작합니다.
마무리
이 장에서 배운 것은 화려하지 않지만, 아주 중요합니다. 변수는 값을 담고, 타입은 그 값의 성격을 분명히 하며, 함수는 코드를 작게 나누고, 제어 흐름은 프로그램이 상황에 맞게 움직이게 만듭니다. 이 네 가지는 앞으로 만날 모든 Rust 개념의 공통 기반입니다.
특히 Rust는 "기본은 불변", "타입은 명확하게", "표현식으로 값을 만든다", "모든 경우를 빠뜨리지 않는다"라는 철학을 기본 문법부터 드러냅니다. 그래서 기본 문법을 익히는 일은 단순히 문법 암기가 아니라, Rust다운 사고방식에 적응하는 과정이기도 합니다.
다음 장에서는 드디어 Rust를 Rust답게 만드는 핵심 규칙, 소유권을 배웁니다. 그리고 그때 이런 생각이 들 가능성이 큽니다.
"아, 그래서 앞에서 변수와 값에 그렇게 엄격했구나."
참고
1Variables, Types, and Functions | sunface/rust-course | DeepWiki
2Data Types - The Rust Programming Language
3Functions - The Rust Programming Language
4Control Flow - The Rust Programming Language
5Control Flow and Pattern Matching | sunface/rust-course | DeepWiki
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.