이종관
글 목록으로

Rust 개요: 안전성과 성능을 겸비한 시스템 언어

소유권, 차용, 라이프타임, 트레이트, async/await 등 Rust 핵심 개념 총정리

2025년 1월 30일·54 min read·
backend
rust
systems-programming
ownership
concurrency

아래는 Rust를 처음 접하거나, 기초를 확실히 다지려는 분들을 위한 각 단계별 상세 설명입니다. Rust의 기본 개념부터 문법, 메모리 관리, 자료구조 등에 대해 차례대로 알아보겠습니다.

1단계: 기초 다지기

1. 러스트 소개 및 개발 환경 설정

1) 언어 개요: 러스트의 탄생 배경 및 특징

  • 탄생 배경
    Rust는 Mozilla에서 2006년경부터 개발하기 시작하여 2010년대 초에 공개된 프로그래밍 언어입니다. 주된 목표는 “안전성(safety)”, “성능(performance)”, “동시성(concurrency)”을 모두 만족하는 시스템 프로그래밍 언어를 만드는 것이었습니다.
    C/C++과 유사한 시스템 프로그래밍 분야를 지향하지만, 러스트 특유의 메모리 안전성(Ownership/ Borrowing 개념)과 빌드 시점의 철저한 검사(compiler checks)로 인해 런타임 에러(예: 널 포인터 참조, 데이터 레이스 등)를 효과적으로 방지할 수 있습니다.

  • 주요 특징

    1. 안전성(safety)
      러스트는 컴파일 타임에 많은 검사(예: 소유권, 생명주기, 데이터 레이스 검사)를 수행하여, 런타임에 발생할 수 있는 메모리 오류나 동시성 버그를 방지합니다.
    2. 성능(performance)
      시스템 프로그래밍 언어답게 C/C++과 유사한 수준의 퍼포먼스를 기대할 수 있습니다. GC(Garbage Collector)를 사용하지 않고, 필요한 경우 저수준 메모리 제어도 가능합니다.
    3. 동시성(concurrency)
      러스트는 스레드 안전성을 보장해주는 Ownership/Thread-Safety 규칙을 통해, 안전하고 효율적인 동시성 프로그래밍을 지원합니다.

2) 개발 환경: Rust 설치(rustup, Cargo) 및 IDE/에디터 선택

  • Rust 설치

    • rustup을 사용하는 것이 일반적이며, 한 번에 Rust 컴파일러(rustc)와 패키지 매니저(cargo)를 설치할 수 있습니다.
    • Windows, macOS, Linux 모두 간단한 스크립트 실행으로 설치 가능합니다.
    • 설치 완료 후 rustc --version, cargo --version 커맨드로 버전 확인이 가능합니다.
  • IDE/에디터 선택

    • Visual Studio Code
      • Rust용 확장(예: “rust-analyzer”)을 설치하여 코드 자동 완성, 린트, 디버깅을 지원받을 수 있습니다.
    • IntelliJ IDEA / CLion
      • JetBrains의 Rust 플러그인을 사용하면, 유사한 코드 인사이트와 린트 기능을 제공합니다.
    • 이외 Vim, Emacs, Neovim 등 에디터용 플러그인도 다양하게 존재합니다.

3) 기본 도구 사용: rustc(컴파일러), cargo(패키지 매니저) 기본 명령어

  • rustc

    • Rust 컴파일러로, 단일 파일을 직접 컴파일할 수 있습니다. 예:

      bash
      rustc main.rs
      ./main
  • cargo

    • Rust 생태계의 패키지 매니저이자 빌드 시스템.
    • 프로젝트 생성: cargo new 프로젝트이름
    • 빌드: cargo build (디버그 빌드)
    • 실행: cargo run (빌드 후 자동 실행)
    • 배포용 빌드: cargo build --release (최적화 빌드)
    • 의존성 관리: Cargo.toml 파일을 통해 라이브러리(크레이트) 종속성을 설정하고, cargo build 시 자동으로 다운로드/빌드합니다.

2. 러스트 기본 문법

1) 프로그램 구조: main 함수, 모듈 구조, 파일 구조

  • main 함수

    • C 계열 언어처럼 프로그램의 진입점으로서 main 함수가 필요합니다.

    • 예:

      rust
      fn main() {
          println!("Hello, Rust!");
      }
  • 모듈 구조

    • Rust에서는 mod 키워드를 사용해 모듈을 정의하고, use 키워드로 가져와 사용할 수 있습니다.
    • 프로젝트 규모가 커지면, 여러 파일로 나누어 모듈 트리를 구성할 수 있습니다.
  • 파일 구조

    • src 디렉터리 안에 main.rs가 있는 경우, cargo run 시 해당 파일을 실행 진입점으로 인식합니다.
    • 라이브러리 크레이트(lib.rs)인 경우는, main.rs 대신 라이브러리 형태로 구성할 수도 있습니다.

2) 기본 자료형: 정수형, 부동소수점형, 불리언, 문자, 문자열(str, String)

  • 정수형
    • 부호 있는 정수: i8, i16, i32, i64, i128, isize
    • 부호 없는 정수: u8, u16, u32, u64, u128, usize
    • 디폴트는 i32
  • 부동소수점형
    • f32, f64 (기본은 f64)
  • 불리언
    • bool, 값은 true 또는 false
  • 문자
    • char, 유니코드 스칼라 값을 표현 (예: 'a', '한', '😊')
  • 문자열
    • &str: 문자열 슬라이스(immutable, 스택에 참조)
    • String: 힙에 저장되며 가변(mutable) 가능

3) 변수와 가변성: let, mut, 상수(const)

  • let

    • 변수 선언 시 사용, 기본적으로 불변(immutable)입니다.

    • 예:

      rust
      let x = 5; // x는 불변
  • mut

    • 변수 앞에 mut 키워드를 붙여주면 가변(값 변경 가능) 변수가 됩니다.

    • 예:

      rust
      let mut y = 10;
      y = 20; // 값 변경 가능
  • 상수(const)

    • 런타임이 아닌 컴파일 타임에 결정되는 상수를 정의합니다.

    • 반드시 타입 어노테이션이 필요합니다.

    • 예:

      rust
      const MAX_POINTS: u32 = 100_000;

