`if let`을 사용한 간결한 제어 흐름
실패할 수 있는 패턴 하나를 간결하게 처리하는 if let의 동작, 바인딩 범위, else의 합류 의미와 match로 전환할 기준을 익힙니다.
if let은 값 하나를 패턴과 비교해, 그 패턴에 맞을 때만 코드를 실행하는 식입니다. match에서 관심 있는 패턴이 하나뿐이고 나머지는 무시할 때, 같은 일을 더 짧게 쓸 수 있습니다. Some에만 쓰는 문법은 아니어서, 열거형의 배리언트뿐 아니라 리터럴, 범위, 튜플, 구조체처럼 match에 쓰는 패턴을 그대로 쓸 수 있습니다. 이 글에서는 if let과 else, 그리고 원하는 값이 아니면 바로 빠져나가는 let...else를 살펴보고, match로 쓰는 편이 나은 경우를 정리합니다.
한 패턴이 맞을 때만 실행하기
다음 match는 config_max가 Some일 때만 안의 값을 출력합니다. 3u8은 숫자 뒤에 타입을 붙여 u8 타입의 3을 적은 리터럴입니다.
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}값이 Some이면 안의 값을 max에 묶어 출력하고, None이면 아무것도 하지 않습니다. 그런데 match의 철저함을 맞추려면 한 배리언트만 처리한 뒤에도 _ => ()를 덧붙여야 하므로 코드가 길어집니다. 같은 일을 if let으로 쓰면 다음과 같습니다.
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}The maximum is configured to be 3if let은 =로 나눈 패턴과 식을 받습니다. = 왼쪽의 Some(max)가 패턴이고 오른쪽의 config_max가 비교할 값으로, match의 첫 갈래와 같은 방식으로 동작합니다. 패턴이 맞으면 안의 값이 max에 묶이고 본문이 실행되며, 맞지 않으면 본문을 건너뜁니다. max는 패턴이 맞아 실행되는 본문 안에서만 쓸 수 있고, 뒤에 붙는 else나 if let 식 뒤에서는 쓸 수 없습니다. 2024 에디션에서는 if let Some(max) = config_max && max > 2처럼 &&로 조건을 이어 붙이는 let 체인(let chain)도 쓸 수 있습니다.
아래 그림은 if let의 두 실행 경로를 보여 줍니다.
if let의 두 실행 경로와 match 전환 기준
if let의 값을 평가해 일치하면 max 등 바인딩을 만들어 본문만 실행하고 불일치면 else/건너뜀으로 가는 실제 제어다. else는 특정 두 번째 패턴이 아니며 기본형 바인딩은 성공 본문 범위라는 경계를 단계에 남긴다.
IF LET · ONE PATTERN
if let은 한 패턴의 성공과 모든 불일치를 나눈다
관심 있는 패턴이 맞으면 바인딩을 만들어 본문을 실행합니다. else는 패턴에 맞지 않은 모든 값을 한 경로로 모읍니다.
if let P = value
패턴 일치
본문을 실행하고 P가 만든 바인딩을 그 본문 안에서 사용합니다.
패턴 불일치
바인딩은 생기지 않습니다. 모든 불일치 값은 하나의 else로 가거나, else가 없으면 건너뜁니다.
| 판단 축 | if let |
match |
|---|---|---|
| 관심 범위 | 한 패턴 또는 한 or-패턴이 중요함 | 여러 경우가 서로 다른 의미를 가짐 |
| 나머지 값 | 무시하거나 하나의 else로 합침 |
모든 가능성을 갈래로 덮음 |
| 전환 신호 | 짧은 부수 효과 또는 단순한 두 경로 | 갈래별 결과·매치 가드·의미 있는 경우가 늘어남 |
| 변경 감지 | 불일치 값의 종류를 검사하지 않음 | 배리언트를 명시하면 추가 시 누락이 오류가 됨 |
if let유지- 한 패턴 또는 or-패턴만 중요하고, 나머지는 무시하거나 하나의
else로 처리합니다. match로 전환- 경우별 결과, 매치 가드, 서로 다른 의미가 늘어나 모든 갈래를 한눈에 검토해야 합니다.
match의 _도 철저성을 만족하지만 미래 배리언트까지 기존 기본 갈래에 넣습니다. 새 배리언트마다 의미를 재검토해야 한다면 닫힌 열거형의 현재 배리언트를 명시적으로 나열합니다.
if let은 안전하지 않은 축약이 아니라 불일치 값을 의도적으로 한 경로로 접는 표현식입니다. 그 접힘이 더 이상 도메인 의미와 맞지 않을 때 match로 펼칩니다.이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 원래의 4노드와 3간선과 같다. if let 값 평가→패턴 조건. 예는 바인딩이 있는 본문으로, 아니요는 바인딩 없는 else 또는 건너뜀으로 우회한다. 성공 본문에서 else로 이어지는 간선은 없다. 기본형의 바인딩 범위를 보존하며 2024 let 체인 확장과 구별한다. 세로 도표에서도 성공/모든 불일치의 관계가 같고 아래 if let/match 설계 비교표를 그대로 남긴다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 패턴 입력 본문의 입력 Some(201)을 검사한다. |
|
|
| 성공 본문 code=201 바인딩으로 성공 본문을 실행한다. |
|
|
| 후속 경계 불일치 사본에는 code 바인딩이 없다. |
|
|
if let의 두 실행 경로와 match 전환 기준
if let의 값을 평가해 일치하면 max 등 바인딩을 만들어 본문만 실행하고 불일치면 else/건너뜀으로 가는 실제 제어다. else는 특정 두 번째 패턴이 아니며 기본형 바인딩은 성공 본문 범위라는 경계를 단계에 남긴다.
IF LET · ONE PATTERN
if let은 한 패턴의 성공과 모든 불일치를 나눈다
관심 있는 패턴이 맞으면 바인딩을 만들어 본문을 실행합니다. else는 패턴에 맞지 않은 모든 값을 한 경로로 모읍니다.
if let P = value
패턴 일치
본문을 실행하고 P가 만든 바인딩을 그 본문 안에서 사용합니다.
패턴 불일치
바인딩은 생기지 않습니다. 모든 불일치 값은 하나의 else로 가거나, else가 없으면 건너뜁니다.
| 판단 축 | if let |
match |
|---|---|---|
| 관심 범위 | 한 패턴 또는 한 or-패턴이 중요함 | 여러 경우가 서로 다른 의미를 가짐 |
| 나머지 값 | 무시하거나 하나의 else로 합침 |
모든 가능성을 갈래로 덮음 |
| 전환 신호 | 짧은 부수 효과 또는 단순한 두 경로 | 갈래별 결과·매치 가드·의미 있는 경우가 늘어남 |
| 변경 감지 | 불일치 값의 종류를 검사하지 않음 | 배리언트를 명시하면 추가 시 누락이 오류가 됨 |
if let유지- 한 패턴 또는 or-패턴만 중요하고, 나머지는 무시하거나 하나의
else로 처리합니다. match로 전환- 경우별 결과, 매치 가드, 서로 다른 의미가 늘어나 모든 갈래를 한눈에 검토해야 합니다.
match의 _도 철저성을 만족하지만 미래 배리언트까지 기존 기본 갈래에 넣습니다. 새 배리언트마다 의미를 재검토해야 한다면 닫힌 열거형의 현재 배리언트를 명시적으로 나열합니다.
if let은 안전하지 않은 축약이 아니라 불일치 값을 의도적으로 한 경로로 접는 표현식입니다. 그 접힘이 더 이상 도메인 의미와 맞지 않을 때 match로 펼칩니다.이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 원래의 4노드와 3간선과 같다. if let 값 평가→패턴 조건. 예는 바인딩이 있는 본문으로, 아니요는 바인딩 없는 else 또는 건너뜀으로 우회한다. 성공 본문에서 else로 이어지는 간선은 없다. 기본형의 바인딩 범위를 보존하며 2024 let 체인 확장과 구별한다. 세로 도표에서도 성공/모든 불일치의 관계가 같고 아래 if let/match 설계 비교표를 그대로 남긴다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 패턴 입력 본문의 입력 Some(201)을 검사한다. |
|
|
| 성공 본문 code=201 바인딩으로 성공 본문을 실행한다. |
|
|
| 후속 경계 불일치 사본에는 code 바인딩이 없다. |
|
|
비교할 값을 계산한 뒤 패턴에 맞으면 패턴이 만든 변수를 쓰는 본문을 실행하고, 맞지 않으면 else로 가거나 본문을 건너뜁니다. 맞지 않은 경로에는 패턴이 만든 변수가 없습니다. 패턴에는 |로 여러 대안을 묶은 패턴도 쓸 수 있으며, 다음 코드의 패턴은 Some(200)이나 Some(201)에만 맞고 그 값을 code에 묶습니다.
let status = Some(201u16);
if let Some(code @ (200 | 201)) = status {
println!("accepted: {code}");
}accepted: 201code @는 괄호 안의 패턴에 맞은 값을 code라는 이름에 묶습니다. 이 패턴에는 그 밖의 Some 값과 None이 모두 맞지 않습니다. @ 바인딩은 패턴 문법에서 자세히 다룹니다.
if let을 쓰면 입력할 것이 줄고 들여쓰기와 반복 코드가 적어지지만, match가 강제하는 철저한 검사는 잃습니다. if let은 값이 한 패턴에 맞을 때 코드를 실행하고 나머지 값은 무시하는 match를 짧게 쓰는 문법 설탕(syntax sugar)이라고 볼 수 있습니다. 둘 가운데 무엇을 쓸지는 지금 상황에서 짧은 코드와 철저한 검사 가운데 무엇이 더 중요한지에 따라 정합니다.
else는 모든 불일치 값을 한 경로로 모읍니다
if let에 else를 붙일 수 있습니다. else는 두 번째 패턴 하나가 아니라, 앞의 패턴에 맞지 않은 모든 값을 한꺼번에 처리하며, match의 _ 갈래에 해당합니다. match 제어 흐름 구조의 Coin 열거형에서, 쿼터가 나오면 주 이름을 알리고 쿼터가 아닌 동전은 개수만 세는 경우를 match로 쓰면 다음과 같습니다.
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {state:?}!"),
_ => count += 1,
}두 경로만 나누면 되므로 if let과 else로 같은 뜻을 쓸 수 있습니다. match의 첫 갈래는 if let의 본문이 되고, _ 갈래에 있던 count += 1은 아래 코드의 else 블록으로 옮겨 갑니다.
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {state:?}!");
} else {
count += 1;
}동전 여러 개를 차례로 세는 완전한 프로그램으로 바꾸면 다음과 같습니다. 개수는 함수에 &mut으로 빌려 주어 늘립니다.
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn observe_coin(coin: Coin, count: &mut u8) {
if let Coin::Quarter(state) = coin {
println!("State quarter from {state:?}!");
} else {
*count += 1;
}
}
fn main() {
let mut count = 0;
observe_coin(Coin::Quarter(UsState::Alaska), &mut count);
observe_coin(Coin::Nickel, &mut count);
observe_coin(Coin::Dime, &mut count);
println!("non-quarter: {count}");
}State quarter from Alaska!
non-quarter: 2Alaska 쿼터는 주 이름만 출력하고 개수는 0으로 둡니다. Nickel은 else로 가서 개수를 1로, Dime은 2로 늘립니다. *count는 빌려 받은 참조를 따라가 그 값을 고친다는 뜻입니다. 이 프로그램은 Alabama와 Penny를 만들지 않으므로, 컴파일러가 만들지 않는 배리언트가 있다는 경고를 냅니다.
state는 Coin::Quarter(state)가 맞은 본문에만 있습니다. else에는 Penny, Nickel, Dime이 모두 들어오지만, 그 가운데 어느 것이었는지는 알 수 없습니다. if let과 else도 식이므로 두 블록이 같은 타입의 값을 만들 수 있지만, 배리언트마다 다른 값을 만들기 시작하면 match가 더 읽기 쉽습니다.
let...else로 주된 흐름 유지하기
값이 있으면 그 값으로 계산을 이어 가고, 없으면 바로 함수에서 빠져나가는 일도 흔합니다. 예를 들어 쿼터 동전이면 그 주가 오래된 주인지 알려 주고, 쿼터가 아니면 None을 돌려주는 함수를 생각해 봅니다. if let으로 쓰면 주된 계산이 if let의 본문 안에 들어가 한 단계 더 들여써집니다. let...else를 쓰면 패턴이 맞을 때 만든 변수를 바깥 범위에 두고, 맞지 않을 때만 else에서 빠져나가게 할 수 있습니다.
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
let Coin::Quarter(state) = coin else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
fn main() {
println!("{:?}", describe_state_quarter(Coin::Quarter(UsState::Alabama)));
println!("{:?}", describe_state_quarter(Coin::Quarter(UsState::Alaska)));
println!("{:?}", describe_state_quarter(Coin::Penny));
}Some("Alabama is pretty old, for America!")
Some("Alaska is relatively new.")
Nonelet Coin::Quarter(state) = coin else { ... };는 coin이 패턴에 맞으면 state를 이 함수의 범위에 만들고 다음 줄로 넘어갑니다. 맞지 않으면 else 블록이 실행되는데, 이 블록은 return이나 panic!처럼 반드시 지금의 흐름을 벗어나야 하며, 반복문 안이라면 break나 continue도 쓸 수 있습니다. 그래서 else 뒤의 코드에서는 state가 언제나 있다고 믿고 쓸 수 있습니다. 1819년에 생긴 Alabama는 1900년에 이미 있었고, 1959년에 생긴 Alaska는 없었으므로 두 메시지가 다르게 나오며, Penny는 else에서 None을 돌려줍니다. format!은 println!처럼 문자열을 만들되 출력하지 않고 String으로 돌려주는 매크로입니다.
match로 전환할 기준
다음과 같은 경우에는 짧은 코드보다 match의 구조가 더 중요합니다. 여러 경우가 저마다 다른 뜻과 동작을 가질 때, 갈래마다 다른 값을 돌려 하나의 값을 만들 때, 패턴마다 매치 가드를 붙여 조건을 좁힐 때, 그리고 열거형의 모든 배리언트를 하나씩 검토하고 빠뜨린 경우를 컴파일 오류로 잡고 싶을 때입니다.
match는 모든 경우를 다뤄야 하지만, _도 나머지를 모두 다루는 올바른 포괄 패턴입니다. 그래서 지금의 배리언트를 _로 묶어 두면, 나중에 새 배리언트를 더해도 그 배리언트가 조용히 _ 갈래로 들어가 컴파일러가 알려 주지 않습니다. 정말 같은 기본 동작이 맞을 때만 _를 쓰고, 새 배리언트마다 동작을 다시 정해야 한다면 배리언트를 모두 나열합니다.
Option<T>에서 Some일 때만 무언가를 하고 None은 일부러 무시한다면 if let이 잘 맞습니다. Some과 None이 각각 결과를 만들거나 경우마다 처리가 늘어난다면 match가 낫고, 원하는 값이 아니면 바로 빠져나가는 경우라면 let...else가 주된 흐름을 깔끔하게 유지합니다.
더 깊이: 2024 에디션에서 달라진 if let의 임시 값
if let의 = 오른쪽 식을 계산하면서 생긴 임시 값(temporary)이 언제 버려지는지는 에디션에 따라 다릅니다. 2021 에디션까지는 이 임시 값이 else 블록까지 포함한 if let 식 전체가 끝날 때 버려졌습니다. 에디션 가이드의 예처럼 if let Some(x) = *value.read().unwrap()이라고 쓰면, read가 만든 읽기 락이 else에서도 살아 있습니다. 그래서 else에서 같은 RwLock의 쓰기 락을 얻으려 하면 교착 상태에 빠졌습니다. 2024 에디션은 이 임시 값을 본문이 끝나거나 else로 들어가기 전에 버리도록 바꿔, 이 코드가 교착 상태 없이 동작하게 했습니다.
본문에서 말한 let 체인을 2024 에디션에서만 쓸 수 있는 것도 이 변경 때문입니다. Rust 1.88 발표는 let 체인이 버려지는 순서를 일관되게 하려고 이 임시 값 범위의 변경에 기대며, 그래서 2024 에디션 크레이트에서만 쓸 수 있다고 설명합니다. 2021 에디션 코드를 옮길 때는 cargo fix --edition이 함께 실행하는 if_let_rescope 린트가, 버려지는 시점이 바뀌는 if let을 찾아 알려 줍니다.
정리
if let 패턴 = 값은 값이 패턴에 맞을 때만 본문을 실행하며, 패턴이 만든 변수는 그 본문에서만 쓸 수 있습니다.if let은 한 패턴만 처리하고 나머지를 무시하는match를 짧게 쓴 것이며, 그 대신 철저한 검사를 하지 않습니다.if let의else는 패턴에 맞지 않은 모든 값을 한 경로로 처리하며,match의_갈래에 해당합니다.let 패턴 = 값 else { ... };는 맞으면 변수를 바깥 범위에 만들고, 맞지 않으면 반드시 빠져나가는else블록을 실행합니다.- 경우마다 다른 동작이나 값, 가드가 필요하거나 모든 배리언트를 검토해야 한다면
match를 씁니다.
연습 문제
1. 동전을 더 세기
observe_coin 예제에서 마지막 println! 앞에 Alabama 쿼터와 Penny를 차례로 한 번씩 더 넘기면, 출력은 어떻게 바뀌나요?
해설 보기
State quarter from Alaska! 다음에 State quarter from Alabama!가 한 줄 더 나오고, 마지막 줄은 non-quarter: 3이 됩니다. Alabama 쿼터는 주 이름만 출력하고 개수를 늘리지 않으므로 그때 개수는 2이고, Penny가 else로 가서 3이 됩니다. 넘긴 동전은 모두 다섯 개이지만 세는 것은 쿼터가 아닌 세 개입니다. 이제 Alabama와 Penny도 만들었으므로 그 배리언트에 대한 경고는 사라집니다.
2. 맞지 않는 상태 코드
status 예제에서 status를 (가) Some(202u16), (나) None으로 바꾸면 무엇이 출력되나요? 본문 밖에서 code를 쓸 수 있나요?
해설 보기
둘 다 아무것도 출력하지 않습니다. Some(202)는 Some이지만 안의 값이 200이나 201이 아니므로 패턴에 맞지 않고, None도 맞지 않으므로 본문을 건너뜁니다. code는 패턴이 맞아 실행되는 본문 안에만 있으므로 본문 밖에서 쓰면 컴파일되지 않습니다.
3. 어떤 구조로 쓸까
(가) 설정 값이 있으면 로그에 한 줄 남기고 없으면 아무것도 하지 않기, (나) Coin의 네 배리언트마다 다른 센트 값을 돌려주기, (다) 함수 첫머리에서 인자가 Some이 아니면 바로 None을 돌려주고 나머지 계산은 그 값으로 이어 가기에는 각각 무엇이 알맞나요?
해설 보기
(가)는 한 패턴만 처리하고 나머지를 무시하므로 if let이 알맞습니다. (나)는 배리언트마다 다른 값을 돌려주고 빠뜨린 배리언트를 컴파일러가 잡아 주어야 하므로 match가 알맞습니다. (다)는 맞지 않을 때 바로 빠져나가고 맞은 값을 바깥 범위에서 계속 쓰므로 let...else가 알맞습니다.
이것으로 5장을 마칩니다. 구조체와 열거형으로 프로그램이 다루는 값을 타입으로 만들고, Option<T>로 값이 없을 수 있다는 것을 타입에 담고, match와 if let으로 그 값을 안전하게 분해했습니다. 다음 장의 패키지와 크레이트에서는 커지는 프로그램을 패키지와 크레이트, 모듈로 나누어 관리하는 방법을 살펴봅니다.