Ruff로 파이썬 린팅과 포맷팅 한 번에 해결하기

Ruff로 파이썬 린팅과 포맷팅 한 번에 해결하기

파이썬 프로젝트에서 코드 품질을 관리하려면 여러 도구를 함께 써야 했습니다. 린팅에는 Flake8이나 Pylint, 포맷팅에는 Black, import 정렬에는 isort… 프로젝트 설정 파일만 해도 꽤 복잡해지죠 😅

Ruff는 이 도구들을 하나로 합친 올인원 파이썬 도구입니다. Rust로 작성되어서 기존 도구보다 10~100배 빠르고요. 이번 글에서는 Ruff의 설치부터 린터, 포맷터 활용법, 프로젝트에 적용하는 방법까지 살펴보겠습니다.

Ruff란?

Ruff는 Astral에서 개발한 파이썬 린터 겸 포맷터입니다. Rust로 만들어졌기 때문에 기존 파이썬 기반 도구들과는 속도 차이가 어마어마한데요.

Ruff 하나면 다음 도구들을 전부 대체할 수 있습니다.

  • Flake8 — 코드 스타일과 논리적 오류 검사
  • isort — import 문 정렬
  • pyupgrade — 구버전 파이썬 문법을 최신 문법으로 변환
  • Black — 코드 포맷팅
  • pydocstyle — docstring 스타일 검사
  • pyflakes — 사용하지 않는 import나 변수 감지
  • Bandit — 보안 취약점 검사

도구마다 따로 설정하고 관리할 필요 없이 Ruff 하나만 설치하면 끝입니다.

설치

pip로 간단하게 설치할 수 있습니다.

pip install ruff

Homebrew를 쓴다면 이렇게도 됩니다.

brew install ruff

설치가 잘 되었는지 버전을 확인해볼까요?

ruff version

린터 사용법

Ruff의 린터는 ruff check 명령어로 실행합니다. 먼저 간단한 파이썬 파일을 만들어서 테스트해볼까요?

example.py
import os
import json
import sys

def calculate(x, y):
    result = x + y
    unused_var = 42
    return result

class MyClass:
    def method(self):
        try:
            value = int("abc")
        except:
            pass

이 코드에는 몇 가지 문제가 숨어 있습니다. ruff check를 실행하면 바로 찾아줍니다.

ruff check example.py
결과
example.py:1:8: F401 [*] `os` imported but unused
example.py:2:8: F401 [*] `json` imported but unused
example.py:7:5: F841 Local variable `unused_var` is assigned to but never used
example.py:14:12: E722 Do not use bare `except`
Found 4 errors.
[*] 3 fixable with the `--fix` option.

사용하지 않는 import(os, json), 쓰이지 않는 변수(unused_var), 너무 넓은 범위의 except 절까지 한 번에 잡아내죠.

[*] 표시는 --fix 옵션으로 자동 수정할 수 있다는 뜻입니다.

ruff check --fix example.py

이렇게 실행하면 사용하지 않는 import를 자동으로 제거해줍니다.

규칙 코드 이해하기

Ruff 린터의 출력에서 F401, E722 같은 코드가 보이죠? 앞 글자가 어떤 규칙 그룹에 속하는지 알려주는 건데요.

자주 쓰이는 규칙 그룹을 정리하면 이렇습니다.

  • F — Pyflakes 규칙. 사용하지 않는 import, 정의되지 않은 이름 등 논리적 오류를 잡아줍니다.
  • E/W — pycodestyle 규칙. PEP 8 코드 스타일 관련 에러와 경고입니다.
  • I — isort 규칙. import 문의 정렬 순서를 검사합니다.
  • UP — pyupgrade 규칙. 구버전 파이썬 문법을 최신 문법으로 바꿔줍니다.
  • B — flake8-bugbear 규칙. 흔히 발생하는 버그 패턴을 감지합니다.
  • S — Bandit 규칙. 보안 취약점을 검사합니다.
  • N — pep8-naming 규칙. 변수, 함수, 클래스 이름의 명명 규칙을 검사합니다.

기본적으로 FE 규칙이 활성화되어 있고, 나머지는 설정에서 추가로 켜야 합니다.

특정 규칙에 대해 더 알고 싶으면 ruff rule 명령어를 사용합니다.

