[Rust 시리즈 4편] 함수와 제어 흐름

English version

3편까지는 변수와 자료형에 집중했다면, 이제 그 값들을 어떻게 조합하고 흐름을 제어할지 살펴봅니다. Rust의 함수는 fn 키워드로 정의하며, 표현식 기반 언어답게 if나 블록이 값을 돌려줄 수 있다는 점이 특징입니다. 4편에서는 함수 선언, 표현식 vs. 문(statement), 조건 분기, 반복문을 차근차근 정리합니다.

이번 글에서 새로 나오는 용어

  1. 표현식 (expression): 값을 만들어 내는 구문으로, Rust에서는 거의 모든 것이 표현식입니다.
  2. (statement): 실행하지만 값을 돌려주지 않는 구문으로, 변수 선언 등이 여기에 해당합니다.
  3. loop 반복문: 조건 없이 무한 루프를 만들고 break로 빠져나오는 반복 구조입니다.
  4. for 반복문: 이터러블을 순회하는 반복문으로, 배열·슬라이스 등에서 자주 사용합니다.

핵심 개념

  • 함수는 fn 이름(매개변수: 타입) -> 반환타입 { ... } 형태이며, 반환 타입을 생략하면 ()(unit)이 기본입니다. Rust는 동적 타입 언어와 달리 매개변수 타입을 함수 정의에 명시해야 하므로, 함수 시그니처만 봐도 어떤 값이 오가는지 더 분명하게 읽을 수 있습니다.
  • 블록 {}은 마지막 표현식의 값을 반환하므로, 5 + 3 같은 표현식 뒤에는 세미콜론을 붙이지 않아야 값이 돌아옵니다.
  • if 자체가 표현식이므로 let grade = if score >= 90 { "A" } else { "B" };처럼 변수에 바로 대입할 수 있습니다.
  • 반복문은 loop, while, for 세 가지를 우선 익히며, breakcontinue로 흐름을 제어합니다.

코드로 따라하기

1. 함수 기본 형태

fn main() {
    greet("Mathbong");
    let doubled = double(21);
    println!("두 배: {doubled}");
}

fn greet(name: &str) {
    println!("안녕하세요, {name}!");
}

fn double(value: i32) -> i32 {
    value * 2 // 세미콜론 X: 표현식 반환
}

double 함수의 마지막 줄에 세미콜론을 붙이면 ()가 반환되므로 타입 오류가 발생합니다. Rust가 표현식 기반 언어라는 사실을 떠올리면 이유를 금방 이해할 수 있습니다.

예를 들어 아래 두 함수는 겉보기에는 비슷하지만 결과가 다릅니다.

fn add_ok(a: i32, b: i32) -> i32 {
    a + b
}

fn add_wrong(a: i32, b: i32) -> i32 {
    a + b;
    0
}

첫 번째 함수는 마지막 표현식 a + b를 그대로 반환합니다. 반면 세미콜론이 붙으면 그 줄은 값을 돌려주지 않는 문이 되므로, 마지막 반환값을 따로 다시 적어야 합니다.

2. 표현식 vs. 문

fn main() {
    let statement = {
        let helper = 2;
        helper * 3
    }; // 블록 표현식 → 값 6

    let condition = true;
    let result = if condition { 10 } else { 0 };

    println!("statement = {statement}, result = {result}");
}

블록 { ... } 안에서도 마지막 표현식이 값으로 반환됩니다. if 표현식은 모든 분기에서 같은 타입을 돌려줘야 한다는 점만 주의하면 됩니다.

3. if 조건문

fn main() {
    let score = 87;

    if score >= 90 {
        println!("A 등급");
    } else if score >= 80 {
        println!("B 등급");
    } else {
        println!("C 이하");
    }
}

Rust의 조건식에는 반드시 bool 타입이 필요합니다. 따라서 if score와 같은 파이썬 스타일의 truthy 체크는 허용되지 않습니다.

4. 반복문

fn main() {
    let mut count = 0;

    loop {
        count += 1;
        if count == 3 {
            println!("loop 탈출");
            break;
        }
    }

    let mut n = 3;
    while n > 0 {
        println!("while: {n}");
        n -= 1;
    }

    let numbers = [10, 20, 30];
    for value in numbers {
        println!("for: {value}");
    }
}
  • loop는 명시적으로 break를 호출해야 종료됩니다. break value; 형태로 값을 반환해 변수에 대입할 수도 있습니다.
  • while은 조건이 true인 동안 반복합니다.
  • forIterator 구현체를 순회하므로, 배열이나 범위 같은 값을 한 칸씩 꺼내 처리합니다. 예를 들어 0..5는 0부터 4까지를 뜻하고, 1..=5는 1부터 5까지를 뜻합니다.

왜 중요한가

  • 함수와 제어 흐름을 이해해야 이후 ownership 규칙을 함수 경계에서 어떻게 적용할지 설명할 수 있습니다.
  • 표현식 기반 사고방식은 ifmatch, 블록 반환 등을 자연스럽게 받아들이게 하고, 나중에 ? 연산자나 클로저를 배울 때도 도움이 됩니다.
  • 반복문 패턴을 익혀 두면 나중에 이터레이터나 패턴 매칭으로 확장할 때 비교 기준을 세울 수 있습니다.

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가 필요한 예제는 파일 배치를 조금 더 손봐야 할 수 있습니다.

실습

  1. fn max(a: i32, b: i32) -> i32 함수를 작성해 if 표현식으로 더 큰 값을 반환해 봅니다.
  2. loop 안에서 break value를 사용해 피보나치 수열의 n번째 값을 찾고, 결과를 변수에 담아 출력합니다.
  3. for number in (1..=5).rev()처럼 범위를 이용해 역순 루프를 돌려 보고, 범위 구문이 Iterator라는 사실을 확인합니다.
  4. 함수 블록에서 중간에 return을 쓰지 않고 마지막 표현식으로 값을 돌려주는 패턴을 찾아 코드에 적용해 봅니다.

마무리

이제 함수 선언과 제어 흐름을 활용해 작은 프로그램의 뼈대를 직접 세울 수 있습니다. 다음 5편에서는 Rust의 차별점인 ownership 규칙을 본격적으로 다루며, 값이 이동(move)하거나 범위를 벗어날 때(drop) 어떤 일이 일어나는지 이해해 봅니다.

💬 댓글

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