use 키워드로 경로를 스코프로 가져오기
함수를 호출할 때마다 전체 경로를 일일이 적는 것은 번거롭고 반복적으로 느껴질 수
있습니다. 목록 7-7에서는 add_to_waitlist 함수를 호출하기 위해 절대 경로를 쓰든
상대 경로를 쓰든, 매번 front_of_house 와 hosting 을 함께 적어야 했습니다.
다행히 이를 단순화하는 방법이 있습니다. use 키워드를 사용해 한 번만 경로의
지름길을 만들고, 그 뒤로는 같은 스코프 안에서 더 짧은 이름만 쓰면 됩니다.
목록 7-11에서는 crate::front_of_house::hosting 모듈을 eat_at_restaurant
함수의 스코프로 가져와, eat_at_restaurant 안에서 hosting::add_to_waitlist
라고만 적어 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 로 모듈을 스코프로 가져오기스코프 안에 use 와 경로를 추가하는 것은 파일시스템에서 심볼릭 링크를 만드는 것과
비슷합니다. 크레이트 루트에 use crate::front_of_house::hosting 을 추가하면,
hosting 이 이제 그 스코프 안에서 유효한 이름이 됩니다. 마치 hosting 모듈이
크레이트 루트에 정의된 것처럼 보이게 되는 것입니다. use 로 스코프 안에 가져온
경로도 다른 경로와 마찬가지로 privacy 검사를 받습니다.
use 는 오직 그것이 선언된 특정 스코프 에서만 지름길을 만든다는 점에 주의하세요.
목록 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();
}
}
use 문은 자신이 선언된 스코프에만 적용된다컴파일러 오류를 보면, 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
또한 use 가 원래 있던 스코프 안에서는 더 이상 사용되지 않는다는 경고도 함께 뜹니다.
이 문제를 해결하려면 use 를 customer 모듈 안으로 옮기거나, 자식 모듈 customer
안에서 super::hosting 처럼 부모 모듈의 지름길을 참조하면 됩니다.
관용적인 use 경로 만들기
목록 7-11을 보며 이런 의문이 들었을 수도 있습니다. 왜 use crate::front_of_house::hosting 을 적고, eat_at_restaurant 안에서는
hosting::add_to_waitlist 를 호출했을까요? 같은 효과를 얻기 위해
add_to_waitlist 함수까지 포함한 경로를 use 로 가져와도 될 것 같은데 말입니다.
목록 7-13처럼요.
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();
}
use 로 add_to_waitlist 함수를 직접 스코프로 가져오는 방식. 관용적이지 않다목록 7-11과 목록 7-13은 둘 다 같은 일을 하지만, use 로 함수를 스코프로 가져오는
관용적인 방식은 목록 7-11 쪽입니다. 함수 자체가 아니라 부모 모듈을 use 로 가져오면,
함수 호출 시 부모 모듈 이름을 붙여야 합니다. 이 방식은 함수가 로컬에 정의된 것이
아니라는 사실을 드러내면서도, 전체 경로를 계속 반복하는 일은 줄여 줍니다. 반대로
목록 7-13의 코드는 add_to_waitlist 가 어디서 정의된 것인지 한눈에 분명하지 않습니다.
반면 구조체, enum, 기타 항목을 use 로 가져올 때는 전체 경로를 지정하는 것이
관용적입니다. 목록 7-14는 표준 라이브러리의 HashMap 구조체를 바이너리 크레이트
스코프로 가져오는 관용적인 방식을 보여 줍니다.
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
HashMap 을 스코프로 가져오기이 관용구 뒤에 특별히 강한 이유가 있는 것은 아닙니다. 그냥 이런 방식이 널리 퍼졌고, 사람들이 이런 형태로 러스트 코드를 읽고 쓰는 데 익숙해졌기 때문입니다.
이 관용구의 예외는, use 문으로 같은 이름의 항목 두 개를 동시에 스코프로 가져오려는
경우입니다. 러스트는 같은 이름 두 개를 허용하지 않기 때문입니다. 목록 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::Result 와 use std::io::Result 를 그대로 둘 다 가져오면,
같은 스코프에 Result 타입이 두 개 있게 되어, 우리가 Result 라고 썼을 때
러스트는 어느 것을 뜻하는지 알 수 없게 됩니다.
as 키워드로 새 이름 붙이기
같은 이름의 타입 두 개를 같은 스코프로 가져오는 문제를 해결하는 또 다른 방법도
있습니다. 경로 뒤에 as 와 새 로컬 이름, 즉 별칭(alias) 을 붙이는 것입니다.
목록 7-16은 as 를 사용해 두 Result 타입 중 하나의 이름을 바꿈으로써, 목록
7-15의 코드를 다른 방식으로 쓴 예입니다.
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
as 키워드로 스코프로 가져온 타입 이름 바꾸기두 번째 use 문에서는 std::io::Result 타입의 새 이름으로 IoResult 를 선택했습니다.
이제 std::fmt 에서 가져온 Result 와 충돌하지 않게 됩니다. 목록 7-15와 7-16은
둘 다 관용적인 방식으로 여겨지므로, 어느 쪽을 선택할지는 여러분의 취향에 달려 있습니다.
pub use 로 이름 재수출하기
use 키워드로 어떤 이름을 스코프로 가져오면, 그 이름은 가져온 그 스코프 안에서만
private 합니다. 그 스코프 밖의 코드가 그 이름을 마치 그 스코프 안에서 정의된 것처럼
참조할 수 있게 하려면, pub 와 use 를 결합할 수 있습니다. 이 기법을
재수출(re-exporting) 이라고 합니다. 어떤 항목을 스코프로 가져오면서 동시에, 다른
사람도 자기 스코프로 가져올 수 있게 공개하기 때문입니다.
목록 7-17은 목록 7-11의 루트 모듈 안 use 를 pub 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();
}
pub use 로 새로운 스코프에서 누구나 사용할 수 있게 이름을 공개하기이 변경 이전에는, 외부 코드는
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 내보내기” 절에서 다시 보게 됩니다.
외부 패키지 사용하기
2장에서는 난수를 얻기 위해 rand 라는 외부 패키지를 사용한 숫자 맞히기 게임을
만들었습니다. 프로젝트에서 rand 를 사용하기 위해 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}");
}
러스트 커뮤니티 구성원들은 crates.io 에 수많은 패키지를
공개해 두었고, 그중 어떤 것이든 여러분의 패키지에 가져오는 과정은 똑같습니다.
즉, Cargo.toml 에 의존성을 적고, use 로 그 크레이트의 항목을 스코프로 가져오면
됩니다.
표준 라이브러리 std 도 사실은 우리 패키지 밖에 있는 외부 크레이트라는 점도
기억하세요. 다만 표준 라이브러리는 러스트 언어와 함께 제공되므로, Cargo.toml
을 바꿔서 std 를 따로 추가할 필요는 없습니다. 그러나 그 안의 항목을 패키지
스코프로 가져오려면 여전히 use 를 통해 참조해야 합니다. 예를 들어 HashMap 을
쓰려면 다음 줄을 사용합니다.
#![allow(unused)]
fn main() {
use std::collections::HashMap;
}
이것은 표준 라이브러리 크레이트 이름인 std 로 시작하는 절대 경로입니다.
중첩 경로로 use 목록 정리하기
같은 크레이트나 같은 모듈 안에 정의된 여러 항목을 사용한다면, 각 항목을 별도
use 줄로 적는 것은 파일에서 세로 공간을 많이 차지할 수 있습니다. 예를 들어 목록
2-4의 숫자 맞히기 게임에서는 다음 두 use 문으로 std 의 항목을 스코프로
가져왔습니다.
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을 보세요.
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 문을 합칠 때도 유용합니다. 예를 들어 목록 7-19는 두 개의 use 문을
보여 줍니다. 하나는 std::io 를, 다른 하나는 std::io::Write 를 스코프로
가져옵니다.
use std::io;
use std::io::Write;
use 문이 두 경로의 공통 부분은 std::io 이며, 첫 번째 경로 전체가 바로 그 공통 부분입니다.
이 둘을 하나의 use 문으로 합치려면, 목록 7-20처럼 중첩 경로 안에서 self 를
사용할 수 있습니다.
use std::io::{self, Write};
use 문으로 합치기이 한 줄은 std::io 와 std::io::Write 를 둘 다 스코프로 가져옵니다.
글롭 연산자로 항목 가져오기
어떤 경로 안에 정의된 모든 public 항목을 스코프로 가져오고 싶다면, 경로 뒤에
* 글롭 연산자를 붙일 수 있습니다.
#![allow(unused)]
fn main() {
use std::collections::*;
}
이 use 문은 std::collections 안에 정의된 모든 public 항목을 현재 스코프로
가져옵니다. 다만 글롭 연산자는 사용할 때 주의가 필요합니다! 글롭은 어떤 이름들이
현재 스코프에 들어와 있는지, 그리고 프로그램에서 사용한 이름이 원래 어디서 정의된
것인지를 파악하기 어렵게 만들 수 있습니다. 또한 의존성이 정의를 바꾸면, 여러분이
가져온 내용도 같이 바뀝니다. 예를 들어 그 의존성이 같은 스코프 안에서 여러분의 정의와
같은 이름의 항목을 새로 추가하면, 의존성을 업그레이드했을 때 컴파일 오류로 이어질 수
있습니다.
글롭 연산자는 테스트할 때 테스트 대상의 모든 것을 tests 모듈 안으로 가져오는 데
자주 사용되며, 이는 11장의 “테스트 작성 방법”
절에서 설명합니다. 또한 prelude 패턴의 일부로 사용되기도 합니다. 그 패턴에 대한 더
자세한 내용은 표준 라이브러리 문서
를 참고하세요.