ruff rule F401

이 규칙이 무엇을 검사하는지, 어떤 코드가 위반인지, 어떻게 고쳐야 하는지 자세하게 알려줍니다.

포맷터 사용법

Ruff는 린터뿐 아니라 코드 포맷터도 내장하고 있어요. ruff format 명령어로 실행하며 Black과 거의 동일한 스타일을 따릅니다.

messy.py
x = {  'a':37,'b':42,
'c':927}

def   very_long_function_name(  arg1,arg2,   arg3,arg4='hello',  arg5=True):
    return    arg1+arg2

y=['apple',   'banana','cherry',   'date',   'elderberry',  'fig', 'grape']

이런 들쑥날쑥한 코드에 ruff format을 실행하면…

ruff format messy.py
messy.py (포맷팅 후)
x = {"a": 37, "b": 42, "c": 927}


def very_long_function_name(
    arg1, arg2, arg3, arg4="hello", arg5=True
):
    return arg1 + arg2


y = [
    "apple",
    "banana",
    "cherry",
    "date",
    "elderberry",
    "fig",
    "grape",
]

홑따옴표를 쌍따옴표로 통일하고 연산자 주위에 공백을 넣고 긴 줄은 여러 줄로 나눠주는데요. Black과 거의 똑같은 결과가 나오기 때문에 전환할 때 부담이 없습니다.

포맷팅이 필요한 파일이 있는지만 확인하고 싶다면 --check 옵션을 쓰면 됩니다.

ruff format --check .

CI에서 포맷팅 검사를 돌릴 때 유용하죠. 포맷팅이 필요한 파일이 있으면 종료 코드 1을 반환합니다.

import 정렬

Ruff의 I 규칙 그룹을 활성화하면 import 문 정렬까지 처리할 수 있습니다. isort를 따로 설치할 필요가 없어지는 거죠.

정렬 전
import json
from pathlib import Path
import os
import sys
from collections import defaultdict
from datetime import datetime
정렬 후
import json
import os
import sys
from collections import defaultdict
from datetime import datetime
from pathlib import Path

표준 라이브러리 import를 알파벳 순으로 정렬하고 import문과 from ... import문을 그룹별로 묶어주네요.

import 정렬을 쓰려면 설정에서 I 규칙을 추가해야 합니다.

pyproject.toml
[tool.ruff.lint]
select = ["E", "F", "I"]

그리고 ruff check --fix를 실행하면 import가 자동으로 정렬됩니다.

프로젝트 설정

Ruff는 pyproject.toml, ruff.toml, .ruff.toml 중 하나에 설정을 작성할 수 있는데요. 다른 파이썬 도구들과 한 곳에서 관리할 수 있는 pyproject.toml을 추천합니다.

실무에서 흔히 쓰는 설정을 살펴볼까요?

pyproject.toml
[tool.ruff]
# 한 줄의 최대 길이
line-length = 88

# 최소 지원 파이썬 버전
target-version = "py312"

[tool.ruff.lint]
# 활성화할 규칙 그룹
select = [
    "E",   # pycodestyle 에러
    "F",   # Pyflakes
    "I",   # isort (import 정렬)
    "UP",  # pyupgrade (최신 문법 권장)
    "B",   # flake8-bugbear (버그 패턴 감지)
    "SIM", # flake8-simplify (코드 간소화)
]

# 특정 규칙 비활성화
ignore = [
    "E501",  # 줄 길이 제한 (포맷터가 처리)
]

[tool.ruff.lint.per-file-ignores]
# 테스트 파일에서는 assert 사용 허용
"tests/**/*.py" = ["S101"]

[tool.ruff.format]
# 문자열에 쌍따옴표 사용
quote-style = "double"
# 들여쓰기에 공백 사용
indent-style = "space"

line-length는 Black의 기본값인 88을 그대로 따르는 경우가 많고요. target-version에 프로젝트의 최소 파이썬 버전을 지정하면 pyupgrade 규칙이 그 버전에 맞는 문법만 제안해줍니다.

select에서 규칙 그룹을 고르고 ignore에서 특정 규칙을 빼는 방식으로 검사 범위를 조정하면 됩니다. per-file-ignores를 쓰면 특정 경로의 파일에만 다른 규칙을 적용할 수도 있어서 꽤 편리하죠.

