Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

제어 흐름

조건이 true 인지에 따라 어떤 코드를 실행할 수 있는 능력과, 조건이 true 인 동안 어떤 코드를 반복해서 실행할 수 있는 능력은 대부분의 프로그래밍 언어에서 기본이 되는 구성 요소입니다. 러스트 코드의 실행 흐름을 제어하게 해 주는 가장 흔한 구문은 if 식과 루프입니다.

if

if 식은 조건에 따라 코드를 분기하게 해 줍니다. 조건을 제공한 뒤 “이 조건이 성립하면 이 코드 블록을 실행하고, 그렇지 않으면 이 코드 블록을 실행하지 않는다”라고 말하는 방식입니다.

if 식을 실험하기 위해 projects 디렉터리에 branches 라는 새 프로젝트를 만드세요. src/main.rs 파일에 다음 내용을 입력합니다.

파일명: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

모든 if 식은 if 키워드로 시작하고, 그 뒤에 조건이 옵니다. 여기서는 number 변수가 5보다 작은 값인지 확인합니다. 조건이 true 일 때 실행할 코드 블록은 조건 바로 뒤 중괄호 안에 둡니다. if 식에서 조건과 연결된 코드 블록은 2장의 “추측값을 비밀 숫자와 비교하기” 절에서 다룬 match 식의 arm과 비슷하게, 때때로 arm 이라고 부르기도 합니다.

필요하다면 여기서처럼 else 식을 포함해, 조건이 false 로 평가될 때 실행할 대체 코드 블록을 줄 수도 있습니다. else 식을 제공하지 않은 상태에서 조건이 false 가 되면, 프로그램은 if 블록을 건너뛰고 다음 코드로 그냥 넘어갑니다.

이 코드를 실행해 보세요. 다음과 같은 출력이 나타납니다.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was true

이번에는 number 값을 조건이 false 가 되도록 바꿔서 어떤 일이 일어나는지 확인해 봅시다.

fn main() {
    let number = 7;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

프로그램을 다시 실행하고 출력을 확인해 보세요.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was false

또 하나 주목할 점은, 이 코드의 조건은 반드시 bool 이어야 한다는 것입니다. 조건이 bool 이 아니면 오류가 발생합니다. 예를 들어 다음 코드를 실행해 보세요.

파일명: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

이번에는 if 조건이 값 3 으로 평가되므로, 러스트는 오류를 냅니다.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

오류는 러스트가 bool 을 기대했지만 정수를 받았다고 알려 줍니다. Ruby나 JavaScript 같은 언어와 달리, 러스트는 불리언이 아닌 타입을 자동으로 불리언으로 변환하지 않습니다. if 의 조건으로는 항상 명시적으로 불리언을 제공해야 합니다. 예를 들어 숫자가 0 이 아닐 때만 if 코드 블록을 실행하고 싶다면, 다음처럼 if 식을 바꿀 수 있습니다.

파일명: src/main.rs

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");
    }
}

이 코드를 실행하면 number was something other than zero 가 출력됩니다.

else if 로 여러 조건 다루기

ifelse 를 결합한 else if 식으로 여러 조건을 사용할 수 있습니다. 예를 들면 다음과 같습니다.

파일명: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

이 프로그램은 네 가지 가능한 경로를 가집니다. 실행하면 다음과 같은 출력이 보일 것입니다.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

이 프로그램이 실행되면 각 if 식을 차례로 검사하고, 조건이 true 로 평가되는 첫 번째 본문만 실행합니다. 예를 들어 6은 2로도 나누어떨어지지만, number is divisible by 2 는 출력되지 않습니다. 마찬가지로 else 블록의 number is not divisible by 4, 3, or 2 도 보이지 않습니다. 러스트는 첫 번째 true 조건에 해당하는 블록만 실행하고, 그 하나를 찾고 나면 나머지는 검사하지 않기 때문입니다.

else if 식이 너무 많아지면 코드가 복잡해 보일 수 있으므로, 하나보다 많아질 것 같다면 리팩터링을 고려해 보는 것이 좋습니다. 이런 경우를 위해 6장에서는 match 라는 강력한 러스트 분기 구문을 설명합니다.

