Rust

Rust 모듈 사용법 - 크레이트 생성부터 모듈 공개까지 완벽 가이드

드리프트2 2024. 5. 17. 19:48

샘플 프로젝트 생성

Rust 프로젝트를 시작하려면 cargo를 사용하여 새로운 프로젝트를 생성합니다.

$ cargo new rust_modules

cargo new 명령은 기본적으로 바이너리 프로젝트를 생성합니다. Cargo.toml 파일과 함께 src 디렉터리 내에 main.rs 파일이 생성됩니다.

모듈과 크레이트

모듈에 대해 설명하기 전에 크레이트에 대해 먼저 알아보겠습니다.

크레이트는 Rust 프로그램의 가장 큰 단위입니다. 크게 두 가지로 나뉩니다.

  1. 라이브러리 크레이트 (lib.rs): 재사용 가능한 코드를 포함하는 라이브러리입니다.
  2. 바이너리 크레이트 (main.rs): 실행 가능한 프로그램을 만듭니다.

각 크레이트의 루트 파일은 lib.rs 또는 main.rs이며, 이 파일에서 다른 모든 모듈을 참조합니다. 프로젝트 내에 파일이 존재하더라도 루트 파일(즉, lib.rs 또는 main.rs)에서 추적하지 않으면 컴파일되지 않습니다.

모듈 생성 방법

모듈을 생성하는 방법은 크게 두 가지가 있습니다.

  1. 한 파일 내에 인라인으로 생성
  2. 별도의 파일로 생성

1. 인라인 모듈

인라인 모듈은 한 파일 내에서 모듈을 정의하는 방법입니다. 보통 설명을 위해 사용하거나 접근성을 제어하기 위해 사용됩니다.

lib.rs 파일에서 인라인 모듈을 생성하는 예제는 다음과 같습니다.

// src/lib.rs
mod inline_module {
    pub fn greet() {
        println!("Hello from inline module!");
    }
}

모듈에 정의된 greet 함수는 inline_module::greet으로 접근할 수 있습니다.

// src/main.rs
fn main() {
    rust_modules::inline_module::greet();
}

2. 파일로 모듈 생성

별도의 파일로 모듈을 생성하려면 먼저 파일을 만들어야 합니다.

$ tree src
src
├── lib.rs
└── new_module.rs

그리고 lib.rs에서 mod 키워드로 참조하면 사용 가능합니다.

// src/lib.rs
mod new_module;

new_module.rs 파일에서 greet 함수를 정의합니다.

// src/new_module.rs
pub fn greet() {
    println!("Hello from new_module!");
}

이제 main.rs에서 이를 호출할 수 있습니다.

// src/main.rs
use rust_modules::new_module;

fn main() {
    new_module::greet();
}

디렉터리로 서브 모듈 생성

모듈을 디렉터리 구조로 중첩해서 만들 수 있습니다. 예를 들어 new_module 모듈 안에 서브 모듈을 만들려면 다음과 같이 디렉터리를 생성합니다.

$ tree src
src
├── lib.rs
├── new_module
│   └── new_sub_module.rs
└── new_module.rs

그리고 new_module.rs에서 서브 모듈을 참조합니다.

// src/new_module.rs
mod new_sub_module;

new_sub_module.rs 파일에서 greet 함수를 정의합니다.

// src/new_module/new_sub_module.rs
pub fn greet() {
    println!("Hello from new_sub_module!");
}

이제 main.rs에서 이를 호출할 수 있습니다.

// src/main.rs
use rust_modules::new_module::new_sub_module;

fn main() {
    new_sub_module::greet();
}

2015 버전 모듈 구조의 유산

2015 버전에서는 모듈을 디렉터리로 만들 때 mod.rs 파일을 사용했습니다. 예를 들어 new_module.rs 대신 new_module/mod.rs를 사용할 수 있습니다.

$ tree src
src
├── lib.rs
└── new_module
    └── mod.rs