4) 함수와 스코프: 함수 정의, 반환 값, 스코프 개념

  • 함수 정의

    • 함수는 fn 키워드를 사용하여 선언합니다.

    • 매개변수 타입은 반드시 명시해야 합니다.

    • 예:

      rust
      fn add(a: i32, b: i32) -> i32 {
          a + b // 세미콜론이 없으면 이 값이 반환값으로 간주
      }
  • 반환 값

    • 함수는 마지막 표현식 혹은 return 키워드로 값을 반환합니다.
  • 스코프

    • 중괄호 {} 블록이 하나의 스코프를 형성합니다.
    • 스코프가 끝나면 해당 스코프 내에서 생성된 변수는 메모리에서 해제(drop)됩니다(Ownership 규칙).

3. Ownership(소유권)과 Borrowing(빌림)

러스트의 핵심 철학인 메모리 안전성은 ‘소유권’ 개념으로 구현됩니다. 이를 통해 컴파일 타임에 메모리 관련 오류를 방지하고, 런타임 비용 없이 안전성을 확보합니다.

1) Ownership 규칙: 스택과 힙, 변수 이동(Move), 복사(Copy) 개념

  1. 모든 값은 하나의 소유자(owner)를 가진다.
  2. 소유자가 스코프를 벗어나면, 그 값은 메모리에서 해제된다(자동 drop).
  3. 단일 소유권이 이동(move)될 수 있다.
  • 스택과 힙

    • 스택: 크기가 컴파일 시점에 고정된 자료(예: 정수, 부동소수점, bool 등)
    • 힙: 런타임에 동적으로 할당되는 자료(예: String, Vec<T> 등)
  • Move

    • 힙 데이터를 가진 변수를 다른 변수에 대입하면, 원래 변수의 소유권이 이동하며, 원래 변수는 더 이상 유효하지 않아 사용 불가능해집니다.

    • 예:

      rust
      let s1 = String::from("hello");
      let s2 = s1; // s1의 소유권이 s2로 이동
      // s1은 더 이상 유효하지 않음
  • Copy

    • 스택에 저장되는 단순 값 타입(예: i32, bool 등)은 Copy 트레잇을 구현하여, 대입 시 깊은 복사가 일어납니다.

    • 예:

      rust
      let x = 5;
      let y = x; // x와 y 모두 사용 가능 (복사)

2) Borrowing: 참조(Reference)와 참조의 유효 범위

  • 참조(Reference)
    • & 연산자를 통해 다른 변수의 값을 빌려올 수 있습니다.

    • 소유권은 옮기지 않고 읽기 전용 권한만 빌려서 사용할 수 있습니다.

    • 예:

      rust
      fn main() {
          let s1 = String::from("hello");
          let len = calculate_length(&s1); // s1을 참조로 넘김
          println!("길이: {}, 값: {}", len, s1);
      }
       
      fn calculate_length(s: &String) -> usize {
          s.len()
      }
    • 함수에 &s1을 전달해도 s1 자체의 소유권은 변하지 않습니다.

3) 가변 참조(Mutable reference): &mut

  • &mut 키워드를 사용하면 가변 참조가 가능해집니다.

  • 단, 특정 스코프 내에서 “단 하나의 가변 참조만” 존재할 수 있습니다(데이터 레이스 방지).

  • 예:

    rust
    let mut s = String::from("hello");
    change(&mut s);
     
    fn change(some_string: &mut String) {
        some_string.push_str(", world");
    }

4) 스마트 포인터 기초: Box<T>의 간단한 사용

  • Box<T>
    • 힙에 데이터를 저장하고, 스택에는 포인터(주소)만 유지하는 스마트 포인터입니다.

    • 트레이트 객체 등을 다룰 때도 자주 사용됩니다.

    • 간단한 예:

      rust
      let b = Box::new(5);
      println!("b = {}", b);
    • Box는 가리키는 데이터가 스코프를 벗어날 때 자동으로 drop됩니다.

4. 제어문과 기본 자료구조

1) 제어문: if, else if, match, while, loop, for

  • if / else if / else

    rust
    let number = 7;
    if number < 5 {
        println!("작다");
    } else if number > 10 {
        println!("크다");
    } else {
        println!("중간이다");
    }
  • match

    • 패턴 매칭을 통한 분기, switch와 유사하면서도 더 강력합니다.
    rust
    let x = 3;
    match x {
        1 => println!("1!"),
        2 | 3 => println!("2나 3!"),
        _ => println!("그 외"),
    }
  • while

    • 조건이 true인 동안 반복.
    rust
    while condition {
        // ...
    }
  • loop

    • 무한 루프. breakreturn으로 빠져나옵니다.
    rust
    loop {
        println!("반복 중");
        break;
    }
  • for

    • 주로 컬렉션 순회 시 사용.
    rust
    let arr = [10, 20, 30];
    for element in arr.iter() {
        println!("값: {}", element);
    }

2) 컬렉션: Vector, String, HashMap

  • Vector<T> (Vec<T>)

    • 가변 길이 리스트. 배열과 달리 런타임에 크기를 늘리거나 줄일 수 있습니다.

    • 예:

      rust
      let mut v = Vec::new();
      v.push(1);
      v.push(2);
  • String

    • 힙에 저장되는 가변 문자열.
    • push_str, push 등을 통해 문자열을 이어붙일 수 있습니다.
  • HashMap<K, V>

    • 키-값 쌍을 저장하는 자료구조.

    • std::collections에서 제공.

    • 예:

      rust
      use std::collections::HashMap;
       
      let mut scores = HashMap::new();
      scores.insert("Blue", 10);
      scores.insert("Red", 50);

3) 열거형(Enums) & 패턴 매칭: enum의 정의, match를 이용한 분기

  • 열거형(Enums)

    • 관련된 여러 종류의 값을 하나의 타입으로 묶어서 표현 가능.

    • 예:

      rust
      enum IpAddrKind {
          V4,
          V6,
      }
       
      fn route(ip_kind: IpAddrKind) { /* ... */ }
       
      fn main() {
          let four = IpAddrKind::V4;
          let six = IpAddrKind::V6;
          route(four);
          route(six);
      }
  • match를 사용한 분기

    • 각 variant에 따라 다르게 동작시킬 수 있습니다.
    rust
    enum Coin {
        Penny,
        Nickel,
        Dime,
        Quarter,
    }
     
    fn value_in_cents(coin: Coin) -> u8 {
        match coin {
            Coin::Penny => 1,
            Coin::Nickel => 5,
            Coin::Dime => 10,
            Coin::Quarter => 25,
        }
    }

4) Option & Result: 값이 있거나 없음(Option), 에러 처리(Result)

  • Option<T>

    • Rust의 널(null) 개념 대체로, 값이 있거나 없음을 타입 시스템으로 표현합니다.
    • Some(T) 또는 None 두 가지 variant를 가집니다.
    rust
    let some_number = Some(5);
    let absent_number: Option<i32> = None;
  • Result<T, E>

    • 오류(Error)를 처리하는 열거형.
    • Ok(T) 또는 Err(E) variant를 가집니다.
    • ? 연산자를 사용해 간단하게 에러 전파가 가능합니다.
    rust
    fn read_file() -> Result<String, io::Error> {
        let mut s = String::new();
        File::open("hello.txt")?.read_to_string(&mut s)?;
        Ok(s)
    }
    • 에러 처리를 통해 안전하게 런타임 예외를 방지하고, 예측 가능한 방법으로 흐름을 제어할 수 있습니다.

5. 라이프타임(Lifetime)

1) 라이프타임 개념

  • Rust에서 라이프타임(lifetime) 은 “참조가 유효한 범위(scope)”를 의미합니다.
  • 소유권과 참조(Ownership & Borrowing)를 철저히 검사하는 러스트 컴파일러는, 서로 다른 라이프타임을 가진 참조들이 안전하게 사용되는지 확인하기 위해 때로는 라이프타임을 코드로부터 추론하거나, 명시적으로 지정하게끔 요구합니다.
  • 목표: 유효하지 않은 참조(dangling pointer)나 이중 해제(double free) 등을 컴파일 타임에 방지.

2) 함수 파라미터에서의 라이프타임: 'a 등 라이프타임 명시 방법

  • 함수에 참조 타입의 매개변수가 있을 때, 서로 다른 참조 간의 생존 기간을 컴파일러가 추론할 수 없는 경우, 라이프타임 파라미터를 명시해야 합니다.

    rust
    // 라이프타임 파라미터 'a를 사용하여, 입력 참조 x, y와 반환값이 모두 같은 라이프타임 'a 범위 내에 있음을 표기
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
     
    fn main() {
        let s1 = String::from("long string");
        let s2 = "short";
     
        let result = longest(s1.as_str(), s2);
        println!("더 긴 문자열: {}", result);
    }
  • 'a는 관습적으로 많이 사용하는 라이프타임 파라미터 이름이며, 여러 개가 필요하면 'b, 'c 등을 사용할 수 있습니다.

3) 구조체와 라이프타임: 구조체, 메서드 내 라이프타임 주의

  • 구조체가 참조 필드를 가진다면, 필드의 참조 유효 범위를 나타내기 위해 라이프타임 파라미터를 명시해야 합니다.

    rust
    struct ImportantExcerpt<'a> {
        part: &'a str,
    }
     
    impl<'a> ImportantExcerpt<'a> {
        fn level(&self) -> i32 {
            3
        }
    }
     
    fn main() {
        let novel = String::from("Rust is fun. Really fun!");
        let first_sentence = novel.split('.').next().expect("문장이 없습니다.");
        let i = ImportantExcerpt { part: first_sentence };
        println!("Excerpt: {}", i.part);
    }
  • 구조체 메서드를 정의할 때에도, 구조체에 선언된 라이프타임 파라미터를 사용하거나, 메서드 자체에 별도로 라이프타임을 정의해야 하는 경우가 있습니다.

4) 추론: 컴파일러의 라이프타임 추론과 언제 명시가 필요한지

  • 러스트 컴파일러는 라이프타임 추론(elision) 규칙을 통해, 명시하지 않아도 충분히 추론 가능한 경우라면 에러 없이 빌드합니다. 대표적으로 아래 상황에서 자동으로 추론합니다.
    1. 입력 참조가 하나뿐일 때 -> 반환 값에 자동으로 동일 라이프타임을 부여
    2. 메서드의 첫 번째 파라미터가 &self (or &mut self) 인 경우 등
  • 그러나 참조가 여러 개이거나, 스코프가 복잡하게 얽혀 있으면 컴파일러가 추론 불가능하므로 오류를 내고, 직접 'a 등을 써서 라이프타임 관계를 표현해야 합니다.

6. 제네릭(Generics)과 트레이트(Trait)

1) 제네릭(Generics): 타입 파라미터(<T>) 사용법

  • Rust 함수나 구조체 등을 선언할 때 타입 파라미터를 통해 다양한 타입에서 동작하도록 일반화할 수 있습니다.

    rust
    // 제네릭 함수 예시
    fn largest<T: PartialOrd>(list: &[T]) -> &T {
        let mut max = &list[0];
        for item in list {
            if item > max {
                max = item;
            }
        }
        max
    }
    fn main() {
        let numbers = vec![1, 2, 3, 10, 4];
        println!("{}", largest(&numbers)); // 10
    }
  • <T>, <U> 등으로 다양한 타입 파라미터를 받을 수 있고, 여러 제네릭 파라미터를 선언할 수 있습니다.

  • impl<T>로 구조체나 메서드를 제네릭하게 선언하는 것도 가능합니다.

2) 트레이트(Trait): 정의, 구현(impl), 디폴트 메서드

  • 트레이트(Trait) 는 “어떤 타입이 제공해야 하는 메서드의 집합”을 정의합니다. C++의 인터페이스/자바의 인터페이스와 유사한 개념이지만, 약간의 차이가 있습니다.

    rust
    pub trait Summary {
        fn summarize(&self) -> String;
        
        // 디폴트 메서드
        fn summarize_author(&self) -> String {
            String::from("(저자 정보 없음)")
        }
    }
  • 이를 구현(impl)하여 각 타입마다 구체적인 메서드를 정의합니다.

    rust
    struct NewsArticle {
        headline: String,
        author: String,
    }
     
    impl Summary for NewsArticle {
        fn summarize(&self) -> String {
            format!("{} by {}", self.headline, self.author)
        }
        // summarize_author는 디폴트 구현이 있으므로, 재정의하지 않아도 됨
    }

3) 트레이트 바운드(Trait Bound): where 구문, 여러 바운드 사용

  • 트레이트 바운드: 제네릭 함수나 구조체에서 특정 트레이트를 구현한 타입만 사용하도록 제한할 때 사용합니다.

    rust
    // 함수 파라미터 T가 Summary 트레이트를 구현해야 함
    fn notify<T: Summary>(item: &T) {
        println!("Breaking news: {}", item.summarize());
    }
  • 바운드가 여러 개인 경우, + 로 연결하거나 where 구문을 사용해 가독성을 높일 수 있습니다.

    rust
    fn some_function<T, U>(t: T, u: U) 
        where T: Display + Clone,
              U: Clone + Debug
    {
        // ...
    }

