JS, TS랑 비교하며 Rust 공부하기 (4)

크레이트, 패키지, 모듈

크레이트

크레이트는 러스트의 컴파일 단위로, 실행파일 또는 라이브러리 형태로 생성된다. JS의 ESM 모듈이나 npm 패키지와 비슷한 개념이다.
바이너리 크레이트는 커맨드 라인 프로그램이나 서버처럼 실행 가능한 main 함수를 가지고 있는 실행파일이다.
라이브러리 크레이트는 main 함수를 가지고 있지 않고 실행파일 형태로 컴파일되지 않는다. 그 대신, 여러 프로젝트에서 공용될 의도로 만들어진 기능들이 정의되어 있다.

패키지

패키지는 Cargo.toml 파일로 정의된 하나의 프로젝트 단위이며 여러 개의 크레이트를 포함할 수 있다. package.json으로 관리되는 npm 패키지와 비슷하다.

모듈

모듈은 하나의 .rs 파일 또는 동일한 이름의 디렉터리로 구성된 소스 코드 조직 단위이다. JS로 보면 ESM 모듈 시스템과 비슷하다.
mod 키워드와 모듈 이름을 지정하여 모듈을 선언할 수 있으며 컴파일러는 현재 또는 하위 디렉터리에서 선언한 모듈 이름과 같은 이름의 파일이 있는지 확인한다.

pub 키워드

모듈 내 아이템은 기본적으로 비공개이며, pub 키워드를 사용하면 해당 모듈의 부모 모듈에 접근 권한이 부여된다.
예를 들어, 아래와 같이 구성된 패키지에서 parent.rspub mod child; 로 모듈을 선언한 후 main.rs 에서 mod parent;로 선언하면 child.rs 내의 공개된 아이템을 사용할 수 있게 된다.

├── Cargo.lock
├── Cargo.toml
└── src
    ├── parent
    │   └── child.rs
    ├── parent.rs
    └── main.rs
// child.rs

pub fn greet() {
    println!("Hello from child")
}
// parent.rs

pub mod child;
// main.rs

mod parent;

fn main() {
    parent::child::greet();
}

구조체 정의에 pub를 쓰면 구조체는 공개되지만, 구조체의 필드는 비공개로 유지된다. 공개 여부는 각 필드마다 정할 수 있다.
구조체가 비공개 필드를 갖고 있다면, 해당 구조체의 인스턴스를 생성할 공개 연관 함수를 반드시 제공해야 한다.

mod parent {
    pub struct Parent {
        pub name: String,
        age: u16,
    }

    impl Parent {
        pub fn new(name: String, age: u16) -> Parent {
            Parent { name, age }
        }

        pub fn get_age(&self) -> u16 {
            self.age
        }
    }
}

use parent::Parent;

fn main() {
    let p = Parent::new(String::from("철수"), 30);

    println!("name: {}", p.name);
    println!("age: {}", p.get_age());
}

열거형은 공개로 지정할 경우 모든 배리언트가 공개된다. 열거형을 공개하는 방법은 enum 키워드 앞에 pub 키워드만 작성하면 된다.

mod parent {
    pub enum Member {
        Father,
        Mother,
    }
}

use parent::Member;

fn main() {
    let p1 = Member::Father;
    let p2 = Member::Mother;
}

crate, super 키워드

부모 모듈 내 아이템은 자식 모듈 내 비공개 아이템을 사용할 수 없지만, 자식 모듈 내 아이템은 부모 모듈 내 아이템을 사용할 수 있다.
crate 키워드로 크레이트 루트로부터 시작되는 절대 경로를 만들 수 있다.
super 키워드로 부모 모듈을 기준으로 한 상대 경로를 만들 수 있다.

mod parent {
    fn greet_from_parent() {
        println!("Hello from parent");
    }

    pub mod child {
        pub fn greet_from_child() {
            crate::greet_from_main();
            super::greet_from_parent();
            println!("Hello from child");
        }
    }
}

fn greet_from_main() {
    println!("Hello from main");
}

fn main() {
    parent::child::greet_from_child();
}

use 키워드

use 키워드를 사용하여 단축경로를 만들 수 있으며 스코프 안쪽 어디서라도 짧은 이름을 사용할 수 있다.
use 키워드로 가져온 경우도 다른 경로와 마찬가지로 비공개 규칙이 적용된다.
일반적으로 함수는 호출할 때 모듈 경로를 함께 사용하는 경우가 많아, 함수가 포함된 모듈까지만 use로 가져오는 경우가 많다. 반면, 구조체나 열거형은 아이템 이름까지 명시하여 스코프에 바로 두는 경우가 많다.

mod parent {
		pub struct Parent {
				pub age: u16
		}

    pub mod child {
        pub fn greet_from_child() {
            println!("Hello from child");
        }
    }
}

use parent::Parent; // 구조체를 직접 가져옴
use parent::child; // 함수는 모듈까지만 가져오고 호출 시 경로 사용

fn main() {
		let p = Parent {
				age: 40,
		};

    child::greet_from_child();
}

as 키워드

as 키워드를 통해 새로운 이름을 지정할 수 있다.

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --생략--
}

fn function2() -> IoResult<()> {
    // --생략--
}

다시 내보내기

pub과 use를 함께 사용하면, 외부 모듈이 해당 항목을 간결하게 참조할 수 있다.

mod parent {
    pub mod child {
        pub fn greet_from_child() {
            println!("Hello from child");
        }
    }
}

pub use crate::parent::child; // 외부에서 패키지명::child로 접근 가능

외부 패키지

패키지의 Cargo.toml 파일에 의존성을 추가하고, use 키워드를 사용해 스코프로 가져오면 된다.

[package]
name = "hello"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8.5"

중첩 경로

중첩 경로를 사용하여 동일한 아이템을 한 줄로 가져올 수 있다. 경로의 공통된 부분을 작성하고 콜론 두 개를 붙인 다음, 중괄호 내에 경로가 다른 부분을 나열한다.

// use std::cmp::Ordering;
// use std::io;

use std::{cmp::Ordering, io};
// use std::io;
// use std::io::Write;

use std::io::{self, Write};

글롭 연산자

경로에 글롭 연산자 *를 붙이면 경로 안에 정의된 모든 공개 아이템을 가져올 수 있다.

use std::collections::*;