서브 모듈을 만드는 예는 다음과 같습니다.

$ tree src
src
├── lib.rs
└── new_module
    ├── mod.rs
    └── new_sub_module.rs

mod.rs에서 서브 모듈을 참조합니다.

// src/new_module/mod.rs
mod new_sub_module;

하지만 2018 에디션 이후로는 디렉터리 내부의 mod.rs 대신 디렉터리 자체를 모듈로 취급하는 방식을 권장합니다.

$ tree src
src
├── lib.rs
└── new_module
    └── new_module.rs

형제 모듈 간 접근 제어

형제 모듈 간의 가시성을 살펴보겠습니다. 우선 두 개의 모듈을 만듭니다. lib.rs에는 module_a만 선언합니다.

$ tree src
src
├── lib.rs
├── module_a.rs
└── module_b.rs

lib.rs의 내용은 다음과 같습니다.

// src/lib.rs
mod module_a;

그리고 module_a.rs에서 module_b를 서브 모듈로 선언해봅니다.

// src/module_a.rs
mod module_b;

이 상태에서 컴파일하면 다음과 같은 오류가 발생합니다.

$ cargo build
error[E0583]: file not found for module `module_b`
 --> src/module_a.rs:1:5
  |
1 | mod module_b;
  |     ^^^^^^^^
  |

가장 상위에 있는 lib.rs만 다른 모듈을 선언할 수 있으므로 module_a에서는 module_b를 선언할 수 없습니다.

모듈의 경로 이름

이번에는 lib.rs에서 module_amodule_b 모두를 선언하고 각 모듈에 함수를 추가해봅니다.

$ tree src
src
├── lib.rs
├── module_a.rs
└── module_b.rs

lib.rs의 내용은 다음과 같습니다.

// src/lib.rs
mod module_a;
mod module_b;

module_a.rs 파일에 함수를 추가합니다.

// src/module_a.rs
pub fn name() {
    println!("This is module_a");
}

이때 module_b에서 module_a의 함수를 사용할 때 경로 이름은 다음과 같습니다.

// src/module_b.rs
use crate::module_a::name; // 절대 경로
use super::module_a::name; // 상대 경로

서브 모듈과 접근 제어

서브 모듈과 접근 제어를 알아봅시다. 다음과 같이 디렉터리 구조를 만듭니다.

$ tree src
src
├── lib.rs
├── module_a
│   └── submodule.rs
├── module_a.rs
└── module_b.rs

lib.rs에서는 module_amodule_b 모두를 모듈로 선언합니다.

// src/lib.rs
mod module_a;
mod module_b;

module_a.rs에서는 서브 모듈을 선언합니다.

// src/module_a.rs
mod submodule;

이때 submodule에서 module_b를 참조하려면 다음과 같습니다.

// src/module_a/submodule.rs
use crate::module_b; // 절대 경로
use super::super::module_b; // 상대 경로

하지만 module_b에서 submodule을 참조하려고 하면 보이지 않습니다. 이는 module_asubmodule을 공개하지 않았기 때문입니다. module_asubmodule을 공개하려면 pub mod를 사용해야 합니다.

// src/module_a.rs
pub mod submodule;

이렇게 하면 module_b에서도 다음과 같이 submodule을 참조할 수 있습니다.

// src/module_b.rs
use crate::module_a::submodule;
use super::module_a::submodule;

모듈의 이름 변경

모듈의 이름을 변경하려면 use를 사용합니다.

// src/lib.rs
mod module_a;
use self::module_a as a;

이렇게 하면 이후부터 a라는 모듈이 있는 것처럼 사용할 수 있습니다.

// src/lib.rs
mod module_a;
pub use self::module_a as a;

이렇게 pub use를 사용하여 공개할 수도 있습니다.

크레이트 외부에서 모듈 사용

이제 main.rs를 추가하여 크레이트 외부에서 모듈을 사용하는 방법을 알아봅시다.