4) 트레이트 오브젝트(Trait Object): 동적 디스패치(dyn Trait)

  • 트레이트 오브젝트를 사용하면, 컴파일 시점이 아닌 런타임에 “어떤 트레이트를 구현한 타입”인지를 결정할 수 있습니다(동적 디스패치).

  • Box<dyn Trait> 형태로 많이 사용합니다.

    rust
    fn main() {
        let article = NewsArticle { /* ... */ };
        let tweet = Tweet { /* ... */ };
     
        // 서로 다른 타입이지만, 둘 다 Summary 트레이트 구현
        let items: Vec<Box<dyn Summary>> = vec![
            Box::new(article),
            Box::new(tweet),
        ];
     
        for item in items {
            println!("요약: {}", item.summarize());
        }
    }
  • 컴파일 시점에 타입이 고정되지 않으므로, 런타임 오버헤드(가상 메서드 테이블 사용)가 발생한다는 점 유의.

7. 에러 처리 고급

1) Result 활용: 에러 전파(? 연산자), 표준 라이브러리 에러 처리

  • Rust는 예외(Exception)이 아닌 Result<T, E> 를 통한 함수 반환으로 에러 처리를 권장합니다.

  • 에러가 발생하면 Err를, 정상 동작이면 Ok를 반환하여, 호출자가 이를 처리할 수 있도록 합니다.

  • ? 연산자를 사용해 함수 내부에서 에러를 간결히 전파할 수 있습니다.

    rust
    fn read_file(path: &str) -> Result<String, std::io::Error> {
        let mut s = String::new();
        std::fs::File::open(path)?.read_to_string(&mut s)?;
        Ok(s)
    }
  • 에러 처리를 꼼꼼히 함으로써, 런타임 에러를 안전하게 제어하고 프로그램이 예측 불가능하게 중단되는 상황을 방지합니다.

2) thiserror, anyhow 라이브러리: 에러 정의 및 핸들링 베스트 프랙티스

  • 프로젝트 규모가 커질수록 에러 처리를 체계적으로 관리하는 것이 중요합니다.

  • thiserror: 사용자 정의 에러 타입을 쉽게 만들 수 있도록 도와주는 매크로 기반 라이브러리

  • anyhow: 여러 종류의 에러를 단일 타입(anyhow::Error)으로 래핑하여 간단히 전파하고, 원인을 추적하기 쉽게 만들어 줍니다.

    rust
    use thiserror::Error;
     
    #[derive(Error, Debug)]
    pub enum MyError {
        #[error("파일을 열 수 없습니다: {0}")]
        FileOpenError(std::io::Error),
        #[error("유효하지 않은 입력 데이터")]
        InvalidInput,
    }
     
    // anyhow 사용 예시
    use anyhow::{Context, Result};
     
    fn do_something() -> Result<()> {
        let content = std::fs::read_to_string("config.toml")
            .with_context(|| "config.toml 파일을 읽는 중 오류 발생")?;
        // ...
        Ok(())
    }

3) 패닉(Panic)과 복구 가능/불가능 에러

  • panic!: 프로그램이 더 이상 정상 동작할 수 없는 치명적 상황에서 호출됩니다.
  • 복구 불가능한 에러: 논리적으로 계속 진행이 불가능한 경우(예: out of bound, 치명적 버그)
  • 복구 가능한 에러: Result로 처리 가능한 I/O 에러, 네트워크 에러 등
  • unwrap, expect: 에러 시 즉시 panic!을 일으켜 실행 중단. 데모나 간단한 테스트에서는 유용하지만, 실제 프로덕션 코드에서는 에러 처리가 필요합니다.

8. 러스트 표준 라이브러리와 생태계

1) 표준 라이브러리 주요 구조체/함수: std::collections, std::fs, std::io

  • std::collections:
    • Vec<T>, HashMap<K, V>, HashSet<T>, BTreeMap<K, V>, LinkedList<T> 등 다양
  • std::fs:
    • 파일 열기/읽기/쓰기(File, read_to_string, write 등)
  • std::io:
    • 입출력 스트림(stdin, stdout), 버퍼(BufReader, BufWriter), 오류(Error) 등
  • 그 외 std::thread, std::sync(동시성), std::time(시간 측정) 등 유용한 모듈이 많습니다.

2) Cargo 기능 살펴보기: 의존성, 릴리스/디버그 빌드, 스크립트(build.rs)

  • 의존성 관리: Cargo.toml 파일의 [dependencies] 섹션에서 크레이트를 추가하면, cargo build 시 자동으로 필요한 라이브러리를 다운로드 및 빌드합니다.

  • 릴리스/디버그 빌드:

    • 디버그: 기본 모드, 빠른 빌드 속도, 최적화 최소화

      bash
      cargo build
    • 릴리스: 최적화 최대화, 빌드 속도 느림

      bash
      cargo build --release
  • 스크립트(build.rs): 빌드 시 특정 코드를 실행해야 하는 경우(예: 프로토콜 버퍼 생성, C 라이브러리 빌드 등)에 사용. Cargo가 빌드 전에 build.rs를 실행해 필요한 작업을 수행할 수 있습니다.

3) 커뮤니티 & 생태계: crates.io, Rust Cookbook, Awesome Rust

  • crates.io:
    • Rust의 공식 패키지 저장소. 라이브러리를 쉽게 검색, 설치 가능.
    • cargo add crate_name (Cargo 1.62+에서 지원) 명령어로 편리하게 의존성을 추가할 수 있습니다.
  • Rust Cookbook:
    • Rust 언어 공식 문서에서 제공하는 예제 모음. 흔히 하는 작업(파일 I/O, 문자열 파싱, HTTP 요청 등)에 대한 예시 코드가 정리되어 있습니다.
  • Awesome Rust:
    • GitHub에서 유지되는 인기 라이브러리, 예제, 프로젝트 목록. 생태계를 파악하거나 유용한 라이브러리를 찾기 좋습니다.

9. 모듈과 패키지 구조

1) 모듈 시스템: mod, use, pub를 이용한 가시성 제어

  • Rust의 모듈 시스템은 소스 코드를 논리적으로 분리/재배치하고, 각 부분의 공개 여부(pub)를 결정할 수 있게 합니다.

  • 정의: mod my_module 로 선언, 또는 별도의 파일 my_module.rs를 사용

  • 가져오기: use crate::my_module::SubModule;

  • 가시성 제어: pub 키워드를 통해 외부 모듈에서 접근 가능 여부를 설정

    rust
    // src/lib.rs
    pub mod network {
        pub fn connect() {
            println!("네트워크 연결 시도");
        }
        
        fn private_helper() {
            println!("네트워크 내부 헬퍼");
        }
    }

2) 패키지/크레이트 구조: 라이브러리 크레이트, 바이너리 크레이트 구분

  • 패키지(package): Cargo가 관리하는 하나 이상의 크레이트(crate)로 구성된 단위
    • Cargo.toml이 있는 루트 디렉터리가 패키지의 시작점
  • 크레이트(crate):
    • 라이브러리 크레이트(lib.rs): 다른 프로그램에서 불러다 쓸 수 있는 라이브러리
    • 바이너리 크레이트(main.rs): 실행 가능한 바이너리(프로그램 진입점)
  • 하나의 패키지 안에 src/main.rs (바이너리)와 src/lib.rs (라이브러리)를 동시에 둘 수도 있음.

3) 워크스페이스(Workspace): 프로젝트를 여러 패키지로 구성하기

  • 대규모 프로젝트에서 여러 패키지를 하나의 루트로 묶는 방법.

  • workspace를 사용하면, 각 패키지별로 Cargo.toml을 두면서도, 공통된 Cargo.lock과 빌드 출력을 공유해 효율적으로 관리할 수 있습니다.

    toml
    # 루트의 Cargo.toml
    [workspace]
    members = [
        "core-lib",
        "cli-tool",
    ]
  • core-lib 디렉터리와 cli-tool 디렉터리가 각각 패키지가 되며, 이 둘을 전체 workspace로 묶어 빌드/의존성 관리를 함께 할 수 있습니다.


10. 고급 트레이트 개념

1) 연관 타입(Associated Types)

  • 트레이트 정의 시, 트레이트를 구현하는 타입이 ‘내부에서 사용할 타입’을 지정할 수 있도록 하는 기능입니다.

  • 대표적인 예시로, Iterator 트레이트type Item이 있습니다.

    rust
    pub trait Iterator {
        type Item;
        fn next(&mut self) -> Option<Self::Item>;
    }
  • 제네릭 파라미터(<T>) 대신 연관 타입을 사용하면, 트레이트 메서드 간에 연관된 타입 정보를 묶어서 표현하기가 수월합니다.

2) 디폴트 제네릭 파라미터, 트레이트 바운드의 고급 문법

  • Rust는 제네릭 파라미터에 디폴트 타입을 지정하거나, 특정 조건일 때만 타입을 요구하는 방식을 지원합니다.

    rust
    // 제네릭 파라미터에 디폴트 타입 부여 예시
    trait MyTrait<T = i32> {
        fn do_something(&self, x: T);
    }
  • 또한 where 절, 고급 트레이트 바운드 등을 통해 여러 타입 제약을 깔끔하게 명시할 수 있습니다.

    rust
    fn complex_function<T, U>(arg: T, data: U)
        where T: MyTrait + AnotherTrait,
              U: Debug + Clone,
    {
        // ...
    }
  • 이러한 문법을 활용하면 복잡한 타입 제약을 명확하고 유지보수 쉽게 표현할 수 있습니다.

3) 추상화 패턴: 객체 지향 스타일, 함수형 스타일 모두 지원

  • Rust는 전통적인 객체 지향 패턴(트레이트 오브젝트, Box<dyn Trait>)을 지원하는 동시에, 함수형 스타일로 고차 함수를 통한 추상화(Iterators, Closures)도 지원합니다.
  • 고급 트레이트 설계를 통해 다형성(polymorphism) 을 다양한 방식으로 구현할 수 있습니다.
    • 정적 디스패치(static dispatch) vs 동적 디스패치(dynamic dispatch)
    • 제네릭 + 트레이트 바운드 vs 트레이트 오브젝트

11. 고급 라이프타임과 참조

1) 서브타이핑(subtyping)과 역변성(invariance)

  • Rust 라이프타임 체계에서, 특정 라이프타임 'a'b하위타입(subtype)으로 간주될 수 있는 경우가 존재합니다. 예: 'static 라이프타임은 모든 라이프타임의 상위가 됩니다.
  • 역변성(invariance), 공변성(covariance), 반공변성(contravariance) 등의 개념이 있어, 복잡한 참조 구조에서 왜 컴파일러가 특정 라이프타임 에러를 내는지 이해할 때 필요합니다.
    • &'a mut T는 대부분 역변성
    • &'a T는 대부분 공변성

2) 고급 라이프타임 주석: 'static 라이프타임, 포인터 연산

  • 'static 라이프타임: 프로그램이 시작부터 끝날 때까지 유효한 참조를 의미합니다. 예: 문자열 리터럴("hello")은 'static 라이프타임을 가집니다.
  • 포인터 연산: &'static str, Box<T> 등이 'static 라이프타임으로 묶여 있을 경우, 메모리가 절대 해제되지 않는(또는 전역적으로 존재하는) 것으로 컴파일러가 이해합니다.
    • 주의: 'static 참조를 잘못 사용하면, 실제로는 해제될 메모리를 'static으로 참조하게 되어 Undefined Behavior가 발생할 수 있으므로, Unsafe Rust 영역에서 주의 깊게 다뤄야 합니다.

3) RAII & Drop 트레이트

  • RAII (Resource Acquisition Is Initialization): 객체가 생성될 때 리소스를 획득하고, 스코프를 벗어날 때 자동으로 해제되는 개념. C++의 RAII와 유사합니다.

  • Rust에서는 Drop 트레이트를 구현하여, 객체가 스코프를 벗어날 때(혹은 소유권이 이동되어 해제될 때) 커스텀 정리 로직(cleanup)을 수행할 수 있습니다.

    rust
    struct Resource;
     
    impl Drop for Resource {
        fn drop(&mut self) {
            println!("리소스 정리 로직 실행!");
        }
    }

12. 동시성(Concurrency) & 병렬 프로그래밍

