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

use 키워드로 경로를 스코프 안으로 가져오기 (Bringing Paths into Scope with the use Keyword)

함수를 호출할 때마다 긴 경로를 전부 써야 한다면 금방 지겹고 반복적이게 느껴져요. use 키워드는 그 경로에 대한 단축키를 만들어 줘서, 스코프 안 어디서든 짧은 이름으로 항목을 쓸 수 있게 해 줘요. 이번 절에서는 use의 기본 사용법부터, 이름 충돌을 피하는 as, 외부 크레이트에서 항목을 재공개하는 pub use, 그리고 중첩 경로와 글롭 연산자까지 차례로 살펴볼게요.

출처: The Rust Book — use 키워드로 경로를 스코프 안으로 가져오기

use로 단축키 만들기

경로를 써서 함수를 호출하는 일이 불편하고 반복적으로 느껴질 수 있어요. Listing 7-7에서 add_to_waitlist 함수로 가는 절대 경로든 상대 경로든, 호출할 때마다 front_of_househosting도 함께 적어야 했어요. 다행히 이 과정을 단순화할 방법이 있어요. use 키워드로 경로에 대한 단축키(shortcut)를 한 번 만들면, 그 후부터는 그 스코프 안 어디서든 짧은 이름을 쓰면 돼요.

Listing 7-11에서는 crate::front_of_house::hosting 모듈을 eat_at_restaurant 함수의 스코프로 가져와서, eat_at_restaurant 안에서 add_to_waitlist 함수를 호출할 때 hosting::add_to_waitlist만 쓰면 되게 만들었어요.

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();
}

스코프에 use와 경로를 추가하는 것은 파일시스템에 심볼릭 링크(symbolic link)를 만드는 것과 비슷해요. 크레이트 루트에 use crate::front_of_house::hosting을 추가하면, 마치 hosting 모듈이 크레이트 루트에 정의된 것처럼 그 스코프에서 hosting이 유효한 이름이 돼요. use로 스코프 안으로 가져온 경로도 다른 경로와 마찬가지로 비공개 여부를 확인해요.

use는 그 use가 나타나는 특정 스코프에 대해서만 단축키를 만든다는 점을 기억하세요. Listing 7-12는 eat_at_restaurant 함수를 customer라는 새 자식 모듈 안으로 옮겼는데, 그 모듈은 use 문과는 다른 스코프이므로 함수 본문이 컴파일되지 않아요.

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();
    }
}

컴파일러 오류는 그 단축키가 더는 customer 모듈 안에서는 적용되지 않는다는 것을 보여 줘요.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of unresolved module or unlinked crate `hosting`
  --> src/lib.rs:11:9
   |
11 |         hosting::add_to_waitlist();
   |         ^^^^^^^ use of unresolved module or unlinked crate `hosting`
   |
   = help: if you wanted to use a crate named `hosting`, use `cargo add hosting` to add it to your `Cargo.toml`
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

자식 customer 모듈 안에서 use가 더는 사용되지 않는다는 경고도 함께 나오는 걸 볼 수 있어요. 이 문제를 고치려면 usecustomer 모듈 안으로도 옮기거나, 자식 customer 모듈 안에서 super::hosting으로 부모 모듈의 단축키를 참조하면 돼요.

관용적인(idiomatic) use 경로 만들기 (Creating Idiomatic use Paths)

Listing 7-11에서, 왜 use crate::front_of_house::hosting으로 지정하고 eat_at_restaurant 안에서 hosting::add_to_waitlist를 호출했을까 궁금했을 수 있어요. 같은 결과를 얻으려면 Listing 7-13처럼 use 경로를 add_to_waitlist 함수까지 끝까지 지정해도 되는데 말이죠.

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-11과 Listing 7-13이 같은 일을 하긴 하지만, 함수를 use로 스코프 안으로 가져올 때는 Listing 7-11이 관용적인 방법이에요. use로 함수의 부모 모듈을 스코프 안으로 가져오면 함수를 호출할 때 부모 모듈을 지정해야 해요. 호출할 때 부모 모듈을 지정하면 그 함수가 로컬로 정의된 것이 아님을 분명히 하면서도 전체 경로를 반복하는 일은 최소화돼요. Listing 7-13의 코드는 add_to_waitlist가 어디에 정의되어 있는지 모호해요.