$ tree src
src
├── lib.rs
├── main.rs
├── module_a
│   └── submodule.rs
├── module_a.rs
└── module_b.rs

lib.rs의 내용은 다음과 같습니다.

// src/lib.rs
mod module_a;
mod module_b;

module_a.rs에서는 서브 모듈을 공개합니다.

// src/module_a.rs
pub mod submodule;

main.rs에서 module_b를 사용할 수 없습니다. lib.rs에서 module_bpub으로 공개하지 않았기 때문입니다.

// src/lib.rs
mod module_a;
pub mod module_b;

이렇게 pub으로 공개하면 main.rs에서 다음과 같이 모듈을 사용할 수 있습니다.

// src/main.rs
use rust_modules::module_b;

fn main() {
    module_b::name();
}

외부 크레이트 이름 변경

Cargo.toml에서 외부 크레이트 이름을 변경할 수 있습니다. 예를 들어 rand 크레이트를 random으로 이름을 변경하여 사용하려면 다음과 같이 설정합니다.

# Cargo.toml
[dependencies]
rand = "0.8"

main.rs에서 이렇게 사용합니다.

// src/main.rs
use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let n: u32 = rng.gen_range(0..10);
    println!("Random number: {}", n);
}

extern crate와 모듈 리네이밍

Rust 2018 에디션 이후로 extern crate는 필수가 아니지만, 여전히 모듈을 리네이밍할 때 사용할 수 있습니다.

// src/lib.rs
extern crate rand as random;

pub use self::random::Rng;

이렇게 하면 이후에 random이라는 이름으로 사용할 수 있습니다.

// src/main.rs
use rust_modules::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let n: u32 = rng.gen_range(0..10);
    println!("Random number: {}", n);
}

모듈을 공개하지 않고 서브 모듈만 공개하기

lib.rs에서 모듈을 공개하지 않고 서브 모듈만 공개하려면 pub use를 사용합니다.

// src/lib.rs
mod module_a;
pub mod module_b;

pub use self::module_a::submodule as module_c;

이렇게 하면 main.rs에서 다음과 같이 사용할 수 있습니다.

// src/main.rs
use rust_modules::module_c;

fn main() {
    module_c::greet();
}

크레이트 내에서만 공개하기

크레이트 내부에서는 모듈을 자유롭게 참조하고 싶지만 외부에는 공개하고 싶지 않은 경우가 있습니다. 그럴 때는 pub(crate)를 사용합니다.

// src/module_a.rs
pub(crate) fn greet() {
    println!("Hello from module_a");
}

이렇게 하면 같은 크레이트 내에서는 module_agreet 함수를 사용할 수 있지만, 외부에서는 접근이 불가능합니다.

lib.rsmain.rs

lib.rsmain.rs 모두 동일한 방식으로 모듈을 선언하고 사용할 수 있습니다. 하지만 일반적으로는 lib.rs를 통해 라이브러리로 구성하고 main.rs에서는 이를 사용하는 방식으로 작성하는 것이 좋습니다.

// src/lib.rs
pub mod module_a;

pub fn greet_from_lib() {
    println!("Hello from the library");
}
// src/main.rs
use rust_modules::greet_from_lib;
use rust_modules::module_a::greet;

fn main() {
    greet_from_lib();
    greet();
}

이렇게 lib.rs를 통해 라이브러리로 모듈을 구성하면 자연스럽게 API 설계가 가능해지고 코드 구조가 깔끔해집니다.

요약

  • lib.rsmain.rs가 가장 중요한 루트 파일입니다.
  • mod를 사용하여 모듈을 선언하고, pub mod를 사용하여 상위 모듈에도 공개합니다.
  • crate, self, super 등의 예약어를 통해 모듈 경로를 지정합니다.
  • pub use를 사용하여 모듈을 리네임하거나 공개 범위를 조절할 수 있습니다.

참고 문서

Rust 모듈 시스템에 대해 자세히 알고 싶다면 공식 문서를 참고하세요.