1) 스레드: std::thread, spawn, join

  • Rust 표준 라이브러리 std::thread에서는 쉽고 안전하게 스레드를 생성하고 관리할 수 있습니다.

    rust
    use std::thread;
    use std::time::Duration;
     
    fn main() {
        let handle = thread::spawn(|| {
            for i in 1..5 {
                println!("새 스레드: {}", i);
                thread::sleep(Duration::from_millis(500));
            }
        });
     
        for i in 1..5 {
            println!("메인 스레드: {}", i);
            thread::sleep(Duration::from_millis(500));
        }
     
        // 새 스레드가 끝날 때까지 대기
        handle.join().unwrap();
    }

2) 채널(Channels): std::sync::mpsc, 송신자/수신자

  • 채널은 **‘송신자(Sender)’**가 여러 개일 수 있고, **‘수신자(Receiver)’**는 하나인(MPSC) 형태로 메시지를 주고받을 수 있는 구조입니다.

    rust
    use std::sync::mpsc;
    use std::thread;
     
    fn main() {
        let (tx, rx) = mpsc::channel();
     
        thread::spawn(move || {
            let val = String::from("안녕하세요");
            tx.send(val).unwrap();
        });
     
        let received = rx.recv().unwrap();
        println!("수신: {}", received);
    }
  • 채널을 통해 스레드 간 통신을 안전하게 처리할 수 있습니다.

3) 동기화: Mutex, RwLock, Arc

  • Mutex(Mutual Exclusion)

    • 여러 스레드가 동시에 접근해서는 안 되는 데이터를 보호하기 위한 동기화 primitive.
    • Arc<Mutex<T>> 형태로, 참조 카운팅(Arc)과 락(Mutex)을 함께 사용하여 데이터를 공유/보호하는 것이 일반적입니다.
    rust
    use std::sync::{Arc, Mutex};
    use std::thread;
     
    fn main() {
        let counter = Arc::new(Mutex::new(0));
     
        let mut handles = vec![];
        for _ in 0..10 {
            let counter_clone = Arc::clone(&counter);
            let handle = thread::spawn(move || {
                let mut num = counter_clone.lock().unwrap();
                *num += 1;
            });
            handles.push(handle);
        }
     
        for handle in handles {
            handle.join().unwrap();
        }
     
        println!("결과: {}", *counter.lock().unwrap());
    }
  • RwLock: 여러 스레드가 동시에 읽기는 허용하지만, 쓰기는 오직 하나의 스레드만 가능하도록 하는 락

  • Arc(Atomic Reference Counting): 멀티스레드 환경에서 안전하게 참조 카운팅을 수행

4) 비동기 프로그래밍: async/await, Future 트레이트, Tokio, async-std

  • Rust는 async/await 문법을 통해 비동기 프로그래밍을 지원합니다.

    • async fn, await 키워드를 사용하여, Future 트레이트를 반환하고, 비동기 처리를 간단히 표현할 수 있습니다.
    rust
    async fn do_work() {
        println!("비동기 작업 중...");
    }
     
    #[tokio::main]
    async fn main() {
        do_work().await;
    }
  • Tokio, async-std 등 런타임(Runtime) 라이브러리를 사용해 이벤트 루프(reactor) 기반의 동시성을 구현합니다.

  • HTTP 서버, 네트워킹 등 I/O 병행 처리 상황에서 높은 성능과 안전성을 동시에 제공합니다.

13. Unsafe Rust

1) unsafe 키워드: 안전하지 않은 블록에서 허용되는 작업

  • Rust의 가장 큰 특징은 컴파일러가 메모리 안전성을 강제하지만, unsafe 블록 안에서는 제한적으로 이 규칙들을 우회할 수 있게 합니다.
  • unsafe 블록에서 허용되는 작업:
    1. 원시 포인터(*const T, *mut T) 조작
    2. 안전하지 않은 함수나 메서드 호출(FFI 등)
    3. 가변 정적 변수 접근, 정적 변수 초기화
    4. 트레이트 메서드 구현에서 컴파일러 검사를 우회
  • 반드시 필요한 곳에만 최소화하여 사용해야 하며, 사용 시 Undefined Behavior(UB) 발생 가능성을 사전에 차단할 수 있도록 주의해야 합니다.

2) Raw 포인터: *const T, *mut T

  • Raw 포인터는 Rust 컴파일러의 빌림 검사나 소유권 검사가 적용되지 않습니다.
  • C/C++ 코드와 상호작용(FFI)하거나, 특별히 저수준 메모리 접근이 필요할 때 사용합니다.

3) FFI(외부 함수 인터페이스): C 라이브러리 호출을 위한 extern "C"

  • Rust에서 C로 작성된 라이브러리를 직접 호출하거나, 반대로 Rust를 C에서 호출하기 위해서는 FFI를 사용합니다.

    rust
    #[link(name = "mylib")]
    extern "C" {
        fn c_function(x: i32) -> i32;
    }
     
    fn main() {
        unsafe {
            let result = c_function(10);
            println!("C 함수 호출 결과: {}", result);
        }
    }
  • ABI(Application Binary Interface)에 맞춰 함수 시그니처를 일치시켜야 하며, unsafe 블록에서 호출합니다.

4) 메모리 안전성 주의사항: Undefined Behavior(UB)

  • Rust 컴파일러는 unsafe 블록 내부에 대한 안전성 보장을 해주지 않습니다.
  • 원시 포인터를 잘못 사용하거나, 라이프타임을 무시하고 해제된 메모리에 접근하면 UB가 발생할 수 있습니다.
  • 따라서 unsafe 코드를 작성할 때는 검증, 리뷰, 테스트 등을 매우 철저히 해야 합니다.

14. 메모리 최적화 & 고성능 기법

1) Zero-cost Abstractions: 러스트의 철학과 최적화 관점

  • Rust는 고수준 문법(제네릭, 트레이트, async 등)을 제공하면서도, 실행 시 오버헤드가 거의 없는(Zero-cost) 코드를 생성하도록 설계되어 있습니다.
  • 즉, 추상화된 코드가 컴파일러에 의해 강력하게 최적화되어, C/C++ 수준의 성능을 낼 수 있는 것이 Rust의 핵심 경쟁력입니다.

2) Inlining, SIMD: 컴파일러 최적화 옵션, 벡터화

  • Rust 컴파일러(rustc)는 LLVM 기반이므로, C/C++와 유사한 각종 최적화(inlining, loop unrolling, auto-vectorization 등)를 수행합니다.
  • SIMD(Single Instruction Multiple Data)를 사용하여 CPU 벡터 명령어로 병렬 연산을 할 수 있습니다.
    • Rust 표준 라이브러리와 별도 크레이트를 이용해 SIMD를 활용할 수 있습니다.

