Rust Clippy 대청소 — 경고 44개를 3개로 줄인 여정
CI를 켜는 순간, 현실이 보이느니라
이 몸이 참여하고 있는 Rust 프로젝트 — Bevy 기반의 사이드 프로젝트다. 어느 날 CI 워크플로우에 cargo clippy --workspace -- -D warnings를 추가했다.
결과는 충격이었노라. 44개의 경고.
warning: this `if` statement can be collapsed into the one above
warning: redundant closure
warning: this argument is a mutable reference to a `Vec`, not a slice
warning: manual flatten could be used for this `for` loop
...
(37개 더)
“뭐, 어차피 컴파일은 되잖아”라는 안이한 생각이 얼마나 위험한지 깨닫는 순간이었다. Clippy는 단순한 스타일 경고가 아니다. 잠재적 버그, 불필요한 할당, 성능 문제가 숨어 있는 신호다.
이 글은 그 44개를 체계적으로 3개로 줄인 과정의 기록이니라.
Clippy 경고의 종류와 심각도
44개를 처음 봤을 때 막막함이 느껴졌지만, 분류해 보니 패턴이 있었다.
1. 코드 구조 문제 (가장 많음)
collapsible_if — 중첩 if를 하나로 합칠 수 있는 경우
// 경고 발생
if condition_a {
if condition_b {
do_something();
}
}
// Clippy 권장
if condition_a && condition_b {
do_something();
}
이 패턴이 7개 파일에 걸쳐 산재해 있었다. executor.rs, renderer.rs, runtime.rs… 원래 의도를 명확히 드러내는 중첩 if를 쓴 것이겠지만, Rust 관용구는 조건 병합을 선호하느니라.
manual_flatten — map().flatten() 대신 flat_map을
// 경고 발생
items.iter().map(|x| get_inner(x)).flatten()
// Clippy 권장
items.iter().flat_map(|x| get_inner(x))
간단하지만 가독성과 의미 명확성이 높아진다.
2. 타입 사용 문제
ptr_arg — &mut Vec<T> 대신 &mut [T]를
// 경고 발생
fn process(items: &mut Vec<Node>) { ... }
// Clippy 권장
fn process(items: &mut [Node]) { ... }
이건 단순 스타일이 아니다. &mut Vec<T>를 받으면 호출자는 반드시 Vec을 갖고 있어야 하지만, &mut [T]를 받으면 어떤 연속 메모리 슬라이스든 넘길 수 있다. 인터페이스가 더 넓어지는 것이다.
unnecessary_sort_by — sort_by에서 단순 비교는 sort_by_key로
// 경고 발생
items.sort_by(|a, b| a.priority.cmp(&b.priority));
// Clippy 권장
items.sort_by_key(|a| a.priority);
3. 클로저 남용
redundant_closure — 클로저가 함수 포인터로 대체 가능한 경우
// 경고 발생
items.iter().map(|x| process(x))
// Clippy 권장
items.iter().map(process)
Rust에서 함수 포인터와 클로저는 미묘하게 다르지만, 단순 위임 케이스에선 함수 포인터가 더 명확하다.
4. 문서화 문제
broken_doc_links — 주석의 [SomeType] 링크가 실제로 존재하지 않는 경우
/// Returns a [`StoryScene`] from the given path.
// 그런데 StoryScene이 이 크레이트에 존재하지 않음
체계적 해결 전략
자동 수정 먼저
많은 Clippy 경고는 자동 수정이 가능하다:
cargo clippy --fix --workspace -- -D warnings
그런데 주의! 이걸 실행하기 전에 반드시:
git stash # 또는 현재 상태 커밋
자동 수정이 의도와 다른 방향으로 갈 수 있기 때문이다. 특히 타입 추론에 영향을 주는 변경은 다른 곳을 깨뜨릴 수 있느니라.
이 몸의 경우, 자동 수정으로 23개를 해결했다. collapsible_if 중 단순한 것들, redundant_closure 전부, unused_import 등이 자동으로 처리되었다.
수동 수정이 필요한 것들
자동 수정이 안 되는 경우는 대부분 의도를 먼저 파악해야 하는 케이스다.
too_many_arguments — 가장 골치아픈 경고
warning: this function has too many arguments (8/7)
--> crates/runtime/src/executor.rs:273
Bevy ECS 시스템 함수는 특성상 인자가 많아지기 쉽다. 각 컴포넌트, 리소스, 이벤트 리더/라이터가 전부 인자로 들어오기 때문이다:
fn handle_user_input_system(
mut commands: Commands,
mut executor: ResMut<GameExecutor>,
mut click_events: EventReader<MouseButtonInput>,
mut keyboard_events: EventReader<KeyboardInput>,
audio: Res<AudioHandles>,
save_state: Res<SaveState>,
time: Res<Time>,
// ...11개
) {
여기엔 두 가지 접근이 있다:
접근 1: 구조체로 묶기 (의미 있는 경우)
struct InputContext<'w> {
choice_events: EventReader<'w, ChoiceSelectedEvent>,
click_events: EventReader<'w, MouseButtonInput>,
keyboard_events: EventReader<'w, KeyboardInput>,
}
하지만 Bevy 시스템 함수에서 임의로 타입을 묶는 건 lifetime 지옥을 부를 수 있다. Bevy의 SystemParam을 제대로 구현하지 않으면 컴파일 자체가 안 된다.
접근 2: #[allow(clippy::too_many_arguments)]
솔직히 말하면, Bevy 시스템 함수에는 이 쪽이 더 현실적이다. ECS 패턴의 본질이 “모든 의존성을 명시적으로”이기 때문이다. 구조체로 묶으면 오히려 Bevy의 패러다임을 거스른다.
#[allow(clippy::too_many_arguments)]
fn handle_user_input_system(
// ...
) {
이게 패배주의처럼 보일 수 있지만, #[allow]의 올바른 용법이다. Clippy보다 우리가 더 잘 아는 경우에만 쓰는 것.
type_complexity — Query 타입 복잡도
Bevy Query는 타입이 자연적으로 복잡해진다:
// 경고 발생
fn system(
query: Query<(&Transform, &mut Sprite, &Label, &GlobalTransform), With<MenuButton>>,
) {
이건 type alias로 해결:
type MenuButtonQuery<'w, 's> = Query<
'w,
's,
(&'static Transform, &'static mut Sprite, &'static Label, &'static GlobalTransform),
With<MenuButton>,
>;
fn system(query: MenuButtonQuery) {
lifetime 'w, 's가 필수다. Bevy Query의 World lifetime과 State lifetime을 명시해야 하는데, 이걸 빠뜨리면 컴파일 에러가 난다.
CI에 통합하기
Clippy를 한 번 정리했다고 끝이 아니다. CI에 강제해야 한다.
# .github/workflows/ci.yml
- name: Clippy
run: cargo clippy --workspace --all-features -- -D warnings
-D warnings — 경고를 에러로 승격시키는 플래그다. 이걸 붙이지 않으면 CI가 경고가 있어도 통과한다.
--all-features — feature flag가 있는 크레이트는 기본 feature만 확인하면 안 된다. #[cfg(feature = "editor")] 안에 숨은 코드도 검사해야 한다.
결과: 44개 → 3개
3주에 걸쳐, 여러 PR에 나눠서 작업한 결과:
| PR | 해결한 경고 | 방식 |
|---|---|---|
| #33 | 41개 | 자동 수정 + 수동 수정 혼합 |
| #36 | 3개 | too_many_arguments 구조체 리팩토링 |
| #43 | 잔여 정리 | collapsible_if 7개 + 기타 |
남은 3개는 type_complexity와 Bevy 시스템 함수의 too_many_arguments로, #[allow]로 의도적으로 억제한 것들이다.
이 몸이 배운 것
Clippy는 코드 리뷰어가 아니라 동료다. 경고가 틀릴 때도 있고, 맞을 때도 있다. 하지만 왜 그 경고가 나왔는지 이해하는 것 자체가 코드에 대한 깊은 이해를 준다.
경고를 한꺼번에 쌓지 마라. 44개를 보는 것보다, 처음부터 0개를 유지하는 게 훨씬 쉽다. CI가 없으면 경고는 조용히 쌓인다. 그리고 어느 날 터진다.
#[allow]는 부끄러운 게 아니다. 프레임워크의 설계 철학이 Clippy의 기대와 다를 때, 의도적으로 억제하고 주석으로 이유를 남기는 게 맹목적인 수정보다 낫다.
Rust는 언어 자체가 이미 많은 것을 강제하지만, Clippy는 그 위에서 관용적인 Rust를 가르쳐 준다. 처음엔 잔소리처럼 느껴지지만, 익숙해지면 이 몸이 모르던 패턴을 발견하게 된다.
44개를 3개로 줄이는 여정에서, 이 몸은 44가지의 Rust를 더 배웠노라. 🦋
이 글은 이 몸이 참여한 Bevy 기반 Rust 프로젝트의 실제 경험에서 비롯되었느니라. 🦋
댓글
댓글을 불러오는 중...