use 키워드로 경로를 스코프에 가져오기

함수를 호출할 때마다 경로를 일일이 작성하는 것은 번거롭고 반복적일 수 있다. 예제 7-7에서 add_to_waitlist 함수를 호출할 때마다 front_of_househosting을 함께 지정해야 했다. 다행히 이 과정을 단순화할 방법이 있다. use 키워드를 사용해 경로에 대한 단축키를 한 번 만들면, 스코프 내에서는 더 짧은 이름을 사용할 수 있다.

예제 7-11에서 crate::front_of_house::hosting 모듈을 eat_at_restaurant 함수의 스코프로 가져온다. 이제 eat_at_restaurant 함수 내에서 hosting::add_to_waitlist만 지정해도 add_to_waitlist 함수를 호출할 수 있다.

Filename: src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}
Listing 7-11: use로 모듈을 스코프에 가져오기

스코프에 use와 경로를 추가하는 것은 파일 시스템에서 심볼릭 링크를 만드는 것과 유사하다. 크레이트 루트에 use crate::front_of_house::hosting을 추가하면, hosting은 해당 스코프 내에서 유효한 이름이 된다. 마치 hosting 모듈이 크레이트 루트에 정의된 것처럼 작동한다. use로 가져온 경로도 다른 경로와 마찬가지로 프라이버시를 검사한다.

use는 해당 use가 발생한 특정 스코프에 대해서만 단축키를 생성한다는 점에 유의해야 한다. 예제 7-12에서 eat_at_restaurant 함수를 customer라는 새로운 자식 모듈로 이동시켰다. 이제 use 문과는 다른 스코프가 되었기 때문에 함수 본문이 컴파일되지 않는다.

Filename: src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

mod customer {
    pub fn eat_at_restaurant() {
        hosting::add_to_waitlist();
    }
}
Listing 7-12: use 문은 해당 스코프 내에서만 적용된다.

컴파일러 오류는 customer 모듈 내에서 단축키가 더 이상 적용되지 않음을 보여준다:

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
  --> src/lib.rs:11:9
   |
11 |         hosting::add_to_waitlist();
   |         ^^^^^^^ use of undeclared crate or module `hosting`
   |
help: consider importing this module through its public re-export
   |
10 +     use crate::hosting;
   |

warning: unused import: `crate::front_of_house::hosting`
 --> src/lib.rs:7:5
  |
7 | use crate::front_of_house::hosting;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted

use가 더 이상 스코프 내에서 사용되지 않는다는 경고도 함께 나타난다. 이 문제를 해결하려면 usecustomer 모듈 내로 이동시키거나, 자식 customer 모듈 내에서 super::hosting으로 부모 모듈의 단축키를 참조해야 한다.

관용적인 use 경로 생성하기

리스트 7-11에서 use crate::front_of_house::hosting을 지정한 후 eat_at_restaurant에서 hosting::add_to_waitlist를 호출한 이유가 궁금할 수 있다. 리스트 7-13처럼 add_to_waitlist 함수까지 전체 경로를 지정해도 동일한 결과를 얻을 수 있지만, 리스트 7-11이 관용적인 방식이다.

Filename: src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
}
Listing 7-13: useadd_to_waitlist 함수를 스코프로 가져오기 (비관용적)

리스트 7-11과 리스트 7-13은 동일한 작업을 수행하지만, 리스트 7-11이 use로 함수를 스코프로 가져오는 관용적인 방식이다. 함수의 상위 모듈을 use로 가져오면 함수를 호출할 때 상위 모듈을 지정해야 한다. 이렇게 하면 함수가 로컬에서 정의되지 않았음을 명확히 하면서도 전체 경로의 반복을 최소화할 수 있다. 리스트 7-13의 코드는 add_to_waitlist가 어디서 정의되었는지 불분명하다.

반면, use로 구조체, 열거형 등을 가져올 때는 전체 경로를 지정하는 것이 관용적이다. 리스트 7-14는 바이너리 크레이트에서 표준 라이브러리의 HashMap 구조체를 스코프로 가져오는 관용적인 방식을 보여준다.

Filename: src/main.rs
use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}
Listing 7-14: 관용적인 방식으로 HashMap을 스코프로 가져오기

이 관례에는 특별한 이유가 없다. 단지 이렇게 작성하는 것이 일반적이 되었고, 사람들이 이 방식으로 Rust 코드를 읽고 쓰는 데 익숙해졌을 뿐이다.