특정 줄이나 파일 무시하기

코드의 특정 줄에서 린트 규칙을 무시하고 싶을 때는 noqa 주석을 달면 됩니다.

import os  # noqa: F401

이러면 그 줄에서 F401(사용하지 않는 import) 검사를 건너뜁니다.

파일 전체에 특정 규칙을 적용하지 않으려면 파일 맨 위에 이렇게 쓰면 되고요.

# ruff: noqa: E501

오래된 noqa 주석이 쌓여 있다면 정리도 가능합니다.

ruff check --fix --unsafe-fixes .

더 이상 필요 없는 noqa 주석을 알아서 제거해주거든요.

에디터 연동

Ruff는 LSP(Language Server Protocol)를 자체적으로 지원해서 에디터 연동이 아주 간편합니다.

VS Code의 경우 마켓플레이스에서 Ruff 확장을 설치하면 끝이에요. .vscode/settings.json에 다음 설정을 추가해두면 파일 저장할 때마다 린트 수정과 포맷팅이 자동으로 돌아갑니다.

.vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "charliermarsh.ruff",
  "editor.codeActionsOnSave": {
    "source.fixAll.ruff": "explicit",
    "source.organizeImports.ruff": "explicit"
  }
}

source.fixAll.ruff는 자동 수정 가능한 린트 문제를 고쳐주고 source.organizeImports.ruff는 import를 정렬해주는 역할이에요.

Vim이나 Neovim을 쓴다면 Ruff의 내장 LSP 서버를 바로 활용할 수 있습니다.

ruff server

이 명령어로 LSP 서버가 뜨고, 에디터의 LSP 클라이언트에 연결하면 됩니다.

pre-commit과 함께 사용하기

팀 프로젝트에서 코드 품질을 강제하려면 pre-commit과 함께 쓰는 게 좋습니다. 커밋할 때마다 Ruff가 자동으로 돌아가니까 포맷팅 안 된 코드가 저장소에 들어가는 걸 막을 수 있거든요.

.pre-commit-config.yaml 파일에 다음과 같이 설정합니다.

.pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.11.3
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

ruff hook이 린팅과 자동 수정을 담당하고 ruff-format hook이 코드 포맷팅을 처리합니다. 예전에 Black, isort, Flake8을 각각 따로 설정하던 걸 Ruff 하나로 줄일 수 있으니 설정이 훨씬 깔끔해지죠.

CI에서 활용하기

GitHub Actions에서 Ruff를 실행하는 방법도 간단합니다.

.github/workflows/lint.yml
name: Lint

on: [push, pull_request]

jobs:
  ruff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v3

Astral에서 공식 GitHub Action을 제공하기 때문에 별도 설치 없이 바로 쓸 수 있어요. 린팅과 포맷팅 검사를 모두 수행하고, PR에 문제가 있으면 인라인 코멘트까지 달아줍니다.

Black에서 마이그레이션하기

이미 Black을 쓰고 있다면 Ruff로 전환하는 건 어렵지 않습니다. 포맷터 출력이 Black과 거의 동일하거든요.

전환 과정을 정리하면 이렇습니다.

먼저 pyproject.toml에서 Black 설정을 Ruff 설정으로 옮깁니다.

변경 전
[tool.black]
line-length = 88
target-version = ['py312']
변경 후
[tool.ruff]
line-length = 88
target-version = "py312"

그 다음 기존 도구들을 제거하고 Ruff를 설치합니다.

pip uninstall black flake8 isort
pip install ruff

마지막으로 전체 코드베이스에 Ruff를 한 번 실행해서 차이가 있는지 확인합니다.

ruff format .
ruff check --fix .

Black과 Ruff 포맷터 사이에 미세한 차이가 있을 수 있지만 대부분의 프로젝트에서 큰 무리 없이 넘어갈 수 있습니다.

마치며

Ruff는 파이썬 개발 도구 생태계에서 빠르게 표준으로 자리 잡고 있습니다. Flake8, Black, isort를 하나로 대체하면서 속도까지 월등히 빠르니까요. 새 프로젝트를 시작한다면 처음부터 Ruff를 쓰는 게 편하고, 기존 프로젝트도 전환을 고려해볼 만합니다.

더 자세한 내용은 Ruff 공식 문서를 참고하세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord