[Rust 시리즈 9편] enum과 패턴 매칭으로 상태 표현하기

English version

구조체로 데이터를 묶었다면, 이제 그 데이터가 가질 수 있는 상태를 정의할 차례입니다. 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_derefOption<String>Option<&str>로 바꿔 주는 편의 메서드입니다. unwrap은 값이 없으면 패닉을 일으키니, 예제에서만 사용하고 실제 코드에서는 matchif 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 checkcargo run 결과를 나란히 보면서 컴파일 메시지와 실행 출력을 비교해 보세요.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. starter를 fork한 뒤 src/main.rs를 연다
  2. 본문 예제를 붙여 넣고 cargo check와 cargo run을 차례로 실행한다
  3. 타입, 값, 참조 흐름을 바꿔 컴파일 피드백과 출력 차이를 비교한다

Rust 실습은 브라우저 미리보기보다 터미널 피드백이 더 중요합니다. 여러 파일 구조나 추가 crate가 필요한 예제는 파일 배치를 조금 더 손봐야 할 수 있습니다.

손으로 따라하기

  • 학습 상태에 Blocked(String) variant를 추가하고, render에서 이유까지 출력해 보세요.
  • [[option|Option]] 대신 [[result|Result]]를 사용하면 어떻게 에러 메시지를 표현할 수 있는지 구상해 보세요. (11편에서 본격적으로 다룹니다.)
  • match 안에서 동시에 여러 필드를 구조 분해하는 연습을 해 보세요. 예를 들어, Progress::Doing(p)notes 길이를 함께 확인해 조건 메시지를 만들어 보세요.

마무리

enum과 패턴 매칭은 Rust에서 상태를 명시적으로 표현하는 핵심 도구입니다. Option처럼 자주 쓰는 enum을 이해하면 null 포인터 없이도 안전하게 값의 부재를 표현할 수 있고, match는 로직을 빠짐없이 나누도록 도와줍니다. 다음 글에서는 VecHashMap 같은 표준 컬렉션을 다루며, 지금까지 만든 구조체·enum을 실제 데이터 목록과 연결해 봅니다.

💬 댓글

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