동작하는 코드
다른 언어에서는 값이 없을 때 null을 사용해요. 그리고 NullPointerException이라는 악명 높은 버그에 시달리죠. Rust는 null이 없어요. 대신 Option<T> 으로 "값이 있을 수도, 없을 수도 있다"를 표현해요.
fn main() {
let fruits = vec!["사과", "바나나", "포도"];
let first = fruits.get(0); // Some("사과")
let fourth = fruits.get(10); // None
println!("첫 번째: {:?}", first);
println!("네 번째: {:?}", fourth);
}
vec.get()은 Option<&T>를 반환해요. 인덱스가 범위 안이면 Some(값), 밖이면 None이에요. fruits[10]처럼 직접 접근하면 panic이 나지만, .get(10)은 안전하게 None을 돌려줘요.
직접 해보기
fruits.get(2)의 결과를 출력해보세요.fruits[10]으로 바꿔서 실행해보세요. 어떤 에러가 나오나요?fruits.get(10)과 비교해보세요 —None이 panic보다 나은 이유가 느껴지나요?
"왜?" — 쇼핑몰 재고 조회
Option을 쇼핑몰 재고 조회로 생각하면 쉬워요.
Some(상품)— 재고 있음! 상품이 들어있어요None— 품절! 상품이 없어요
enum Option<T> {
Some(T), // 값이 있어요
None, // 값이 없어요
}
Option<T>는 사실 모듈 13에서 배운 열거형이에요! Rust 표준 라이브러리에 이미 정의되어 있고, Some과 None은 별도 import 없이 바로 쓸 수 있어요.
match로 처리하기
가장 기본적인 방법이에요. 모든 경우를 빠짐없이 처리해야 하니까 안전해요.
fn find_discount(item: &str) -> Option<u32> {
match item {
"사과" => Some(10),
"바나나" => Some(20),
_ => None,
}
}
fn main() {
let item = "사과";
match find_discount(item) {
Some(percent) => println!("{item}: {percent}% 할인!"),
None => println!("{item}: 할인 없음"),
}
}
if let — 하나만 관심 있을 때
Some인 경우만 처리하고 싶다면 if let이 간결해요.
fn main() {
let name: Option<&str> = Some("지수");
if let Some(n) = name {
println!("안녕, {n}!");
}
// None이면 아무 일도 안 해요
}
unwrap_or() — 기본값 제공
None일 때 대신 쓸 값을 지정할 수 있어요.
fn main() {
let port: Option<u16> = None;
let actual_port = port.unwrap_or(8080);
println!("포트: {actual_port}"); // 포트: 8080
let custom: Option<u16> = Some(3000);
let actual = custom.unwrap_or(8080);
println!("포트: {actual}"); // 포트: 3000
}
unwrap_or_default()도 있어요. 타입의 기본값(0, "", false 등)을 사용해요.
fn main() {
let count: Option<i32> = None;
println!("개수: {}", count.unwrap_or_default()); // 개수: 0
let name: Option<String> = None;
println!("이름: '{}'", name.unwrap_or_default()); // 이름: ''
}
map() — 값이 있을 때만 변환
Some 안의 값을 변환하고 싶을 때 map()을 써요. None이면 그대로 None이에요.
fn main() {
let name: Option<&str> = Some("rust");
let upper = name.map(|n| n.to_uppercase());
println!("{:?}", upper); // Some("RUST")
let empty: Option<&str> = None;
let upper = empty.map(|n| n.to_uppercase());
println!("{:?}", upper); // None
}
and_then() — Option을 반환하는 체이닝
map()은 변환 함수가 일반 값을 반환할 때 써요. 변환 함수 자체가 Option을 반환한다면 and_then()을 쓰세요.
fn parse_port(s: &str) -> Option<u16> {
s.parse::<u16>().ok()
}
fn main() {
let input: Option<&str> = Some("8080");
let port = input.and_then(parse_port);
println!("{:?}", port); // Some(8080)
let bad: Option<&str> = Some("abc");
let port = bad.and_then(parse_port);
println!("{:?}", port); // None
}
map()을 썼다면 Option<Option<u16>>이 되어 버려요. and_then()은 이걸 평탄화해서 Option<u16>으로 만들어줘요.
실전 예제: 사용자 검색
여러 메서드를 조합하면 이렇게 깔끔해져요.
struct User {
name: String,
email: Option<String>,
}
fn find_user(users: &[User], name: &str) -> Option<&User> {
users.iter().find(|u| u.name == name)
}
fn main() {
let users = vec![
User { name: "지수".into(), email: Some("jisu@example.com".into()) },
User { name: "민수".into(), email: None },
];
// 사용자를 찾고, 이메일이 있으면 출력
let email = find_user(&users, "지수")
.and_then(|u| u.email.as_deref())
.unwrap_or("이메일 없음");
println!("지수의 이메일: {email}");
let email = find_user(&users, "영희")
.and_then(|u| u.email.as_deref())
.unwrap_or("이메일 없음");
println!("영희의 이메일: {email}");
}
unwrap()은 왜 위험한가?
unwrap()은 None일 때 panic을 일으켜요. 모듈 16에서 Result의 unwrap()이 위험하다고 배웠죠? Option도 마찬가지예요.
fn main() {
let value: Option<i32> = None;
// value.unwrap(); // panic: called `Option::unwrap()` on a `None` value
}
| 메서드 | None일 때 | 용도 |
| --------------------- | ------------------- | ---------------------------------- |
| unwrap() | panic! | 테스트 코드, 절대 None이 아닌 경우 |
| unwrap_or(기본값) | 기본값 반환 | 대체값이 있을 때 |
| unwrap_or_default() | 타입 기본값 | 0, "" 등으로 충분할 때 |
| expect("메시지") | 메시지와 함께 panic | 디버깅용 |
익숙해지면 map(), and_then(), unwrap_or() 체이닝이 정말 편해져요. 처음엔 어색하지만, Rust 코드를 읽다 보면 자연스러워질 거예요!
연습 1 (쉬움). 아래 함수를 완성하세요. 벡터에서 첫 번째 짝수를 찾아 반환해요.
fn first_even(numbers: &[i32]) -> Option<i32> {
// 여기를 완성하세요
// 힌트: for 루프와 if로 짝수를 찾아 Some(n)을 반환하세요
// 못 찾으면 None을 반환하세요
}
fn main() {
let nums = vec![1, 3, 4, 7, 8];
println!("{:?}", first_even(&nums)); // Some(4)
let odds = vec![1, 3, 5];
println!("{:?}", first_even(&odds)); // None
}
연습 2 (보통). 아래 코드에서 unwrap()을 제거하고 안전한 대안으로 바꿔보세요.
fn main() {
let config = vec![
("host", "localhost"),
("port", "8080"),
];
// unwrap()을 쓰지 않고 고쳐보세요!
let host = config.iter()
.find(|(k, _)| *k == "host")
.unwrap()
.1;
let timeout = config.iter()
.find(|(k, _)| *k == "timeout")
.unwrap() // 여기서 panic!
.1;
println!("host: {host}, timeout: {timeout}");
}
힌트: map()과 unwrap_or()를 조합해보세요.
연습 3 (도전). and_then()을 사용해서 중첩된 Option을 처리하는 함수를 작성하세요.
fn get_area_code(phone: Option<&str>) -> Option<&str> {
// phone이 Some이고, "-"를 포함하면 "-" 앞부분(지역번호)을 반환
// 그 외에는 None 반환
// 힌트: and_then()과 split_once()를 사용하세요
}
fn main() {
println!("{:?}", get_area_code(Some("02-1234-5678"))); // Some("02")
println!("{:?}", get_area_code(Some("01012345678"))); // None
println!("{:?}", get_area_code(None)); // None
}
Q1. Option<T>에 대한 설명으로 올바른 것은?
- A) 값이 없으면 null을 반환한다
- B) Some(T)과 None 두 가지로 값의 유무를 나타낸다
- C) 에러가 발생하면 프로그램을 종료한다
- D) Result<T, E>와 완전히 같다
Q2. 다음 코드의 출력은?
fn main() {
let v = vec![10, 20, 30];
let value = v.get(5).unwrap_or(&0);
println!("{value}");
}
- A) 30
- B) None
- C) 0
- D) 프로그램이 panic으로 멈춘다
Q3. map()과 and_then()의 차이는?
- A) 차이가 없다
- B)
map()은 일반 값을 반환하는 함수에,and_then()은 Option을 반환하는 함수에 사용한다 - C)
map()은 None에서만 동작한다 - D)
and_then()은 항상 panic을 일으킨다