cargo fmt로 Rust 코드 스타일 자동 정리하기
팀에서 코드 리뷰를 하다 보면 로직과 무관한 스타일 논쟁에 시간을 쓸 때가 있어요. 중괄호를 같은 줄에 둘지 다음 줄에 둘지, use 구문을 어떻게 묶을지, 한 줄이 몇 글자를 넘으면 줄바꿈할지… 이런 토론이 쌓이면 정작 중요한 설계나 로직 리뷰에 집중하기 어렵습니다 😅
Rust 생태계에서는 이 문제를 rustfmt라는 공식 포매터로 깔끔하게 해결합니다. cargo fmt 한 번이면 코드 스타일이 일관되게 정리되니까 팀원들은 포매팅에 신경 쓸 필요 없이 코드 자체에만 집중할 수 있죠. 이번 글에서는 rustfmt의 설치부터 설정 커스터마이징 그리고 CI 파이프라인에 통합하는 방법까지 차근차근 알아보겠습니다.
rustfmt가 뭔가요?
rustfmt는 Rust 코드를 공식 스타일 가이드(Rust Style Guide)에 맞춰 자동으로 포매팅해주는 도구입니다. Go 언어의 gofmt에서 영감을 받았는데, 핵심 철학은 동일해요. “포매팅에 대한 논쟁을 없앤다”는 거죠.
Rust 프로젝트 초기부터 공식 도구 체인의 일부로 개발되었고, 지금은 대부분의 Rust 프로젝트에서 사실상 표준으로 사용하고 있습니다. cargo fmt라는 Cargo 서브커맨드로 실행할 수 있어서 별도의 바이너리를 직접 다룰 필요도 없고요.
rustfmt가 처리하는 것들을 몇 가지 예로 들면 들여쓰기와 공백 정리, 줄바꿈 위치 결정, use 문 정렬, 후행 쉼표 추가, 함수 시그니처가 길 때 매개변수별 줄바꿈 같은 것들이에요. 코드의 의미는 전혀 바꾸지 않으면서 시각적인 형태만 통일해줍니다.
설치하기
rustup으로 Rust를 설치했다면 rustfmt가 이미 포함되어 있을 가능성이 높습니다. 확인해볼게요.
rustup component list --installed | grep rustfmt
rustfmt-x86_64-unknown-linux-gnu
출력이 나오지 않는다면 아직 설치되지 않은 거예요. 다음 명령어로 추가하면 됩니다.
rustup component add rustfmt
이제 cargo fmt --version으로 버전을 확인할 수 있습니다.
cargo fmt --version
rustfmt 1.8.0-stable (f7651171 2025-04-17)
rustup을 통해 설치하면 Rust 툴체인을 업데이트할 때 rustfmt도 함께 업데이트되니까 별도로 버전 관리를 할 필요가 없어요.
기본 사용법
프로젝트 루트에서 다음 명령어를 실행하면 현재 패키지의 모든 Rust 소스 파일이 포매팅됩니다.
cargo fmt
아무 출력 없이 끝나면 포매팅이 완료된 거예요. 변경된 파일이 있는지 확인하려면 git diff를 보면 됩니다.
실제로 어떤 변화가 생기는지 예제로 살펴볼게요. 이런 코드가 있다고 해봅시다.
use std::collections::HashMap;
use std::io;
use std::fs;
fn process_data(input:&str, verbose:bool)->Result<HashMap<String,Vec<i32>>,io::Error>{
let mut map=HashMap::new();
if verbose{
println!("Processing: {}",input);
}
let contents=fs::read_to_string(input)?;
for line in contents.lines(){
let parts:Vec<&str>=line.split(',').collect();
if parts.len()>=2{
let key=parts[0].to_string();
let value:i32=parts[1].parse().unwrap_or(0);
map.entry(key).or_insert_with(Vec::new).push(value);
}
}
Ok(map)
}
cargo fmt를 실행하면 이렇게 정리됩니다.
use std::collections::HashMap;
use std::fs;
use std::io;
fn process_data(
input: &str,
verbose: bool,
) -> Result<HashMap<String, Vec<i32>>, io::Error> {
let mut map = HashMap::new();
if verbose {
println!("Processing: {}", input);
}
let contents = fs::read_to_string(input)?;
for line in contents.lines() {
let parts: Vec<&str> = line.split(',').collect();
if parts.len() >= 2 {
let key = parts[0].to_string();
let value: i32 = parts[1].parse().unwrap_or(0);
map.entry(key).or_insert_with(Vec::new).push(value);
}
}
Ok(map)
}
use 문이 알파벳순으로 정렬되었고, 연산자와 콜론 주변에 공백이 추가되었습니다. 함수 시그니처가 길어서 매개변수별로 줄바꿈이 되었고, 후행 쉼표도 자동으로 붙었네요. if 뒤에 공백이 들어가고 들여쓰기도 통일되었습니다.
포맷 검사 모드
코드를 실제로 수정하지 않고 포매팅이 맞는지만 확인하고 싶을 때가 있어요. CI에서 포매팅 검사를 돌리거나 커밋 전에 한번 체크해보고 싶을 때죠.
cargo fmt -- --check
모든 파일이 이미 올바르게 포매팅되어 있으면 아무 출력 없이 종료 코드 0을 반환합니다. 포매팅이 필요한 파일이 있으면 diff를 보여주고 종료 코드 1을 반환하고요.
Diff in /home/user/my-project/src/main.rs at line 5:
-fn hello( name:&str ){
+fn hello(name: &str) {
println!("Hello, {}!", name);
}
이 모드는 파일을 건드리지 않으니까 안심하고 사용할 수 있습니다.
특정 파일만 포매팅하기
프로젝트 전체가 아니라 특정 파일만 포매팅하고 싶다면 rustfmt를 직접 호출하면 됩니다.
rustfmt src/main.rs
여러 파일을 한번에 지정할 수도 있어요.
rustfmt src/main.rs src/lib.rs src/utils.rs
cargo fmt는 내부적으로 rustfmt를 호출하는 래퍼인데, Cargo가 프로젝트 구조를 파악해서 모든 소스 파일을 자동으로 찾아주는 게 차이점이에요. 보통은 cargo fmt를 쓰는 게 편하지만 특정 파일만 빠르게 정리하고 싶을 때는 rustfmt를 직접 쓰는 것도 방법입니다.
rustfmt.toml로 설정 커스터마이징하기
기본 설정만으로도 충분하지만 프로젝트나 팀의 선호에 맞게 조정하고 싶을 수 있습니다. 프로젝트 루트에 rustfmt.toml 또는 .rustfmt.toml 파일을 만들면 됩니다.
max_width = 100
tab_spaces = 4
edition = "2021"
자주 쓰이는 설정 옵션을 살펴볼게요.
우선 max_width는 한 줄의 최대 너비를 지정합니다. 기본값은 100인데, 모니터가 넓다면 120으로 늘리기도 하고 좁은 환경을 고려해 80으로 줄이기도 해요. 이 값을 넘어가면 rustfmt가 자동으로 줄바꿈을 합니다.
tab_spaces는 들여쓰기에 사용할 공백 수예요. 기본값은 4칸인데, 2칸을 선호하는 팀도 있죠.
edition은 Rust 에디션을 지정합니다. Cargo.toml의 에디션과 맞춰주면 해당 에디션에 맞는 포매팅 규칙이 적용돼요.
use_small_heuristics는 줄바꿈 관련 휴리스틱을 한꺼번에 조절하는 옵션입니다. "Default"가 기본값이고, "Max"로 설정하면 가능한 한 줄에 많이 넣으려 하며, "Off"로 설정하면 적극적으로 줄바꿈을 합니다.
좀 더 구체적인 설정도 가능합니다. 실제 프로젝트에서 쓸 만한 설정 예시를 하나 보여드릴게요.
# 한 줄 최대 너비
max_width = 100
# 들여쓰기 4칸
tab_spaces = 4
# Rust 2021 에디션
edition = "2021"
# 함수 인자가 길면 블록 스타일로 줄바꿈
fn_params_layout = "Tall"
# 단일 표현식 함수는 한 줄로
fn_single_line = true
# use 그룹 간 빈 줄 유지
imports_granularity = "Module"
group_imports = "StdExternalCrate"
imports_granularity는 use 문을 어느 수준까지 합칠지 결정합니다. "Module"로 설정하면 같은 모듈의 아이템들을 하나의 use 문으로 묶어주고요. group_imports를 "StdExternalCrate"로 설정하면 표준 라이브러리, 외부 크레이트, 로컬 모듈 순서로 그룹을 나누고 그룹 사이에 빈 줄을 넣어줍니다.
use std::collections::HashMap;
use std::io;
use serde::{Deserialize, Serialize};
use tokio::fs;
use crate::config::Settings;
use crate::error::AppError;
이렇게 출처별로 깔끔하게 분리되니까 코드를 읽을 때 어떤 의존성을 쓰고 있는지 한눈에 파악할 수 있어요.
Nightly 전용 옵션
rustfmt의 설정 옵션 중 일부는 nightly 툴체인에서만 사용할 수 있습니다. 앞서 소개한 imports_granularity, group_imports, fn_single_line 같은 옵션이 여기에 해당해요.
nightly 전용 옵션을 사용하려면 nightly 툴체인으로 실행해야 합니다.
cargo +nightly fmt
또는 프로젝트에서 항상 nightly rustfmt를 사용하고 싶다면 rust-toolchain.toml에서 rustfmt 컴포넌트만 nightly로 지정할 수 있어요.
[toolchain]
channel = "stable"
components = ["rustfmt"]
그리고 rustfmt.toml에서 nightly 옵션을 사용하려면 파일 맨 위에 이 한 줄을 추가하면 됩니다.
unstable_features = true
stable 툴체인에서 nightly 전용 옵션이 포함된 rustfmt.toml을 사용하면 에러가 발생하니까 팀 전체가 동의한 경우에만 nightly 옵션을 쓰는 게 좋아요.
특정 코드 포매팅 건너뛰기
가끔은 rustfmt의 자동 포매팅이 오히려 가독성을 해치는 경우가 있어요. 행렬 데이터를 정렬해둔 코드나 테이블 형태로 맞춰놓은 상수 정의 같은 경우죠.
이럴 때는 #[rustfmt::skip] 어트리뷰트를 사용하면 해당 항목을 포매팅에서 제외할 수 있습니다.
#[rustfmt::skip]
const TRANSFORM: [[f64; 4]; 4] = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
함수나 impl 블록 전체를 건너뛰고 싶을 때도 마찬가지로 어트리뷰트를 붙이면 돼요.
#[rustfmt::skip]
fn carefully_aligned_function() {
let x = 1;
let xy = 2;
let xyz = 3;
let xyzw = 4;
}
매크로 호출 안쪽을 건너뛰려면 #[rustfmt::skip::macros(macro_name)]을 모듈 레벨에서 사용합니다.
#![rustfmt::skip::macros(html)]
fn render() {
html! {
<div class="container">
<h1>{ "Hello" }</h1>
</div>
}
}
다만 #[rustfmt::skip]을 남용하면 코드 스타일이 일관성을 잃게 되니까 정말 필요한 경우에만 쓰는 게 좋습니다.
CI에서 활용하기
cargo fmt의 진가는 CI에 연동했을 때 나타납니다. PR마다 포매팅 검사를 자동으로 돌리면 스타일이 안 맞는 코드가 머지되는 걸 아예 막을 수 있으니까요.
GitHub Actions를 사용한다면 이런 워크플로우를 추가하면 됩니다.
name: Format Check
on:
pull_request:
push:
branches: [main]
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt -- --check
cargo fmt -- --check는 포매팅이 안 된 코드가 있으면 종료 코드 1을 반환하기 때문에 CI가 자동으로 실패 처리를 해줍니다.
nightly 전용 옵션을 사용하고 있다면 툴체인을 nightly로 바꿔주면 돼요.
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- run: cargo +nightly fmt -- --check
CI에서 포매팅 검사가 실패하면 개발자가 로컬에서 cargo fmt를 실행하고 다시 푸시하면 됩니다. 이 흐름이 반복되다 보면 자연스럽게 커밋 전에 cargo fmt를 돌리는 습관이 생기게 돼요.
Git 훅으로 자동화하기
커밋할 때마다 수동으로 cargo fmt를 실행하는 게 번거롭다면 Git 훅을 설정해서 커밋 전에 자동으로 포매팅 검사를 돌릴 수 있습니다.
.git/hooks/pre-commit 파일을 만들어서 실행 권한을 주면 됩니다.
#!/bin/sh
cargo fmt -- --check
if [ $? -ne 0 ]; then
echo "코드 포매팅이 필요합니다. 'cargo fmt'를 실행해주세요."
exit 1
fi
이러면 포매팅이 안 된 상태에서는 커밋이 거부되니까, CI에서 실패하는 일을 미리 방지할 수 있어요.
clippy와의 차이점
Rust 개발에서 cargo fmt와 함께 자주 언급되는 도구가 cargo clippy인데 둘은 역할이 다릅니다.
cargo fmt는 코드의 시각적 형태를 정리하는 포매터입니다. 들여쓰기, 공백, 줄바꿈 같은 스타일만 건드리고 코드의 동작에는 영향을 주지 않아요. “이 코드가 어떻게 보이느냐”에 집중하는 거죠.
반면 cargo clippy는 린터(linter)예요. 코드의 논리적 문제나 관용적이지 않은 패턴, 잠재적 버그 등을 찾아서 개선 방안을 제안합니다. “이 코드가 올바르고 효율적인가”에 집중하는 거예요.
예를 들어 if x == true라는 코드가 있다면, cargo fmt는 공백만 맞추고 넘어가지만 cargo clippy는 “그냥 if x로 쓰세요”라고 알려줍니다.
CI에서는 보통 둘 다 함께 돌립니다.
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- run: cargo fmt -- --check
- run: cargo clippy -- -D warnings
cargo fmt로 스타일을 통일하고 cargo clippy로 코드 품질을 높이는 식이에요. 둘은 서로 보완하는 관계입니다.
에디터 연동
매번 터미널에서 cargo fmt를 실행하는 것보다 저장할 때 자동으로 포매팅되면 훨씬 편하겠죠? 대부분의 에디터에서 이 기능을 지원합니다.
VS Code에서는 rust-analyzer 확장을 설치하면 저장 시 자동 포매팅을 사용할 수 있습니다. settings.json에 다음을 추가하면 돼요.
{
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true
}
}
Neovim에서 rust-analyzer LSP를 사용하고 있다면 저장 시 포매팅을 설정할 수 있고, IntelliJ 기반 IDE에서는 Rust 플러그인이 내장 포매터로 rustfmt를 지원합니다.
에디터에서 저장 시 자동 포매팅을 켜두면, 코드를 작성하면서 자연스럽게 포매팅이 적용되니까 커밋 전에 cargo fmt를 따로 돌릴 필요가 거의 없어집니다.
실무 팁
실제 프로젝트에서 cargo fmt를 쓰면서 알게 된 팁을 공유할게요.
프로젝트 초기에 rustfmt.toml을 확정하세요. 프로젝트 중간에 설정을 바꾸면 모든 파일에 대량 변경이 생겨서 git blame이 무용지물이 됩니다. 기존 프로젝트에 rustfmt를 도입하려면 포매팅 전용 커밋을 하나 만들고 .git-blame-ignore-revs 파일에 해당 커밋 해시를 기록해두면 blame이 깨지는 걸 방지할 수 있어요.
# rustfmt 최초 적용
a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
git config blame.ignoreRevsFile .git-blame-ignore-revs
rustfmt.toml은 가능한 한 간결하게 유지하세요. 기본 설정이 Rust 커뮤니티의 표준이기 때문에 다른 프로젝트에서 온 기여자도 별도 설정 없이 바로 기여할 수 있습니다. 커스터마이징은 정말 필요한 옵션만 추가하는 게 좋아요.
Cargo 워크스페이스에서는 루트에 rustfmt.toml을 하나만 두세요. rustfmt는 포매팅할 파일의 디렉토리에서부터 위로 올라가며 설정 파일을 찾는데, 패키지마다 다른 설정을 두면 혼란스러워집니다. 워크스페이스 전체에 일관된 스타일을 적용하는 게 관리하기 편해요.
그리고 cargo fmt와 cargo clippy는 CI에서 빌드와 테스트보다 먼저 실행하세요. 포매팅이나 린트에서 걸리면 빌드를 돌릴 필요도 없으니까 빠르게 피드백을 줄 수 있습니다.
마치며
cargo fmt는 Rust 개발에서 코드 스타일에 대한 고민을 없애주는 도구입니다. 한번 설정해두면 팀 전체가 일관된 스타일로 코드를 작성하게 되고 코드 리뷰에서 스타일 관련 코멘트가 사라지니까 더 의미 있는 논의에 집중할 수 있어요.
CI에서 cargo fmt -- --check를 돌리고 에디터에서 저장 시 자동 포매팅을 켜두면 포매팅에 대해서는 아예 신경을 끌 수 있습니다. cargo clippy와 함께 쓰면 스타일과 코드 품질 양쪽을 모두 자동으로 관리할 수 있고요. Rust 생태계의 다른 도구 활용법이 궁금하다면 cargo semver-checks로 API 호환성을 검사하는 방법도 살펴보세요.
설정 옵션 전체 목록은 rustfmt 공식 문서에서 확인할 수 있습니다.
This work is licensed under
CC BY 4.0