Rust use 키워드: 긴 경로를 짧게 가져오는 법
Rust 예제를 보면 파일 맨 위에 use std::collections::HashMap; 같은 줄이 자주 나옵니다.
익숙해지면 아무렇지 않게 쓰지만, 처음에는 mod와 use가 비슷해 보여 헷갈리기 쉽습니다.
“파일을 가져오는 건가?”, “모듈을 만드는 건가?”, “이걸 쓰면 공개되는 건가?” 같은 질문이 자연스럽게 따라오죠.
결론부터 말하면 use는 이미 존재하는 경로를 현재 스코프에서 짧은 이름으로 쓰게 해주는 키워드입니다.
모듈을 새로 만들지도 않고, 항목의 공개 범위를 바꾸지도 않습니다.
이번 글에서는 use가 하는 일과 하지 않는 일을 분리해서 보고, crate::, super::, self::, 중괄호 import, 별칭, 트레이트 import까지 실전에서 자주 쓰는 패턴을 정리해보겠습니다.
use는 경로를 스코프로 가져옵니다
HashMap은 표준 라이브러리의 std::collections 모듈 안에 있습니다.
use 없이 쓰려면 전체 경로를 매번 적어야 합니다.
fn main() {
let mut scores = std::collections::HashMap::new();
scores.insert("Alice", 90);
scores.insert("Bob", 85);
}
한두 번이면 괜찮지만 코드가 길어지면 장황합니다.
이때 use로 긴 경로를 현재 스코프에 가져올 수 있습니다.
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("Alice", 90);
scores.insert("Bob", 85);
}
여기서 use std::collections::HashMap;은 HashMap이라는 이름을 현재 파일의 스코프에 바인딩합니다.
실제 타입은 여전히 std::collections::HashMap입니다.
단지 현재 위치에서 짧게 부를 수 있을 뿐이죠.
이 차이를 이해하면 use를 과하게 신비롭게 볼 필요가 없습니다.
use는 코드의 경로 표현을 정리하는 도구입니다.
mod와 use는 다릅니다
Rust 모듈 시스템 큰 그림에서 전체 역할을 나눠봤고, Rust mod 키워드에서는 mod가 모듈을 선언한다는 점을 더 자세히 봤습니다.
반면 use는 이미 선언되어 접근 가능한 항목을 현재 스코프로 가져옵니다.
예를 들어 프로젝트 안에 config 모듈이 있다면 먼저 부모 모듈에서 선언해야 합니다.
mod config;
use crate::config::default_port;
fn main() {
println!("port: {}", default_port());
}
pub fn default_port() -> u16 {
8080
}
mod config;가 없으면 crate::config::default_port라는 경로 자체가 존재하지 않습니다.
use crate::config::default_port;만 추가한다고 config.rs 파일이 자동으로 모듈 트리에 들어오지는 않습니다.
반대로 mod config;만 있고 use가 없다면 긴 경로로 호출하면 됩니다.
mod config;
fn main() {
println!("port: {}", crate::config::default_port());
}
즉 mod는 구조를 만들고, use는 그 구조 안의 경로를 편하게 부르게 합니다.
이 둘을 분리해서 생각하면 Rust 모듈 시스템이 훨씬 덜 낯설어집니다.
crate, self, super 경로
use는 경로를 받습니다.
그 경로는 표준 라이브러리나 외부 크레이트에서 시작할 수도 있고, 현재 크레이트 내부에서 시작할 수도 있습니다.
crate::는 현재 크레이트의 루트에서 시작하는 절대 경로입니다.
use crate::config::default_port;
self::는 현재 모듈에서 시작합니다.
mod parser {
pub fn parse() {}
}
use self::parser::parse;
super::는 부모 모듈에서 시작합니다.
mod api {
pub fn version() -> &'static str {
"v1"
}
pub mod users {
use super::version;
pub fn route() -> String {
format!("/{}/users", version())
}
}
}
users 모듈 입장에서 super는 부모인 api 모듈입니다.
그래서 super::version으로 부모 모듈의 함수를 가져올 수 있습니다.
테스트 모듈에서 자주 보는 use super::*;도 같은 원리입니다.
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_should_sum_two_numbers() {
assert_eq!(add(2, 3), 5);
}
}
tests는 자식 모듈이므로 부모에 있는 add()를 직접 볼 수 없습니다.
use super::*;로 부모 모듈의 이름들을 테스트 모듈 스코프로 가져온 것입니다.
중괄호로 묶어서 가져오기
같은 모듈에서 여러 항목을 가져올 때는 중괄호를 쓰면 깔끔합니다.
use std::collections::{HashMap, HashSet};
아래와 같은 의미입니다.
use std::collections::HashMap;
use std::collections::HashSet;
경로가 더 깊어도 사용할 수 있습니다.
use std::io::{self, Read, Write};
여기서 self는 std::io 자체도 가져오겠다는 뜻입니다.
따라서 io::Result도 쓰고, Read와 Write도 짧게 쓸 수 있습니다.
use std::fs::File;
use std::io::{self, Read};
fn read_file(path: &str) -> io::Result<String> {
let mut file = File::open(path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
std::fs로 파일 다루기에서 본 것처럼 Read와 Write는 트레이트입니다.
이런 트레이트를 use로 가져와야 그 트레이트가 제공하는 메서드를 자연스럽게 호출할 수 있는 경우가 많습니다.
트레이트를 가져와야 메서드가 보일 때
Rust에서는 타입 자체에 정의된 메서드뿐 아니라, 트레이트가 제공하는 메서드도 점 표기법으로 호출할 수 있습니다. 다만 그 트레이트가 현재 스코프에 있어야 메서드 후보로 고려되는 경우가 있습니다.
대표적인 예가 파일 읽기입니다.
use std::fs::File;
use std::io::Read;
fn read_config() -> std::io::Result<String> {
let mut file = File::open("config.toml")?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
File 타입 자체만 보면 read_to_string()이 어디서 온 메서드인지 잘 보이지 않습니다.
이 메서드는 std::io::Read 트레이트가 제공하고, File이 그 트레이트를 구현하고 있기 때문에 호출할 수 있습니다.
그래서 use std::io::Read;를 지우면 컴파일러가 대개 “이 메서드를 제공하는 트레이트가 스코프에 없다”는 식으로 알려줍니다.
이 패턴은 Rust 초반에 꽤 자주 만납니다.
타입은 맞는 것 같은데 메서드가 없다고 나오면, 해당 메서드를 제공하는 트레이트를 use로 가져와야 하는지 확인해보세요.
표준 라이브러리 문서에서 메서드가 “Trait Implementations” 아래에 있는지도 좋은 단서가 됩니다.
as로 이름 바꾸기
서로 다른 모듈에 같은 이름의 타입이 있을 때는 as로 별칭을 붙일 수 있습니다.
use std::fmt::Result as FmtResult;
use std::io::Result as IoResult;
fn render() -> FmtResult {
Ok(())
}
fn load() -> IoResult<String> {
std::fs::read_to_string("config.toml")
}
std::fmt::Result와 std::io::Result는 둘 다 이름이 Result입니다.
그대로 가져오면 충돌하므로 FmtResult, IoResult처럼 의도를 드러내는 이름을 붙이는 편이 좋습니다.
별칭은 외부 크레이트 이름이 길거나, 같은 이름의 타입이 여러 개 있을 때 특히 유용합니다. 다만 너무 자주 쓰면 원래 경로를 추적하기 어려워질 수 있으니, 이름 충돌을 피하거나 의미를 더 분명히 할 때만 사용하는 것이 좋습니다.
use는 공개 범위를 바꾸지 않습니다
use는 이름을 현재 스코프로 가져올 뿐, 항목을 공개하지 않습니다.
다음 코드를 봅시다.
mod config;
use config::default_port;
pub fn default_port() -> u16 {
8080
}
lib.rs 안에서는 default_port()를 짧게 쓸 수 있습니다.
하지만 외부 크레이트 사용자가 my_crate::default_port()로 호출할 수 있는 것은 아닙니다.
외부 API로 내보내려면 pub use가 필요합니다.
mod config;
pub use config::default_port;
이제 외부에서는 my_crate::default_port()로 접근할 수 있습니다.
이런 재공개(re-export)는 라이브러리의 공개 API를 정리할 때 자주 씁니다.
자세한 내용은 Rust pub, crate, self, super에서 이어서 보겠습니다.
어디에 use를 둘까?
Rust에서는 보통 파일 위쪽에 use 선언을 모아둡니다.
그러면 이 파일이 어떤 외부 타입과 함수를 의존하는지 빠르게 훑을 수 있습니다.
규모가 커지면 대략 다음 순서로 정리하면 읽기 좋습니다.
std,core,alloc- 외부 크레이트
- 현재 워크스페이스의 다른 크레이트
super::경로crate::경로
예를 들면 이런 식입니다.
use std::collections::HashMap;
use std::sync::Arc;
use serde::Serialize;
use tokio::sync::RwLock;
use super::error::ApiError;
use crate::config::Config;
물론 프로젝트마다 정렬 규칙은 조금씩 다를 수 있습니다.
가장 중요한 것은 한 코드베이스 안에서 일관되게 쓰는 것입니다.
rustfmt와 cargo clippy를 함께 쓰면 불필요한 import나 어색한 스타일을 어느 정도 자동으로 잡을 수 있습니다.
함수 안쪽에 use를 둘 수도 있습니다.
특정 함수에서만 필요한 이름이거나, 조건부 컴파일 안에서만 쓰는 이름이라면 지역 스코프에 두는 편이 더 읽기 좋을 때가 있습니다.
fn current_dir_name() -> Option<String> {
use std::env;
let path = env::current_dir().ok()?;
let name = path.file_name()?.to_str()?;
Some(name.to_owned())
}
이런 지역 use는 파일 맨 위를 깨끗하게 유지하는 데 도움이 됩니다.
다만 여러 함수에서 반복해서 같은 항목을 가져오고 있다면, 파일 상단으로 올리는 편이 낫습니다.
반복이 많아지면 “이 모듈이 무엇에 의존하는지”를 오히려 파악하기 어려워지기 때문입니다.
마치며
use는 Rust의 긴 경로를 현재 스코프로 가져와 코드의 소음을 줄여주는 키워드입니다.
mod처럼 모듈을 선언하지 않고, pub처럼 공개 범위를 바꾸지도 않습니다.
이미 존재하고 접근 가능한 항목을 더 짧은 이름으로 부르게 해줄 뿐입니다.
실전에서는 crate::로 현재 크레이트의 루트에서 시작하고, super::로 부모 모듈을 참조하며, 중괄호와 as로 import를 정리하는 일이 많습니다.
여기에 pub use까지 이해하면 라이브러리의 공개 API를 깔끔하게 다듬을 수 있습니다.
그 이야기는 Rust 모듈 공개 범위에서 계속 이어가겠습니다.
더 자세한 문법은 Rust Reference의 use declarations 문서를 참고하세요.
This work is licensed under
CC BY 4.0