조용한 등록 킬러: 자동 포매터와 린터가 충돌할 때
Source: Dev.to
번역할 전체 텍스트를 제공해 주시겠어요?
코드 블록, URL 및 마크다운 형식은 그대로 유지하면서 본문만 한국어로 번역해 드리겠습니다.
예상치 못한 등록 버그
기능이 배포되었고, 수동 테스트는 “괜찮은 편”으로 보였으며 코드는 스테이징으로 이동했습니다.
그 직후, 사용자들이 등록 엔드포인트에 접근했을 때 일반적인 500 Internal Server Error를 받았습니다.
서버 로그에는 다음과 같이 표시되었습니다:
TypeError: CustomUser() got unexpected keyword arguments: 'password2'
코드는 처음 보기엔 올바른 것처럼 보였지만, 일련의 “도움이 되는” 도구들이 미묘한 버그를 도입한 것이었습니다.
포맷팅 실수로 인한 TypeError 발생
프로젝트에서는 회원가입에 ModelSerializer를 사용하고 “비밀번호 확인” 검증을 위해 password2 필드를 추가했습니다. 정리 작업이나 자동 포맷팅을 수행하는 과정에서, 들어오는 데이터에서 password2를 제거하던 줄이 실수로 위치가 바뀌거나 주석 처리되었습니다.
def create(self, validated_data):
# Extract the main password
password = validated_data.pop("password")
# Oops – `password2` was left in the dictionary!
# validated_data might look like:
# {"email": "user@example.com", "password2": "secret123"}
user = User.objects.create_user(**validated_data) # <-- crash point
user.set_password(password)
user.save()
return user
validated_data를 그대로 create_user에 전달했기 때문에 Django는 User 모델에 존재하지 않는 password2를 키워드 인자로 전달하려고 시도했고, 그 결과 TypeError가 발생했습니다. UI에는 모호한 “Server Error”만 표시되어 사용자들은 막히게 되었습니다.
Linter 충돌
이러한 “더러운” 딕셔너리를 잡기 위해 팀은 pre‑commit 훅을 통해 Ruff를 도입했습니다. 개발자가 변수를 pop 해서 오류를 수정하려고 시도했을 때, Ruff는 다음과 같이 불평했습니다:
password2 = validated_data.pop("password2")
F841 Local variable 'password2' is assigned to but never used
본능적인 반응은 해당 규칙을 무시하는 것이었습니다:
- id: ruff
args: ["--fix", "--unfixable=F841"]
하지만 --unfixable는 Ruff가 자동으로 문제를 수정하는 것을 막을 뿐이며, 오류를 무시하지 않으므로, pre‑commit 훅은 여전히 실패했습니다.
사용되지 않은 데이터를 팝하는 올바른 방법
“Dirty” Fix (F841 트리거)
password2 = validated_data.pop("password2")
Clean Fix (변수 할당 없음 → 린터 오류 없음)
validated_data.pop("password2", None)
Pythonic Fix (언더스코어가 의도적으로 사용되지 않는 변수임을 표시)
_password2 = validated_data.pop("password2")
언더스코어 접두사는 독자와 린터 모두에게 해당 변수가 의도적으로 무시된다는 것을 알려줍니다.
Django 프로젝트를 위한 권장 Ruff 설정
린터와 싸우기보다, 린터가 여러분과 함께 작동하도록 설정하세요. 아래는 엄격함과 Django‑특화 현실을 균형 있게 맞춘 검증된 ruff.toml입니다.
# ruff.toml
target-version = "py312"
line-length = 88
[lint]
# Enable a wide range of rules
select = [
"F", # Pyflakes (unused variables, etc.)
"E", # Error (standard style)
"W", # Warning
"I", # Isort (import ordering)
"DJ", # Django‑specific rules
"B", # Bugbear (common design flaws)
"SIM", # Simplicity (cleaner code)
]
# Ignore rules that are too noisy for the team
ignore = [
"DJ001", # Direct User imports sometimes necessary in managers
]
[lint.per-file-ignores]
# Ignore unused imports in package initializers
"__init__.py" = ["F401"]
# Ignore line‑length and unused‑import warnings in auto‑generated migrations
"**/migrations/*" = ["E501", "F401"]
[format]
quote-style = "double"
indent-style = "space"
추가 Django 팁
모델을 참조할 때는 구체적인 User 클래스를 직접 임포트하는 대신 get_user_model()(또는 'myapp.CustomUser'와 같은 문자열 참조)를 항상 사용하세요. 이렇게 하면 순환 임포트와 하드코딩을 방지할 수 있습니다.
핵심 요점
- Linting은 방호 레일이며, 장애물이 아닙니다. Ruff가 사용되지 않은 변수를 표시할 때, 이는 종종 하위 로직을 깨뜨릴 수 있는 “쓰레기” 데이터를 경고하는 것입니다.
- 할당 없이 사용되지 않은 키를 팝하기 (
validated_data.pop("password2", None))는 린터를 만족시키고TypeError를 방지합니다. - 린터를 무시하기보다 설정하십시오; 잘 조정된
ruff.toml은 Django의 특성을 고려하면서 코드베이스를 깔끔하게 유지합니다. - 명시적인 것이 암시적인 것보다 낫습니다. 명확한 데이터 처리로 함수가 순수해지고, 로그가 깔끔해지며, 향후 유지보수가 쉬워집니다.
깨끗한 팝 패턴과 합리적인 Ruff 설정을 적용함으로써 팀은 회원가입 충돌을 제거하고 린터를 만족시켰으며 원활한 사용자 경험을 복원했습니다.