3) 프로파일링: perf, cargo profiler 등을 이용해 병목 분석

  • 고성능 코드를 작성하기 위해서는 병목 지점(bottleneck)을 찾아내는 것이 중요합니다.
  • 리눅스에서 perf, macOS에서 Instruments, Windows에서 Visual Studio 프로파일러 등을 활용하거나, Rust 생태계의 cargo profiler 등을 통해 성능 측정 및 최적화를 진행할 수 있습니다.

4) Arena Allocation, Custom Allocators: 특수 상황에서의 메모리 관리

  • 일반적인 경우 Rust의 기본 Allocator 사용만으로도 충분히 효율적입니다.
  • 특정 상황(예: 게임 엔진, 실시간 시스템)에서는 Arena AllocatorCustom Allocator 기법을 사용하여 메모리 할당/해제 비용을 줄일 수 있습니다.
    • 예: bumpalo crate (bump allocator)
    • Rust 1.28+부터는 global_allocator 속성을 통해 사용자 정의 할당기를 전역 설정 가능

15. 테스트 & CI/CD

1) 테스트: 단위 테스트, 통합 테스트, #[test] 애트리뷰트

  • 단위 테스트(Unit Test)

    • 보통 하나의 함수나 모듈이 원하는 대로 동작하는지 검증하기 위해 작성됩니다.
    • #[test] 애트리뷰트를 달아놓은 함수는 cargo test 실행 시 테스트로 인식됩니다.
    rust
    #[cfg(test)]
    mod tests {
        use super::*;
     
        #[test]
        fn it_works() {
            assert_eq!(2 + 2, 4);
        }
    }
  • 통합 테스트(Integration Test)

    • tests 디렉터리 안에 .rs 파일을 두고, 실제 라이브러리나 바이너리 API를 사용해 전체 흐름을 검증합니다.
    • 프로젝트 루트에서 cargo test 시 자동으로 실행됩니다.
  • 테스트 실행

    • cargo test: 모든 테스트를 실행
    • cargo test -- --nocapture: 테스트에서 출력되는 println! 메시지를 확인할 수 있음
    • cargo test test_name: 특정 테스트만 실행

2) 테스트 더블(Mock) & TDD: 의존성 분리, 트레이트 기반의 Mock

  • Rust에서 Mock 객체를 만들 때는 트레이트 기반으로 추상화하는 방법이 많이 쓰입니다.
    • 예: DB나 HTTP 클라이언트에 접근하는 로직을 트레이트로 추상화하고, 실제 구현 대신 테스트용 Mock 구조체를 주입해 테스트할 수 있습니다.
  • TDD(Test-Driven Development): 테스트를 먼저 작성하고, 이를 통과하기 위한 최소한의 구현을 하며 개발을 진행하는 방법.
    • Rust에서도 TDD를 활용해 견고한 코드를 작성할 수 있습니다.