let 문 안에서 if 사용하기

if 는 식이기 때문에, 목록 3-2처럼 let 문 오른쪽에서 사용해 결과를 변수에 대입할 수 있습니다.

Filename: src/main.rs
fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}
Listing 3-2: if 식의 결과를 변수에 대입하기

number 변수는 if 식의 결과에 따라 값이 바인딩됩니다. 어떤 일이 일어나는지 보기 위해 이 코드를 실행해 보세요.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
The value of number is: 5

코드 블록은 내부의 마지막 식으로 평가된다는 점, 그리고 숫자 하나만 있는 것도 식이라는 점을 기억하세요. 이 경우 전체 if 식의 값은 어느 코드 블록이 실행되느냐에 따라 달라집니다. 따라서 if 의 각 arm에서 결과가 될 수 있는 값들은 모두 같은 타입이어야 합니다. 목록 3-2에서는 if arm과 else arm 모두 i32 정수를 결과로 내놓았습니다. 만약 아래 예시처럼 타입이 맞지 않으면 오류가 납니다.

파일명: src/main.rs

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("The value of number is: {number}");
}

이 코드를 컴파일하면 오류가 발생합니다. ifelse arm이 서로 호환되지 않는 타입의 값을 가지며, 러스트는 프로그램 어디에서 문제가 생겼는지를 정확히 알려 줍니다.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

if 블록 안의 식은 정수로 평가되고, else 블록 안의 식은 문자열로 평가됩니다. 이것은 동작할 수 없습니다. 변수는 하나의 단일 타입만 가져야 하고, 러스트는 컴파일 시점에 number 변수의 타입이 무엇인지 확실히 알아야 하기 때문입니다. number 의 타입을 알고 있어야 컴파일러가 number 를 사용하는 모든 곳에서 그 타입이 유효한지 검사할 수 있습니다. 만약 number 의 타입이 런타임에 가서야 결정된다면 컴파일러는 훨씬 더 복잡해지고, 어떤 변수든 여러 가상의 타입을 추적해야 하므로 코드에 대해 보장할 수 있는 것도 줄어들게 됩니다.

루프로 반복하기

어떤 코드 블록을 한 번 이상 실행해야 하는 경우는 자주 있습니다. 이를 위해 러스트는 여러 가지 루프(loop) 를 제공합니다. 루프는 루프 본문 안의 코드를 끝까지 실행한 뒤 즉시 처음으로 다시 돌아갑니다. 루프를 실험해 보기 위해 loops 라는 새 프로젝트를 만들어 봅시다.

러스트에는 세 종류의 루프가 있습니다. loop, while, for 입니다. 하나씩 모두 살펴보겠습니다.

loop 로 코드 반복하기

loop 키워드는 어떤 코드 블록을 영원히, 혹은 여러분이 명시적으로 멈추라고 말할 때까지 계속 반복해서 실행하라고 러스트에게 지시합니다.

예를 들어 loops 디렉터리의 src/main.rs 파일을 다음처럼 바꿔 봅시다.

파일명: src/main.rs

fn main() {
    loop {
        println!("again!");
    }
}

이 프로그램을 실행하면, 우리가 수동으로 중지할 때까지 again! 이 계속 반복해서 출력됩니다. 대부분의 터미널은 무한 루프에 빠진 프로그램을 중단하기 위해 ctrl-C 키보드 단축키를 지원합니다. 한 번 시도해 보세요.

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

^C 기호는 여러분이 ctrl-C 를 눌렀다는 뜻입니다.

인터럽트 신호를 받았을 때 루프가 어느 지점에 있었는지에 따라, ^C 뒤에 again! 이 보일 수도 있고 보이지 않을 수도 있습니다.