이 관례의 예외는 use 문으로 동일한 이름의 두 아이템을 스코프로 가져오는 경우다. Rust는 이를 허용하지 않기 때문이다. 리스트 7-15는 동일한 이름을 가졌지만 상위 모듈이 다른 두 Result 타입을 스코프로 가져오고 참조하는 방법을 보여준다.

Filename: src/lib.rs
use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
    Ok(())
}

fn function2() -> io::Result<()> {
    // --snip--
    Ok(())
}
Listing 7-15: 동일한 이름을 가진 두 타입을 같은 스코프로 가져오려면 상위 모듈을 사용해야 한다.

보는 바와 같이, 상위 모듈을 사용하면 두 Result 타입을 구분할 수 있다. 만약 use std::fmt::Resultuse std::io::Result를 지정하면 같은 스코프에 두 Result 타입이 존재하게 되고, Rust는 Result를 사용할 때 어느 것을 의미하는지 알 수 없다.

as 키워드로 새로운 이름 제공하기

동일한 이름의 두 타입을 같은 스코프로 가져오는 문제를 해결하는 또 다른 방법은 use를 사용할 때 경로 뒤에 as와 함께 새로운 로컬 이름, 즉 _별칭_을 지정하는 것이다. Listing 7-16은 as를 사용해 두 Result 타입 중 하나의 이름을 바꿔 Listing 7-15의 코드를 다시 작성한 예제다.

Filename: src/lib.rs
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
    Ok(())
}

fn function2() -> IoResult<()> {
    // --snip--
    Ok(())
}
Listing 7-16: as 키워드를 사용해 스코프로 가져올 때 타입 이름 변경하기

두 번째 use 문에서 std::io::Result 타입에 IoResult라는 새로운 이름을 선택했다. 이렇게 하면 std::fmtResult와 충돌하지 않는다. Listing 7-15와 Listing 7-16은 모두 관용적인 방식으로 간주되므로, 어떤 방식을 선택할지는 여러분의 선택에 달려 있다!

pub use로 이름 다시 내보내기

use 키워드를 사용해 이름을 스코프로 가져오면, 해당 이름은 임포트한 스코프 내에서만 비공개로 사용된다. 이 이름을 외부 스코프에서도 마치 해당 스코프에 정의된 것처럼 참조할 수 있게 하려면 pubuse를 함께 사용한다. 이 기법을 _다시 내보내기(re-exporting)_라고 부르며, 아이템을 스코프로 가져오는 동시에 다른 스코프에서도 사용할 수 있도록 공개하는 역할을 한다.

Listing 7-17은 Listing 7-11의 코드에서 루트 모듈의 usepub use로 변경한 예제를 보여준다.

Filename: src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}
Listing 7-17: pub use를 사용해 새로운 스코프에서 이름을 공개하기

이 변경 전에는 외부 코드에서 add_to_waitlist 함수를 호출하려면 restaurant::front_of_house::hosting::add_to_waitlist()와 같은 경로를 사용해야 했으며, front_of_house 모듈도 pub으로 표시되어야 했다. 이제 pub use를 통해 루트 모듈에서 hosting 모듈을 다시 내보냈으므로, 외부 코드는 restaurant::hosting::add_to_waitlist()와 같은 간단한 경로를 사용할 수 있다.

다시 내보내기는 코드의 내부 구조와 이를 호출하는 프로그래머가 생각하는 도메인 구조가 다를 때 유용하다. 예를 들어, 레스토랑 비유에서 레스토랑을 운영하는 사람들은 “전면(front of house)“과 “후면(back of house)“을 구분해 생각할 수 있다. 하지만 레스토랑을 방문하는 고객은 이러한 용어로 레스토랑의 구조를 생각하지 않을 것이다. pub use를 사용하면 코드는 한 구조로 작성하되, 다른 구조로 공개할 수 있다. 이렇게 하면 라이브러리를 개발하는 프로그래머와 라이브러리를 호출하는 프로그래머 모두에게 잘 조직된 라이브러리를 제공할 수 있다. pub use의 또 다른 예제와 이 기능이 크레이트의 문서에 미치는 영향에 대해서는 14장의 pub use로 편리한 공개 API 내보내기”에서 살펴볼 것이다.

외부 패키지 사용하기

2장에서는 랜덤 숫자를 얻기 위해 rand라는 외부 패키지를 사용하는 추측 게임 프로젝트를 만들었다. 프로젝트에서 rand를 사용하려면 Cargo.toml 파일에 다음 줄을 추가했다:

Filename: Cargo.toml
rand = "0.8.5"