반면 struct, enum 같은 다른 항목을 use로 가져올 때는 전체 경로를 지정하는 것이 관용적이에요. Listing 7-14는 표준 라이브러리의 HashMap struct를 바이너리 크레이트의 스코프로 가져오는 관용적인 방법을 보여 줘요.

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

이 관용법 뒤에 있는 강한 이유는 없어요. 그저 자연스럽게 퍼진 관례(convention)일 뿐이고, 사람들이 이렇게 러스트 코드를 읽고 쓰는 데 익숙해진 거예요.

이 관용법의 예외는 같은 이름을 가진 항목 두 개를 use 문으로 스코프 안으로 가져오려 할 때예요. 러스트는 그것을 허용하지 않으니까요. Listing 7-15는 같은 이름이지만 부모 모듈이 다른 두 Result 타입을 스코프 안으로 가져오는 방법과, 그것들을 어떻게 참조하는지 보여 줘요.

use std::fmt;
use std::io;

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

    Ok(())
}

fn function2() -> io::Result<()> {
    // --snip--

    Ok(())
}

보시다시피 부모 모듈을 사용하면 두 Result 타입을 구분할 수 있어요. 만약 use std::fmt::Resultuse std::io::Result를 지정한다면 같은 스코프에 Result 타입이 두 개 생겨서, Result를 쓸 때 러스트가 어느 것을 의미하는지 알 수 없어요.

as 키워드로 새 이름 제공하기 (Providing New Names with the as Keyword)

같은 이름의 타입 두 개를 use로 같은 스코프에 가져오는 문제의 또 다른 해결책이 있어요. 경로 뒤에 as와 새 로컬 이름(별명, alias)을 지정하면 돼요. Listing 7-16은 as로 두 Result 타입 중 하나의 이름을 바꿔서 Listing 7-15의 코드를 다른 방식으로 쓴 거예요.

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

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

    Ok(())
}

fn function2() -> IoResult<()> {
    // --snip--

    Ok(())
}

두 번째 use 문에서 std::io::Result 타입의 새 이름으로 IoResult를 골랐어요. 이 이름은 스코프 안으로 가져온 std::fmtResult와 충돌하지 않죠. Listing 7-15와 Listing 7-16 모두 관용적이라고 여겨지므로, 선택은 여러분 몫이에요!

pub use로 이름 재공개하기 (Re-exporting Names with pub use)

use 키워드로 이름을 스코프 안으로 가져오면 그 이름은 가져온 스코프에 비공개예요. 그 스코프 밖의 코드가 그 이름을 마치 그 스코프에 정의된 것처럼 참조할 수 있게 하려면 pubuse를 결합하면 돼요. 이 기법을 재공개(re-exporting) 라고 해요. 항목을 스코프 안으로 가져오면서도, 다른 사람들이 그 항목을 자기 스코프 안으로 가져올 수 있게 만들기 때문이에요.

Listing 7-17은 Listing 7-11의 코드에서 루트 모듈의 usepub use로 바꾼 거예요.

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();
}

이 변경 전에는 외부 코드가 restaurant::front_of_house::hosting::add_to_waitlist() 경로로 add_to_waitlist 함수를 호출해야 했어요. 그 경로는 front_of_house 모듈도 pub로 표시할 것을 요구했죠. 이제 이 pub use가 루트 모듈에서 hosting 모듈을 재공개했으므로, 외부 코드는 대신 restaurant::hosting::add_to_waitlist() 경로를 쓸 수 있어요.

재공개는 내부 구조가 코드를 호출하는 프로그래머들이 도메인을 생각하는 방식과 다를 때 유용해요. 이 레스토랑 비유에서 레스토랑을 운영하는 사람들은 "프론트 오브 하우스"와 "백 오브 하우스"로 생각해요. 하지만 레스토랑을 방문하는 손님들은 레스토랑의 부분을 그런 용어로 생각하지 않을 거예요. pub use를 쓰면 코드를 한 구조로 작성하면서도 다른 구조를 노출할 수 있어요. 이렇게 하면 라이브러리를 만드는 프로그래머와 라이브러리를 호출하는 프로그래머 모두에게 잘 정리된 라이브러리가 돼요. pub use의 또 다른 예시와 크레이트 문서에 미치는 영향은 14장의 "편리한 공개 API 내보내기"에서 다룰게요.

외부 패키지 사용하기 (Using External Packages)