다행히 러스트는 코드 안에서 루프를 빠져나오는 방법도 제공합니다. 루프 안에 break 키워드를 두면 언제 루프 실행을 멈출지를 프로그램에 알려 줄 수 있습니다. 2장의 “정답을 맞히면 종료하기” 절에서 사용자가 정답을 맞혔을 때 프로그램을 종료하기 위해 이것을 이미 써 본 적이 있습니다.

숫자 맞히기 게임에서는 continue 도 사용했었는데, 루프 안에서 continue 는 현재 반복에서 남은 코드를 건너뛰고 다음 반복으로 가라고 프로그램에 지시합니다.

루프에서 값 반환하기

loop 의 한 가지 활용법은, 실패할 수 있다는 것을 알고 있는 작업을 재시도하는 것입니다. 예를 들어 어떤 스레드가 작업을 끝냈는지 계속 확인하는 경우가 그렇습니다. 그리고 그 작업 결과를 루프 밖의 나머지 코드에 전달해야 할 수도 있습니다. 이를 위해 루프를 멈추는 break 식 뒤에 반환하고 싶은 값을 붙일 수 있습니다. 그러면 그 값이 루프 밖으로 반환되어 이후 코드에서 사용할 수 있습니다. 예를 들면 다음과 같습니다.

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

루프 전에 counter 라는 변수를 선언하고 0 으로 초기화합니다. 그런 다음 루프가 반환하는 값을 담기 위해 result 라는 변수를 선언합니다. 루프의 각 반복마다 counter1 을 더하고, 그 뒤 counter10 과 같은지 확인합니다. 같다면 break 키워드와 함께 counter * 2 라는 값을 사용합니다. 루프 뒤에는 세미콜론을 붙여, 이 값이 result 에 대입되는 문장을 끝냅니다. 마지막으로 result 의 값을 출력하는데, 이 경우 값은 20 입니다.

루프 안에서 return 을 사용할 수도 있습니다. break 가 현재 루프만 빠져나가는 반면, return 은 항상 현재 함수 전체를 종료합니다.

루프 레이블로 구분하기

루프 안에 또 다른 루프가 있다면, breakcontinue 는 그 지점에서 가장 안쪽 루프에 적용됩니다. 필요하다면 루프에 레이블(loop label) 을 지정하고, breakcontinue 와 함께 사용해 가장 안쪽 루프가 아니라 그 레이블이 붙은 루프에 적용되게 할 수 있습니다. 루프 레이블은 반드시 작은따옴표 하나로 시작해야 합니다. 다음은 중첩 루프 두 개가 있는 예입니다.

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

바깥쪽 루프에는 'counting_up 이라는 레이블이 있고, 0에서 2까지 올라갑니다. 레이블이 없는 안쪽 루프는 10에서 9로 내려갑니다. 레이블을 지정하지 않은 첫 번째 break 는 안쪽 루프만 종료합니다. break 'counting_up; 문은 바깥쪽 루프를 종료합니다. 이 코드는 다음을 출력합니다.

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

while 로 조건부 루프 단순화하기

프로그램은 루프 안에서 어떤 조건을 평가해야 하는 경우가 많습니다. 그 조건이 true 인 동안 루프가 실행되고, 조건이 더 이상 true 가 아니게 되면 프로그램은 break 를 호출해 루프를 멈춥니다. 이런 동작은 loop, if, else, break 를 조합해서 구현할 수도 있습니다. 원한다면 직접 그렇게 한 번 만들어 봐도 좋습니다. 하지만 이 패턴은 너무 흔해서, 러스트는 이를 위한 내장 언어 구성 요소인 while 루프를 제공합니다. 목록 3-3에서는 while 을 사용해 프로그램을 세 번 반복하며 카운트다운하고, 루프가 끝난 뒤 메시지를 출력하고 종료합니다.

Filename: src/main.rs
fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}
Listing 3-3: 조건이 true 인 동안 코드를 실행하기 위해 while 루프 사용하기

이 구문은 loop, if, else, break 를 썼을 때 필요했을 많은 중첩을 없애 주며, 훨씬 더 명확합니다. 조건이 true 로 평가되는 동안에는 코드가 실행되고, 그렇지 않으면 루프를 빠져나옵니다.

