구조체로 데이터를 묶었다면, 이제 그 데이터가 가질 수 있는 상태를 정의할 차례입니다. Rust의 enum은 "여러 형태 중 정확히 하나"를 표현하는 타입이며, [[match|match]]는 그 형태에 따라 로직을 분기하는 표현식입니다. [[option|Option]]은 Rust가 제공하는 대표적인 enum으로, 값이 있거나 없음을 표현할 때 사용합니다. 이번 글에서는 학습 로그 프로그램에 enum을 도입해 상태 표현을 강화합니다.
enum 기본 문법
enum StudyState {
Planned,
InProgress { percent: u8 },
Done,
}
fn main() {
let state = StudyState::InProgress { percent: 40 };
match state {
StudyState::Planned => println!("시작 전"),
StudyState::InProgress { percent } => println!("{}% 진행 중", percent),
StudyState::Done => println!("완료"),
}
}
각 variant는 필요한 데이터를 자체적으로 포함할 수 있습니다. [[match|match]]는 모든 경우를 다뤄야 하며, percent처럼 내부 필드를 구조 분해할 수 있습니다.
구조체와 enum 연결하기
앞서 만든 StudyLog 구조체를 확장해 상태를 enum으로 표현해 보겠습니다.
enum Progress {
Todo,
Doing(u8),
Done,
}
struct StudyLog {
topic: String,
state: Progress,
notes: Vec<String>,
}
impl StudyLog {
fn new(topic: &str) -> Self {
Self {
topic: topic.to_string(),
state: Progress::Todo,
notes: Vec::new(),
}
}
fn update(&mut self, progress: Progress) {
self.state = progress;
}
fn render(&self) -> String {
let label = match &self.state {
Progress::Todo => "대기",
Progress::Doing(p) => {
if *p >= 100 { "완료 직전" } else { "진행" }
}
Progress::Done => "완료",
};
format!("{} ({})", self.topic, label)
}
}
fn main() {
let mut log = StudyLog::new("enum");
log.update(Progress::Doing(60));
println!("{}", log.render());
log.update(Progress::Done);
println!("{}", log.render());
}
match 표현식은 값을 반환할 수 있기 때문에, render 메서드 안처럼 문자열을 바로 만들어 낼 수 있습니다. enum을 사용하면 state가 가질 수 있는 값을 명시적으로 제한할 수 있어 조건문보다 상태 추론이 쉬워집니다.
Option으로 값의 유무 표현하기
[[option|Option<T>]]는 Some(T) 또는 None 두 가지 상태만을 갖는 enum입니다. 즉, Option도 우리가 직접 만드는 enum과 같은 구조를 가진, 표준 라이브러리의 대표 enum이라고 보면 됩니다. Rust는 null 대신 Option을 사용해 값의 존재 여부를 타입 시스템에서 강제합니다.
struct StudyLog {
topic: String,
last_note: Option<String>,
}
impl StudyLog {
fn add_note(&mut self, note: &str) {
self.last_note = Some(note.to_string());
}
fn last_note(&self) -> Option<&str> {
self.last_note.as_deref()
}
}
fn main() {
let mut log = StudyLog {
topic: String::from("Option"),
last_note: None,
};
if let Some(text) = log.last_note() {
println!("{}", text);
} else {
println!("기록 없음");
}
log.add_note("Some/None 패턴");
println!("최근 기록: {}", log.last_note().unwrap());
}
as_deref는 Option<String>을 Option<&str>로 바꿔 주는 편의 메서드입니다. unwrap은 값이 없으면 패닉을 일으키니, 예제에서만 사용하고 실제 코드에서는 match나 if let으로 안전하게 다루세요.
간단히 비교하면 아래와 같습니다.
- [[match|
match]]: 모든 경우를 빠짐없이 처리할 때 if let: 특정 경우 하나만 간단히 처리할 때unwrap: 값이 반드시 있다고 확신하는 아주 짧은 예제나 테스트에서만
패턴 매칭 활용 팁
match는 반드시 모든 경우를 나열해야 하므로, enum이 확장될 때 컴파일러가 놓친 분기를 알려줍니다._패턴을 사용하면 나머지 경우를 한꺼번에 처리할 수 있지만, 너무 자주 사용하면 확장 시 놓치기 쉬우니 조심하세요.if let은 특정 variant만 다루고 나머지는 무시할 때 간결합니다. 예:if let Progress::Doing(percent) = log.state { ... }
CodeSandbox로 이어서 실습하기
아래 샌드박스는 CodeSandbox의 Rust starter입니다. 이번 글의 핵심 코드를 src/main.rs에 옮기고, cargo check와 cargo run 결과를 나란히 보면서 컴파일 메시지와 실행 출력을 비교해 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요