Rust 문서화와 문서 테스트
docs.rs의 모든 페이지, 그리고 표준 라이브러리 문서의 모든 페이지는 누군가 소스 코드 안에 특별한 주석으로 직접 쓴 거예요. Rust는 명령어 하나로 그 주석들을 웹사이트로 바꿔 줘요. 여러분의 코드에도 똑같이 할 수 있어요.
신경 써야 할 이유가 하나 더 있어요. 문서 안에 넣은 코드 예제는 진짜 테스트예요. cargo test를 실행하면 Rust가 그 예제들을 컴파일하고 실행해요. 함수를 바꾸고 예제 고치는 걸 깜빡하면 테스트가 실패해요. 그래서 문서가 나도 모르게 낡아 버릴 수가 없어요. 프로그래밍 언어 중에 이런 기능이 있는 경우는 드물고, Rust의 가장 좋은 점 중 하나예요.
문서 주석: ///와 //!
섹션 제목: “문서 주석: ///와 //!”일반 주석은 주석 페이지에서 이미 봤어요. 문서 주석(doc comment)도 생김새는 비슷하지만 글자가 세 개예요.
///는 바로 아래에 있는 항목을 설명해요. 함수, 구조체, 열거형, 필드, 모듈 등이요.//!는 자신을 감싸고 있는 항목을 설명해요.src/lib.rs나src/main.rs의 맨 위에 쓰면 크레이트 전체를 설명하는 주석이 돼요.
//! 아주 작은 레시피 프로그램이에요.
/// `total`그램을 `portions`인분으로 똑같이 나눠요.fn split_evenly(total: u32, portions: u32) -> u32 { total / portions}문서 주석의 첫 줄은 짧은 요약 문장으로 쓰는 게 좋아요. 도구들이 목록이나 검색 결과에 그 줄을 보여 주니까, 핵심을 잘 담아 주세요.
문서 주석은 프로그램의 동작을 바꾸지 않아요. 아래는 문서를 단 함수를 main에서 사용하는 예제예요.
/// `total`그램을 `portions`인분으로 똑같이 나눠요.////// 남는 그램은 버려요. 그래서 10그램을 3인분으로 나누면 3이 돼요.////// # Panics////// `portions`가 0이면 패닉이 발생해요.fn split_evenly(total: u32, portions: u32) -> u32 { if portions == 0 { panic!("portions는 0보다 커야 해요"); } total / portions}
fn main() { println!("{}", split_evenly(600, 3)); println!("{}", split_evenly(10, 3));}실행 결과:
2003대부분의 에디터에서는 이제 main 안의 split_evenly에 마우스를 올리면 이 설명이 팝업으로 떠요. 그것만으로도 문서를 쓸 가치가 충분해요.
문서 주석 안의 Markdown
섹션 제목: “문서 주석 안의 Markdown”문서 주석은 Markdown으로 써요. GitHub나 여러 채팅 앱에서 쓰는 바로 그 형식이에요. `코드`, **굵게**, *기울임*, 목록, 링크, 제목을 모두 쓸 수 있어요.
/// 요리에 맞는 오븐 온도를 **섭씨**로 돌려줘요.////// 지원하는 요리:////// - `"bread"`: 220도/// - `"cake"`: 180도/// - 그 밖의 요리: 200도////// 오븐마다 다르니까 *꼭* 여러분의 오븐을 확인하세요./// [위키백과의 섭씨 온도](https://ko.wikipedia.org/wiki/섭씨_온도) 문서도 참고하세요.pub fn oven_temperature(dish: &str) -> u32 { match dish { "bread" => 220, "cake" => 180, _ => 200, }}문단 사이에는 빈 /// 줄을 하나 넣어요. 일반 Markdown 파일에서 빈 줄을 넣는 것과 똑같아요. 함수, 타입, 값의 이름은 백틱으로 감싸서 코드로 보이게 해 주세요.
표준 섹션
섹션 제목: “표준 섹션”Rust 개발자들은 몇 가지 제목을 반복해서 써요. 그래서 읽는 사람이 어디를 봐야 할지 바로 알 수 있어요. 하나는 이미 봤죠. 자주 쓰는 제목은 이런 것들이에요.
# Examples: 이 항목을 어떻게 쓰는지 보여 줘요. 공개 항목이라면 거의 다 있어야 해요.# Panics: 함수가 언제 패닉을 일으키는지 알려 줘서, 호출하는 쪽에서 피할 수 있게 해요.# Errors:Result를 돌려주는 함수가 언제, 왜Err를 돌려주는지 설명해요.# Safety:unsafe fn이라면, 호출하는 쪽이 지켜야 할 규칙을 적어요(Unsafe Rust 참고).
이 제목들은 영어로 쓰는 게 관례예요. docs.rs의 거의 모든 크레이트가 이렇게 쓰니까, 본문은 한국어로 쓰더라도 제목은 영어 그대로 두는 걸 추천해요.
아래는 라이브러리 파일 src/lib.rs에 # Errors와 # Safety를 쓴 예예요.
/// `"250g"` 같은 양을 읽어서 그램 수를 돌려줘요.////// # Errors////// 텍스트가 `g`로 끝나지 않거나,/// `g` 앞부분이 정수가 아니면 에러를 돌려줘요.pub fn parse_grams(text: &str) -> Result<u32, String> { let number = text .strip_suffix('g') .ok_or(format!("'{text}': g로 끝나야 해요"))?; number .parse() .map_err(|_| format!("'{number}': 정수가 아니에요"))}
/// 목록이 비어 있는지 확인하지 않고 첫 번째 재료를 돌려줘요.////// # Safety////// 호출하는 쪽에서 `ingredients`가 비어 있지 않다는 걸 보장해야 해요./// 빈 슬라이스에 이 함수를 호출하면 정의되지 않은 동작(undefined behavior)이 일어나요.pub unsafe fn first_unchecked(ingredients: &[String]) -> &String { unsafe { ingredients.get_unchecked(0) }}제목에 #를 하나만 쓴 점을 눈여겨보세요. 문서 주석 안에서 #는 Rust 속성(attribute)이 아니라 Markdown 제목이에요.
cargo doc으로 문서 만들기
섹션 제목: “cargo doc으로 문서 만들기”이 페이지의 나머지 부분에서는 작은 라이브러리를 하나 다룰 거예요. cargo new --lib kitchen으로 만들어 주세요. 그런 다음 프로젝트 안에서 이렇게 실행해요.
cargo doc --open Documenting kitchen v0.1.0 (/path/to/kitchen) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.91s Generated /path/to/kitchen/target/doc/kitchen/index.htmlCargo가 target/doc/ 안에 HTML 웹사이트를 만들고, --open이 그걸 브라우저로 열어 줘요. 생김새는 docs.rs의 페이지와 똑같아요. 사이드바와 검색창이 있고, 함수와 타입마다 페이지가 하나씩 있어요. 여러분이 의존하는 크레이트들의 문서도 전부 함께 만들어 주기 때문에, 인터넷이 안 될 때도 편리해요.
기본적으로는 공개(pub) 항목만 문서에 나와요. 문서는 여러분의 라이브러리를 사용하는 사람을 위한 것이기 때문이에요. 모든 항목을 넣고 싶다면 --document-private-items를 붙이세요.
문서 테스트: 실행되는 예제
섹션 제목: “문서 테스트: 실행되는 예제”아래는 kitchen 라이브러리의 src/lib.rs 앞부분이에요. 함수의 문서 주석 안에 # Examples 섹션이 있고, 그 안에 코드 블록이 들어 있어요.
//! 주방에서 쓰는 작은 도우미 함수들이에요.//!//! `kitchen`은 오븐 온도를 변환하고, 음식을 여러 인분으로 나누고,//! `"250g"` 같은 양을 읽어 들여요.
/// 섭씨 온도를 화씨 온도로 변환해요.////// # Examples////// ```/// let f = kitchen::celsius_to_fahrenheit(180.0);/// assert_eq!(f, 356.0);/// ```pub fn celsius_to_fahrenheit(celsius: f64) -> f64 { celsius * 9.0 / 5.0 + 32.0}이제 cargo test를 실행해 보세요.
Doc-tests kitchen
running 1 testtest src/lib.rs - celsius_to_fahrenheit (line 10) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sCargo가 10번째 줄의 코드 블록을 찾아서 작은 프로그램으로 만들고, 컴파일하고, 실행했어요. 답이 맞는지는 assert_eq!가 확인했고요. 이런 문서 테스트(doc test)에서 알아 둘 점이 몇 가지 있어요.
- 예제에서 크레이트 이름을 붙여
kitchen::celsius_to_fahrenheit로 호출했어요. 문서 테스트는 여러분의 라이브러리를 사용하는 다른 사람의 프로그램처럼 컴파일돼요. 그래서pub항목만 보이고, 전체 경로를 쓰거나use kitchen::...;줄을 넣어야 해요. fn main은 쓰지 않아요. rustdoc이 예제를 알아서main으로 감싸 줘요.assert!와assert_eq!(테스트 페이지 참고)를 쓰면 결과를 보여 주는 동시에 확인할 수 있어요.
예제가 틀렸다면 어떻게 될까요? 356.0을 350.0으로 바꾸고 테스트를 다시 실행해 보세요.
running 1 testtest src/lib.rs - celsius_to_fahrenheit (line 10) ... FAILED
failures:
---- src/lib.rs - celsius_to_fahrenheit (line 10) stdout ----Test executable failed (exit status: 101).
stderr:assertion `left == right` failed left: 356.0 right: 350.0
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sCargo는 이 결과를 영어로 보여 줘요. 실제 값(left)은 356.0인데 기대한 값(right)은 350.0이라서 테스트가 실패(FAILED)했다는 뜻이에요. 바로 이게 핵심이에요. 틀린 예제는 몇 달 뒤에 읽는 사람을 헷갈리게 만드는 대신, 그 자리에서 바로 잡혀요. 문서 테스트만 실행하고 싶다면 cargo test --doc을 쓰세요.
#로 준비 코드 숨기기
섹션 제목: “#로 준비 코드 숨기기”가끔 예제에는 꼭 필요하지만 읽는 사람의 눈을 흩뜨리는 줄이 있어요. 문서 테스트에서 # (샵과 공백)으로 시작하는 줄은 컴파일은 되지만 만들어진 문서에는 보이지 않아요.
가장 흔한 예는 이래요. ? 연산자를 보여 주고 싶은데, ?는 Result를 돌려주는 함수 안에서만 쓸 수 있어요. 그래서 직접 만든 main을 숨겨요.
/// `"250g"` 같은 양을 읽어서 그램 수를 돌려줘요.////// # Examples////// ```/// use kitchen::parse_grams;////// # fn main() -> Result<(), String> {/// let flour = parse_grams("250g")?;/// assert_eq!(flour, 250);////// assert!(parse_grams("많이").is_err());/// # Ok(())/// # }/// ```pub fn parse_grams(text: &str) -> Result<u32, String> { let number = text .strip_suffix('g') .ok_or(format!("'{text}': g로 끝나야 해요"))?; number .parse() .map_err(|_| format!("'{number}': 정수가 아니에요"))}브라우저에서는 use, ?를 쓴 호출, 그리고 두 개의 assert, 이렇게 중요한 네 줄만 보여요. 테스트는 여전히 전부 컴파일해요.
ignore, no_run, should_panic
섹션 제목: “ignore, no_run, should_panic”모든 예제를 그냥 실행할 수 있는 건 아니에요. 코드 블록을 여는 백틱 뒤에 단어를 붙여서 rustdoc에게 어떻게 할지 알려 줄 수 있어요.
```no_run: 예제를 컴파일만 하고 실행하지는 않아요. 파일을 읽거나, 네트워크에 연결하거나, 끝없이 도는 코드에 잘 맞아요.```should_panic: 실행해서 패닉이 나기를 기대해요. 패닉이 나지 않으면 테스트가 실패해요.```ignore: 아예 컴파일하지 않아요. 아이디어 스케치나 의사 코드(pseudo-code)에 써요. 그래도no_run을 먼저 시도해 보세요. 무시된 예제는 아무도 모르게 낡아 버릴 수 있거든요.```text: Rust 코드가 아니라 그냥 텍스트예요(예를 들어 실행 결과 예시).
/// `total`그램을 `portions`인분으로 똑같이 나눠요.////// # Examples////// ```/// assert_eq!(kitchen::split_evenly(600, 3), 200);/// ```////// 0인분은 말이 안 되니까, 이 코드는 패닉이 발생해요:////// ```should_panic/// kitchen::split_evenly(600, 0);/// ```pub fn split_evenly(total: u32, portions: u32) -> u32 { if portions == 0 { panic!("portions는 0보다 커야 해요"); } total / portions}
/// 파일에서 장보기 목록을 읽어 와요. 한 줄에 항목이 하나씩 있어요.////// ```no_run/// let items = kitchen::load_list("shopping.txt").unwrap();/// println!("살 물건이 {}개 있어요", items.len());/// ```pub fn load_list(path: &str) -> std::io::Result<Vec<String>> { let text = std::fs::read_to_string(path)?; Ok(text.lines().map(String::from).collect())}kitchen에 이것들을 모두 넣고, 크레이트의 //! 주석에 ignore 스케치도 하나 넣으면 cargo test는 이렇게 보고해요.
running 7 teststest src/lib.rs - (line 8) ... ignoredtest src/lib.rs - load_list (line 87) - compile ... oktest src/lib.rs - split_evenly (line 34) ... oktest src/lib.rs - split_evenly (line 40) - should panic ... oktest src/lib.rs - parse_grams (line 59) ... oktest src/lib.rs - celsius_to_fahrenheit (line 16) ... oktest src/lib.rs - Recipe (line 104) ... ok
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.01sno_run 테스트 옆에는 - compile이라고 나와요. 빌드만 했다는 뜻이에요. 테스트는 병렬로 실행되기 때문에, 줄의 순서는 실행할 때마다 달라질 수 있어요.
다른 항목으로 링크하기
섹션 제목: “다른 항목으로 링크하기”문서 주석 안에서 이름을 백틱과 대괄호로 감싸 [`Vec`]처럼 쓰면, rustdoc이 그 항목의 페이지로 가는 링크로 바꿔 줘요. 이걸 문서 내부 링크(intra-doc link)라고 해요. 여러분이 만든 항목에도, 표준 라이브러리의 어떤 항목에도 쓸 수 있어요.
//! 아주 작은 레시피 프로그램이에요.
/// 레시피예요. 이름과, 재료별 무게(그램)를 담은 목록으로 이루어져요.////// [`Recipe::new`]로 만들고 [`Recipe::add`]로 재료를 채워요./// 재료는 추가한 순서대로 [`Vec`]에 저장돼요.struct Recipe { /// 레시피 맨 위에 보이는 이름이에요. name: String, /// 각 재료와 그 무게(그램)예요. ingredients: Vec<(String, u32)>,}
impl Recipe { /// 주어진 이름으로 빈 레시피를 만들어요. fn new(name: &str) -> Recipe { Recipe { name: name.to_string(), ingredients: Vec::new() } }
/// 목록 끝에 재료를 추가해요. fn add(&mut self, ingredient: &str, grams: u32) { self.ingredients.push((ingredient.to_string(), grams)); }
/// 모든 재료의 무게를 더해요. fn total_grams(&self) -> u32 { self.ingredients.iter().map(|(_, grams)| grams).sum() }}
fn main() { let mut pancakes = Recipe::new("팬케이크"); pancakes.add("밀가루", 200); pancakes.add("우유", 300); pancakes.add("달걀", 50);
println!("{}", pancakes.name); for (ingredient, grams) in &pancakes.ingredients { println!("- {ingredient}: {grams} g"); } println!("합계: {} g", pancakes.total_grams());}실행 결과:
팬케이크- 밀가루: 200 g- 우유: 300 g- 달걀: 50 g합계: 550 g필드와 메서드에도 각자 /// 주석을 달 수 있고, 모두 구조체의 문서 페이지에 나타나요. 링크가 존재하지 않는 항목을 가리키면 cargo doc이 경고해 줘요. 아래는 Recipe::add를 Recipe::push로 잘못 쓴 경우예요.
warning: unresolved link to `Recipe::push` --> src/lib.rs:100:28 |100 | /// [`Recipe::new`]로 만들고 [`Recipe::push`]로 재료를 채워요. | ^^^^^^^^^^^^ the struct `Recipe` has no field or associated item named `push` | = note: `#[warn(rustdoc::broken_intra_doc_links)]` on by default컴파일러 메시지는 모두 영어로 나와요. 구조체 Recipe에 push라는 이름의 필드나 연관 항목이 없어서 Recipe::push로 가는 링크를 찾을 수 없다는 경고예요.
missing_docs로 문서 작성 강제하기
섹션 제목: “missing_docs로 문서 작성 강제하기”다른 사람이 쓸 라이브러리라면, 문서가 없는 공개 항목을 하나하나 알려 달라고 컴파일러에게 부탁할 수 있어요. src/lib.rs의 맨 첫 줄에 이렇게 써 주세요.
#![warn(missing_docs)]이제 문서 주석을 깜빡하면 경고가 나와요.
warning: missing documentation for a function --> src/lib.rs:138:1 |138 | pub fn grams_to_ounces(grams: f64) -> f64 { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |note: the lint level is defined here --> src/lib.rs:1:9 | 1 | #![warn(missing_docs)] | ^^^^^^^^^^^^“함수에 문서가 없다”는 경고이고, 아래쪽 note는 이 검사를 켠 곳이 1번째 줄이라고 알려 줘요. #!(느낌표가 붙은 형태)는 속성이 크레이트 전체에 적용된다는 뜻이에요. 이 검사는 크레이트 자체를 설명하는 //! 주석도 요구해요. 비공개 항목은 절대 경고하지 않으니까, 내부에서만 쓰는 도우미 함수 때문에 부담스러울 일은 없어요.
docs.rs: 문서를 무료로 호스팅해 줘요
섹션 제목: “docs.rs: 문서를 무료로 호스팅해 줘요”크레이트를 crates.io에 배포하면(Cargo와 크레이트 참고), docs.rs가 문서를 자동으로 만들어서 모든 버전을 무료로 호스팅해 줘요. 여러분은 아무것도 하지 않아도 돼요. 그래서 거의 모든 Rust 크레이트의 문서가 똑같은 모양이고 검색하기도 쉬운 거예요.
팁을 두 가지 더 알려 드릴게요.
rustup doc --std를 실행하면 표준 라이브러리 문서를 오프라인으로 브라우저에서 열 수 있어요.- docs.rs나 표준 라이브러리 문서의 어느 페이지에서든 Source 링크를 누르면 함수 뒤에 있는 실제 코드를 읽을 수 있어요. 공부하기에 정말 좋은 방법이에요.
자주 하는 실수
섹션 제목: “자주 하는 실수”문서 테스트에서 크레이트 이름을 빠뜨리는 실수. 문서 테스트는 크레이트 바깥에 있기 때문에, 이렇게 쓰면 실패해요.
error[E0425]: cannot find function `celsius_to_fahrenheit` in this scope --> src/lib.rs:17:9 |17 | let f = celsius_to_fahrenheit(180.0); | ^^^^^^^^^^^^^^^^^^^^^ not found in this scope“이 스코프에서 celsius_to_fahrenheit 함수를 찾을 수 없다”는 에러예요. kitchen::celsius_to_fahrenheit(180.0)로 고치거나, 예제 맨 위에 use kitchen::celsius_to_fahrenheit; 줄을 넣으면 돼요(원한다면 #로 숨기세요).
아무것도 설명하지 않는 문서 주석. ///는 항목 바로 앞에 와야 해요. 함수 본문 안에서는 //를 쓰세요.
error[E0585]: found a documentation comment that doesn't document anything --> src/main.rs:3:5 |3 | /// 음식을 3인분으로 나눠요. | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: doc comments must come before what they document, if a comment was intended use `//`“설명할 대상이 없는 문서 주석을 발견했다”는 에러예요. 도움말(help)은 문서 주석은 설명할 대상보다 앞에 와야 하고, 일반 주석을 쓰려던 거라면 //를 쓰라고 알려 줘요.
바이너리의 문서 테스트가 실행될 거라고 생각하는 실수. 문서 테스트는 라이브러리 크레이트(src/lib.rs)에서만 실행돼요. src/main.rs의 문서 주석에 있는 예제는 테스트되지 않아요. 프로그램이 커지면 로직을 src/lib.rs로 옮기고 main.rs는 작게 유지하세요.
코드를 그대로 되풀이하는 문서. fn add(a, b) 위에 “두 수를 더해요”라고 써 봤자 도움이 안 돼요. 코드만 봐서는 알 수 없는 걸 설명하세요. 단위(그램인지, 초인지), 경계 상황(edge case), 그리고 언제 실패하는지 같은 것들이요.
///는 바로 다음 항목을,//!는 자신을 감싸는 크레이트나 모듈을 설명해요. 둘 다 Markdown을 써요.- 표준 섹션
# Examples,# Panics,# Errors,# Safety를 활용하세요. cargo doc --open은 크레이트와 그 의존성의 문서를 HTML 사이트로 만들어 줘요.- 문서 주석 안의 코드 블록은 문서 테스트예요.
cargo test가 컴파일하고 실행해요. 준비 코드는#로 숨기고, 필요하면no_run,should_panic,ignore를 쓰세요. [`Name`]은 문서 내부 링크를 만들고,#![warn(missing_docs)]는 문서가 없는 공개 항목을 알려 줘요.- crates.io에 배포한 크레이트의 문서는 docs.rs가 자동으로 호스팅해 줘요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. src/lib.rs 맨 위에 쓰면 크레이트 전체를 설명하는 주석은 무엇일까요?
2. 라이브러리의 /// 주석 안에 있는 코드 블록을 cargo test는 어떻게 처리할까요?
3. 문서 테스트에서 # (샵과 공백)으로 시작하는 줄은 어떻게 될까요?
4. 네트워크에 연결하는 예제가 있어요. 컴파일은 하되 실행은 하지 않으려면 어떤 표시를 붙일까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- The rustdoc book(영어): Rust 문서화 도구의 공식 가이드예요.
- Documentation tests — the rustdoc book(영어): 숨김 줄과
compile_fail을 포함해, 문서 테스트의 모든 옵션을 설명해요. - Linking to items by name — the rustdoc book(영어): 문서 내부 링크를 쓰는 모든 방법을 다뤄요.
- crates.io에 크레이트 배포하기 — The Rust Programming Language 한국어판: 배포하기 전에 쓸모 있는 문서 주석을 작성하는 방법을 알려 줘요.
- Documentation — Rust API Guidelines(영어): 좋은 크레이트 문서에 무엇이 들어가야 하는지 정리한 체크리스트예요.