2장에서 무작위 숫자를 얻는 데 rand라는 외부 패키지를 사용한 추리 게임 프로젝트를 만들었어요. 프로젝트에서 rand를 사용하려면 Cargo.toml에 이 줄을 추가했죠.

rand = "0.8.5"

Cargo.tomlrand를 의존성으로 추가하는 것은 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}");
}

러스트 커뮤니티 멤버들이 crates.io에 많은 패키지를 공개해 두었어요. 그중 아무거나 패키지로 가져오는 일은 같은 단계를 거쳐요. 패키지의 Cargo.toml 파일에 나열하고, use로 그들의 크레이트에서 항목들을 스코프 안으로 가져오는 거예요.

표준 std 라이브러리도 우리 패키지의 바깥에 있는 크레이트라는 점을 기억하세요. 표준 라이브러리는 러스트 언어와 함께 제공되므로 std를 포함시키기 위해 Cargo.toml을 바꿀 필요는 없어요. 하지만 거기서 항목을 패키지의 스코프로 가져오려면 use로 참조해야 해요. 예를 들어 HashMap을 쓰려면 이 줄을 사용해요.

#![allow(unused)]

fn main() {
    use std::collections::HashMap;
}

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

중첩 경로로 use 목록 정리하기 (Using Nested Paths to Clean Up use Lists)

같은 크레이트나 같은 모듈에 정의된 여러 항목을 사용한다면 각 항목을 자기 줄에 나열하는 것은 파일에서 세로 공간을 많이 차지할 수 있어요. 예를 들어 추리 게임의 Listing 2-4에서 std에서 항목들을 가져온 두 use 문이 있어요.

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!"),
    }
}

대신 중첩 경로(nested paths) 를 써서 같은 항목들을 한 줄로 스코프 안으로 가져올 수 있어요. 경로의 공통 부분을 지정하고, 이중 콜론 두 개, 그리고 서로 다른 경로 부분들의 목록을 중괄호 안에 두면 돼요. Listing 7-18이 그 예시예요.

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!"),
    }
}

더 큰 프로그램에서는 같은 크레이트나 모듈에서 많은 항목을 스코프 안으로 가져올 때 중첩 경로를 쓰면 필요한 개별 use 문의 수를 크게 줄일 수 있어요.

중첩 경로는 경로의 어느 수준에서든 쓸 수 있어서, 하위 경로를 공유하는 두 use 문을 합칠 때 유용해요. 예를 들어 Listing 7-19는 두 use 문을 보여 줘요. 하나는 std::io를 스코프 안으로 가져오고, 다른 하나는 std::io::Write를 가져와요.

use std::io;
use std::io::Write;

이 두 경로의 공통 부분은 std::io이고, 이건 첫 번째 경로의 전체이기도 해요. 이 두 경로를 한 use 문으로 합치려면 중첩 경로에서 self를 쓰면 돼요. Listing 7-20이 그 예시예요.

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

이 줄은 std::iostd::io::Write를 스코프 안으로 가져와요.

글롭 연산자로 항목 가져오기 (Importing Items with the Glob Operator)

경로에 정의된 모든 공개 항목을 스코프 안으로 가져오고 싶다면, 그 경로 뒤에 * 글롭 연산자(glob operator)를 붙이면 돼요.

#![allow(unused)]

fn main() {
    use std::collections::*;
}

use 문은 std::collections에 정의된 모든 공개 항목을 현재 스코프 안으로 가져와요. 글롭 연산자를 쓸 때는 조심해야 해요! 글롭은 어떤 이름이 스코프 안에 있는지, 프로그램에서 쓰는 이름이 어디에 정의되어 있는지 알기 어렵게 만들 수 있어요. 게다가 의존성이 정의를 바꾸면 가져온 것도 바뀌어요. 예를 들어 의존성이 여러분의 정의와 같은 이름을 스코프 안에 추가하면, 의존성을 업그레이드할 때 컴파일러 오류가 날 수 있어요.

글롭 연산자는 테스트에서 테스트 대상의 모든 것을 tests 모듈 안으로 가져올 때 자주 사용돼요. 이건 11장의 "테스트 작성 방법"에서 다룰게요. 글롭 연산자는 프렐루드(prelude) 패턴의 일부로 가끔 쓰이기도 해요. 그 패턴에 대한 자세한 내용은 표준 라이브러리 문서에서 확인할 수 있어요.

더 알아보기 (Learn more)