_Cargo.toml_에 rand를 의존성으로 추가하면 Cargo는 crates.io에서 rand 패키지와 필요한 의존성을 다운로드하고 프로젝트에서 사용할 수 있게 한다.

그런 다음, rand의 정의를 패키지 스코프로 가져오기 위해 크레이트 이름인 rand로 시작하는 use 줄을 추가하고 스코프로 가져올 항목을 나열했다. 2장의 “랜덤 숫자 생성하기”에서 Rng 트레이트를 스코프로 가져오고 rand::thread_rng 함수를 호출한 것을 기억할 것이다:

use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Rust 커뮤니티의 멤버들은 crates.io에 많은 패키지를 제공하고 있으며, 이들 중 어떤 것을 프로젝트에 포함시키려면 동일한 단계를 거친다: 패키지의 Cargo.toml 파일에 나열하고 use를 사용해 해당 크레이트의 항목을 스코프로 가져온다.

표준 std 라이브러리도 프로젝트 외부의 크레이트라는 점에 유의하라. 표준 라이브러리는 Rust 언어와 함께 제공되므로 _Cargo.toml_을 변경해 std를 포함시킬 필요는 없다. 하지만 스코프로 항목을 가져오려면 use를 사용해 참조해야 한다. 예를 들어, HashMap을 사용하려면 다음과 같이 작성한다:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
}

이는 표준 라이브러리 크레이트의 이름인 std로 시작하는 절대 경로이다.

중첩 경로를 사용해 긴 use 목록 정리하기

같은 크레이트나 모듈에서 정의된 여러 항목을 사용할 때, 각 항목을 한 줄씩 나열하면 파일에서 많은 세로 공간을 차지한다. 예를 들어, 2장의 추측 게임에서 사용했던 다음 두 use 문은 std에서 항목을 가져온다:

Filename: src/main.rs
use rand::Rng;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

대신, 중첩 경로를 사용해 한 줄로 같은 항목을 가져올 수 있다. 경로의 공통 부분을 지정한 다음, 두 개의 콜론을 붙이고, 중괄호로 감싸서 경로의 다른 부분을 나열한다. 이는 리스트 7-18에서 보여준다.

Filename: src/main.rs
use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}
Listing 7-18: 같은 접두사를 가진 여러 항목을 범위로 가져오기 위해 중첩 경로 지정

더 큰 프로그램에서는 중첩 경로를 사용해 같은 크레이트나 모듈에서 많은 항목을 가져오면 필요한 use 문의 수를 크게 줄일 수 있다!

경로의 어떤 수준에서든 중첩 경로를 사용할 수 있으며, 이는 하위 경로를 공유하는 두 개의 use 문을 결합할 때 유용하다. 예를 들어, 리스트 7-19는 두 개의 use 문을 보여준다. 하나는 std::io를 가져오고, 다른 하나는 std::io::Write를 가져온다.

Filename: src/lib.rs
use std::io;
use std::io::Write;
Listing 7-19: 하나가 다른 하나의 하위 경로인 두 개의 use

이 두 경로의 공통 부분은 std::io이며, 이는 첫 번째 경로의 전체이다. 이 두 경로를 하나의 use 문으로 결합하려면, 중첩 경로에서 self를 사용할 수 있다. 이는 리스트 7-20에서 보여준다.

Filename: src/lib.rs
use std::io::{self, Write};
Listing 7-20: 리스트 7-19의 경로를 하나의 use 문으로 결합

이 한 줄은 std::iostd::io::Write를 범위로 가져온다.

Glob 연산자

특정 경로에 정의된 모든 공개 항목을 범위로 가져오려면, 해당 경로 뒤에 * Glob 연산자를 지정한다:

#![allow(unused)]
fn main() {
use std::collections::*;
}

use 문은 std::collections에 정의된 모든 공개 항목을 현재 범위로 가져온다. 하지만 Glob 연산자를 사용할 때는 주의해야 한다. Glob은 어떤 이름이 범위 내에 있는지, 프로그램에서 사용된 이름이 어디에서 정의되었는지 파악하기 어렵게 만들 수 있다.

Glob 연산자는 주로 테스트 시 tests 모듈로 테스트 대상의 모든 항목을 가져올 때 사용한다. 이에 대해서는 11장 “테스트 작성 방법”에서 자세히 다룬다. 또한 Glob 연산자는 prelude 패턴의 일부로 사용되기도 한다. 이 패턴에 대한 자세한 내용은 표준 라이브러리 문서를 참고한다.