3) CI/CD 파이프라인: GitHub Actions, GitLab CI, Travis 등과 연동

  • CI(Continuous Integration): 코드를 커밋할 때마다 자동으로 빌드, 테스트를 수행하고, 결과를 공유

    • GitHub Actions를 예로 들면, .github/workflows/*.yml 파일을 생성해 cargo build, cargo test 등을 자동화할 수 있습니다.
  • CD(Continuous Deployment): 테스트가 통과된 코드를 스테이징 혹은 프로덕션 환경에 자동 배포

    • Heroku, AWS, Netlify, Vercel 등과 연계 가능
  • 예시(GitHub Actions):

    yaml
    name: Rust CI
     
    on: [push, pull_request]
     
    jobs:
      build_and_test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
          - name: Install Rust
            uses: dtolnay/rust-toolchain@stable
          - name: Build
            run: cargo build --verbose
          - name: Test
            run: cargo test --verbose

16. 코드 스타일 & 리팩토링

1) 러스트 표준 코드 스타일: rustfmt, clippy

  • rustfmt: 공식 코드 포매터. Rust 코드를 일관된 스타일로 자동 정렬해 줍니다.
    • cargo fmt 명령어로 손쉽게 적용 가능
    • .rustfmt.toml 파일을 통해 세부 옵션을 설정할 수도 있습니다.
  • clippy: 러스트용 린트(lint) 툴. 잠재적 오류나 권장되는 코드 스타일, 성능 개선 사항을 알려줍니다.
    • cargo clippy로 사용, 빠른 피드백을 통해 코드 품질 향상

2) 리팩토링: 함수 추출, 중복 제거, 트레이트 추상화

  • 함수 추출: 중복되는 로직이나 너무 복잡한 함수를 적절히 쪼개는 작업
  • 트레이트 추상화: 비슷한 행동을 하는 여러 타입에 공통 트레이트를 정의하고, 코드 중복을 제거
  • **가시성(pub, pub(crate))**과 모듈 구조를 적절히 재설계해, 프로젝트 구조를 간결히 유지

3) API 디자인: 공개 API와 내부 구현 분리, 문서화(///, cargo doc)

  • 라이브러리나 바이너리를 배포할 때, 공개 API와 내부 구현 디테일을 분리해 유지보수성을 높일 수 있습니다.

  • Rust에서는 /// 주석을 사용해 문서 주석을 작성하면, cargo doc으로 문서 웹페이지를 자동 생성할 수 있습니다.

    rust
    /// Adds two numbers together.
    ///
    /// # Examples
    ///
    /// ```
    /// assert_eq!(add(2, 3), 5);
    /// ```
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

17. 네트워킹 & 웹 개발

1) HTTP 클라이언트/서버: reqwest, hyper, actix-web, rocket

  • reqwest:

    • 손쉽게 HTTP 요청을 보낼 수 있는 Rust 라이브러리. GET/POST 등 다양한 메서드를 간단히 호출 가능
    rust
    let body = reqwest::blocking::get("https://www.rust-lang.org")?
        .text()?;
    println!("Body = {}", body);
  • hyper:

    • 저수준 HTTP 라이브러리. 고성능, 비동기 HTTP 서버/클라이언트 기능을 제공
  • actix-web, rocket:

    • Rust에서 웹 서버를 빠르게 만들기 위한 웹 프레임워크. 라우팅, 미들웨어, 세션, 인증 등 편의 기능을 제공

    • 예: actix-web에서 간단한 서버

      rust
      use actix_web::{get, web, App, HttpServer, Responder};
       
      #[get("/")]
      async fn index() -> impl Responder {
          "Hello from Actix!"
      }
       
      #[actix_web::main]
      async fn main() -> std::io::Result<()> {
          HttpServer::new(|| App::new().service(index))
              .bind(("127.0.0.1", 8080))?
              .run()
              .await
      }

2) 직렬화/역직렬화: serde, JSON, TOML, YAML

  • serde 라이브러리:

    • Rust 데이터 구조를 JSON/TOML/YAML/MessagePack 등 다양한 포맷으로 손쉽게 직렬화/역직렬화할 수 있게 해줍니다.
    • #[derive(Serialize, Deserialize)] 매크로를 활용
    rust
    use serde::{Serialize, Deserialize};
     
    #[derive(Serialize, Deserialize)]
    struct Config {
        name: String,
        version: u32,
    }
     
    fn main() {
        let json_str = r#"{ "name": "MyApp", "version": 1 }"#;
        let cfg: Config = serde_json::from_str(json_str).unwrap();
        println!("name: {}, version: {}", cfg.name, cfg.version);
    }

3) REST API 설계: 라우팅, 미들웨어, 인증(토큰 등)

  • 라우팅: HTTP 경로(path)와 메서드(GET, POST 등)를 엔드포인트 함수에 매핑
  • 미들웨어: 인증, 로깅, 에러 핸들링 등 공통 처리를 위한 레이어
  • 인증: JWT(JSON Web Token)나 OAuth 등을 사용해 토큰 기반 인증을 구현
    • Rust 웹 프레임워크는 인증을 위한 미들웨어/라이브러리를 제공하거나, 직접 구현할 수 있습니다.

18. 시스템 프로그래밍 & 임베디드

1) 시스템 인터페이스: 파일, 소켓, OS 레벨 API

  • Rust 표준 라이브러리 std::fs, std::net 등을 통해 로우 레벨 시스템 자원(파일, 소켓)에 접근
  • OS 레벨 API(예: Linux syscalls, Windows Win32 API)와 상호작용할 때는 FFI(extern "C")와 unsafe 블록을 사용하기도 합니다.

2) 임베디드 러스트: no_std, 임베디드용 HAL, 마이크로컨트롤러 프로그래밍

  • 임베디드 환경에서는 일반적인 표준 라이브러리(std)가 아니라, no_std 모드로 빌드해야 할 수 있습니다.
    • 이 경우, 힙 할당이나 OS 기능 없이도 동작 가능하도록 최소화된 환경이 제공
  • 임베디드 Rust HAL(Hardware Abstraction Layer)을 사용하면, 특정 마이크로컨트롤러(예: ARM Cortex-M 계열)에서 핀 제어, 인터럽트 처리 등을 Rust로 구현할 수 있습니다.

3) WASM(웹어셈블리): wasm-bindgen, wasm-pack, 브라우저/서버 사이드 WASM

  • Rust 코드를 웹어셈블리(WASM) 로 컴파일해 브라우저 혹은 서버 사이드 환경에서 실행할 수 있습니다.
  • wasm-bindgen: Rust와 JavaScript 간 상호작용을 돕는 툴
  • wasm-pack: Rust 코드를 빌드해 npm 패키지로 배포하기 쉽게 하는 툴
  • 실시간 성능 요구(예: 게임, 시뮬레이션, 이미지 처리)나, 안전한 샌드박스 환경이 필요한 곳에서 WASM이 유용합니다.

19. 프로젝트 진행

1) 개인/팀 프로젝트 기획: 실제로 동작하는 애플리케이션 설계

  • 지금까지 학습한 Rust 지식(소유권, 트레이트, 동시성, 웹 프레임워크 등)을 종합해, 실제 서비스를 만들어보는 것이 중요합니다.
  • 예) CLI 툴, 웹 서비스, 임베디드 프로젝트, 블록체인 노드, 게임 엔진 등
  • 기획 단계에서 프로젝트 규모, 사용될 라이브러리, 데이터 모델, API 스펙, 테스트 시나리오 등을 구체화

2) 오픈소스 기여: 러스트 오픈소스 프로젝트에 이슈/PR

  • Rust 생태계는 오픈소스로 활발하게 이루어져 있으므로, 관심 있는 라이브러리나 프로젝트에 기여하는 방식으로 실무 감각을 기를 수 있습니다.
  • 작은 문서 수정부터 시작해, 점차 이슈 해결, 신규 기능 PR 등으로 확장해보세요.

3) 코드 리뷰 & 베스트 프랙티스: 팀 내 코드 품질 관리

  • 팀 프로젝트 시 Rust 코드 리뷰를 통해, 소유권과 라이프타임, 에러 처리, 코딩 스타일 등을 점검하고 공유하면 좋습니다.
  • Rust Best Practices
    • 에러 처리는 가능한 명시적으로
    • 안전성(unsafe 최소화)
    • 성능과 가독성의 균형
    • 문서화와 테스트는 필수
    • clippy 경고를 최대한 해결하여 코드 품질 유지

마무리

4단계: 실무 적용 & 프로젝트 에서는 Rust를 “어떻게 실제 제품/서비스/시스템에 활용할 것인가?”에 대한 구체적인 내용을 다룹니다.

  1. 테스트 & CI/CD 로 개발 프로세스 자동화와 품질 보증을 확립
  2. 코드 스타일 & 리팩토링 으로 유지보수성 극대화
  3. 네트워킹 & 웹 개발, 시스템 프로그래밍 & 임베디드 영역을 통해 Rust가 강력한 퍼포먼스를 발휘하는 다양한 분야 탐색
  4. 프로젝트 진행(개인/팀/오픈소스)으로 실무 역량 강화

Rust는 안정성과 성능을 동시에 제공하는 언어로, 서버 사이드, 시스템, 임베디드, 웹어셈블리 등 다양한 영역에서 사용되고 있습니다. 이제 학습을 실전 프로젝트와 연결해보며, 더 큰 규모의 코드를 다뤄보시길 바랍니다. 그 과정에서 발생하는 문제를 해결하고 새로운 라이브러리를 시도해보면서 Rust 생태계에 익숙해지면, 한층 더 높은 수준의 Rust 개발자가 될 수 있습니다.

Happy coding with Rust!