for 로 컬렉션 순회하기

배열 같은 컬렉션의 요소들을 순회할 때 while 구문을 선택할 수도 있습니다. 예를 들어 목록 3-4의 루프는 배열 a 의 각 요소를 출력합니다.

Filename: src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}
Listing 3-4: while 루프로 컬렉션의 각 요소를 순회하기

여기서 코드는 배열 요소를 따라가며 증가합니다. 인덱스 0 에서 시작해, 배열의 마지막 인덱스에 도달할 때까지 반복합니다(즉 index < 5 가 더 이상 true 가 아닐 때까지). 이 코드를 실행하면 배열의 모든 요소가 출력됩니다.

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

예상한 대로 배열의 다섯 값이 모두 터미널에 나타납니다. index 가 어느 시점에 5 가 되더라도, 배열에서 여섯 번째 값을 가져오려 시도하기 전에 루프가 멈춥니다.

하지만 이 접근법은 오류가 나기 쉽습니다. 인덱스 값이나 검사 조건이 잘못되면 프로그램이 패닉을 일으킬 수 있기 때문입니다. 예를 들어 배열 a 정의를 네 개 요소로 바꾸고도 조건을 while index < 4 로 갱신하는 것을 잊어버리면 코드가 패닉을 일으킵니다. 또한 이 방식은 느리기도 합니다. 컴파일러가 루프의 각 반복마다 인덱스가 배열 범위 안에 있는지 조건 검사하는 런타임 코드를 추가하기 때문입니다.

더 간결한 대안으로 for 루프를 사용하면 컬렉션의 각 항목에 대해 코드를 실행할 수 있습니다. for 루프는 목록 3-5와 같습니다.

Filename: src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}
Listing 3-5: for 루프로 컬렉션의 각 요소를 순회하기

이 코드를 실행하면 목록 3-4와 같은 출력을 보게 됩니다. 더 중요한 것은, 이제 코드가 더 안전해졌고 배열 끝을 넘어가거나 일부 요소를 빠뜨리는 버그 가능성도 사라졌다는 점입니다. for 루프에서 생성되는 머신 코드는 매 반복마다 인덱스를 배열 길이와 비교할 필요가 없기 때문에 더 효율적일 수도 있습니다.

for 루프를 사용하면 배열 값 개수를 바꿀 때도 목록 3-4 방식처럼 다른 코드까지 함께 바꾸는 것을 기억할 필요가 없습니다.

안전성과 간결성 덕분에 for 루프는 러스트에서 가장 흔히 쓰이는 반복 구문입니다. 심지어 목록 3-3의 카운트다운 예시처럼 특정 횟수만큼 코드를 실행하고 싶을 때도, 대부분의 Rustacean은 while 대신 for 루프를 사용할 것입니다. 그 방법은 표준 라이브러리가 제공하는 Range 를 사용하는 것입니다. Range 는 한 숫자에서 시작해 다른 숫자 직전까지의 수를 순서대로 생성합니다.

카운트다운을 for 루프와, 아직 설명하지 않은 또 다른 메서드인 rev 를 사용해 범위를 뒤집는 방식으로 작성하면 다음과 같습니다.

파일명: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

이 코드가 조금 더 보기 좋지 않나요?

정리

여기까지 왔다면 잘해낸 것입니다! 꽤 큰 장이었습니다. 여러분은 변수, 스칼라 및 복합 데이터 타입, 함수, 주석, if 식, 루프를 배웠습니다! 이 장에서 다룬 개념을 연습해 보고 싶다면, 다음과 같은 프로그램을 만들어 보세요.

  • 화씨와 섭씨 사이 온도 변환하기
  • n 번째 피보나치 수 구하기
  • 노래의 반복 구조를 활용해 크리스마스 캐럴 “The Twelve Days of Christmas” 가사 출력하기

이제 다음으로 넘어갈 준비가 되었다면, 다른 대부분의 프로그래밍 언어에는 흔히 존재하지 않는 러스트의 개념인 소유권에 대해 이야기하겠습니다.