모든 프로그램이 그렇듯이 프로그램이 커지면 하나의 파일로 관리하기가 불가능해집니다.
따라서 파일을 분리하고 서로 연결하는 과정이 필요합니다.
cargo로 새 프로젝트를 구성합니다.
cargo new greeting
cd greeting
예로 인사 프로그램을 만들어 봅시다.
// main.rs
fn hello_ko() {
println!("안녕하세요!");
}
fn hello_en() {
println!("Hello!");
}
fn main() {
hello_ko();
hello_en();
}
1. mod는 코드를 구역으로 나눕니다.
// main.rs
mod korean {
pub fn hello_ko() {
println!("안녕하세요!");
}
}
mod english {
pub fn hello_en() {
println!("Hello!");
}
}
fn main() {
korean::hello_ko();
english::hello_en();
}
나중에 기능을 추가할 때를 대비해 언어별로 관리하는게 편리할 것입니다. 따라서 함수를 korean과 english 모듈로 나누겠습니다.
mod 모듈이름 {...}을 사용하면 C++의 namespace처럼 코드의 구역을 나눌 수 있습니다. 호출도 C++와 비슷하게 모듈이름::함수이름으로 호출합니다. 이때 모듈 내부에 선언한 것은 pub를 선언해야 외부에서 접근 가능합니다.
2. rust는 mod를 기준으로 파일을 나누고, 찾습니다.
// main.rs
mod korean;
mod english;
fn main() {
korean::hello();
english::hello();
}
본문 없이 mod 모듈이름;을 작성하면 rust는 해당 모듈을 외부의 파일에서 찾고, 그 내용을 가져옵니다.
이때 rust는 mod가 작성된 파일과 같은 위치에 모듈이름.rs 혹은 모듈이름 폴더 내부에 mod.rs 둘 중 하나를 찾습니다.
(만약 한 모듈에 두 가지 방식이 동시에 있다면 작동하지 않습니다.)
// korean/mod.rs
pub fn hello_ko() {
println!("안녕하세요!");
}
// english.rs
pub fn hello_en() {
println!("Hello!");
}
// main.rs
mod korean; // <- korean/mod.rs의 내용을 여기 넣는다.
mod english;
fn main() {
korean::hello_ko();
english::hello_en();
}
korean은 폴더 방식으로, english는 파일 방식으로 분할했습니다. 만약 모듈이 단순하다면 파일 방식이, 모듈 내에서 다시 모듈을 나눌 필요가 있다면 폴더 방식이 편리합니다. 이렇게 나눈 코드는 위에서 하나의 파일에 쓴 코드와 결과적으로 동일합니다.
3. use는 모듈 계층을 축약합니다.
// main.rs
mod korean;
mod english;
use korean::hello_ko;
use english::hello_en;
fn main() {
hello_ko(); // korean::hello_ko와 동일
hello_en();
}
use가 없다면 특정 함수를 호출하기 위해 std::io::stdin() 처럼 구조를 전부 작성해야 합니다. 이때 use를 사용하면 해당 모듈, 함수 등을 축약해 작성할 수 있습니다.
4. lib.rs와 main.rs는 별개입니다.
어떤 프로젝트는 main.rs와 lib.rs를 동시에 가지고 있습니다. main.rs는 실행 가능한 프로그램, lib.rs는 라이브러리를 만들 때 사용되고, 둘은 직접 연결되지 않습니다.
우리가 만든 greeting 프로그램을, 인사 함수를 가지는 greeting 라이브러리와 그걸 쓰는 greeting 프로그램으로 나눠보겠습니다. 프로젝트에 lib.rs를 추가하고 작성합니다.
// lib.rs
pub mod korean; // <- korean/mod.rs의 내용을 여기 추가
pub mod english;
// main.rs
mod korean; // <- korean/mod.rs의 내용을 여기 추가
mod english;
use korean::hello_ko;
use english::hello_en;
fn main() {
hello_ko(); // 라이브러리의 함수가 아님
hello_en();
}
lib.rs에 함수가 들어가고, 프로그램은 작동하지만, 올바른 방법이 아닙니다.
mod는 해당 파일의 내용을 해당 위치에 붙이는 식으로 작동합니다. 그렇기에 lib.rs의 hello_ko()와 main.rs의 hello_ko()는 같은 코드가 두 번 들어간, 서로 다른 함수가 됩니다. main.rs가 라이브러리의 코드를 사용하려면 모듈 이름을 통해 함수를 호출해야 합니다.
// main.rs
fn main() {
greeting::korean::hello_ko(); // greeting 라이브러리의 함수를 사용
greeting::english::hello_en();
//crate::english::hello_en(); // 작동x
}
greeting은 처음에 cargo로 만든 프로젝트 이름입니다. std를 사용할 때처럼 모듈 이름을 명시해서 내부의 함수를 사용할 수 있습니다.
main.rs에서 라이브러리를 호출할 때 자신의 최상위를 지칭하는 crate를 사용할 수 없습니다. main.rs는 lib.rs와 별개이기 때문에 greeting 라이브러리에 속하지 않습니다. main.rs에서 crate는 main.rs 자신을 뜻합니다.
잘 설명된 글:
Rust modules confusion when there is main.rs and lib.rs - Stack Overflow
번외. prelude
원하는 함수를 호출할 때마다 모든 계층 구조를 외워야 한다면 불편할 겁니다. 그렇기에 rust는 기본 라이브러리에서 자주 사용하는 기능에 바로 접근할 수 있게 prelude라는 부분을 사전에 정의합니다. 예를 들어 Option<T>는 표준 라이브러리의 std::option::Option에 있지만, 바로 사용할 수 있습니다.
동일한 맥락으로, 라이브러리 작성자도 자주 사용하는 기능에 빠르게 접근할 수 있도록 prelude를 만들기도 합니다.
// lib.rs
pub mod korean;
pub mod english;
pub mod prelude {
pub use super::korean::hello_ko;
pub use super::english::hello_en;
}
// main.rs
use greeting::prelude::*;
fn main() {
hello_ko();
hello_en();
}
라이브러리에 prelude란 모듈을 만들고 자주 사용되는 모듈에 use문을 추가합니다.
사용자는 prelude::*를 use 해 내부의 use문을 따라 쓴 효과를 얻을 수 있습니다.
prelude는 사용자가 내부의 복잡한 구조를 알 필요 없게 하고, 라이브러리 작성자가 원하는 대로 내부를 재구성해 노출할 수 있게 합니다.