Django에서 gettext vs gettext_lazy 헷갈림 끝내기 (평가 시점으로 이해하기)

Django i18n을 쓰다 보면 gettext()gettext_lazy() 중 뭘 써야 하는지 매번 흔들립니다. 대부분의 혼란은 “둘의 차이”를 용어로 외우려 해서 생깁니다.

핵심은 딱 하나입니다.

  • gettext는 지금 번역한다 (즉시 평가, eager)
  • gettext_lazy는 나중에 번역한다 (지연 평가, lazy)

이 “평가 시점” 하나로 거의 모든 케이스가 정리됩니다.


번역이 “언제” 결정되는지가 왜 중요할까?



Django는 보통 요청(request)마다 언어가 바뀝니다.

Django에서 번역시점을 비교하는 플로우

  • 미들웨어가 요청을 보고 activate("ko") / activate("en") 같은 걸로 현재 스레드/컨텍스트의 언어를 활성화
  • 템플릿 렌더링, 폼 렌더링, admin 화면 렌더링 시점에 그 언어로 번역이 되어야 함

즉, 문자열을 “언제” 번역해버리느냐가 결과를 갈라요.


gettext() : “지금” 번역해 문자열을 반환

from django.utils.translation import gettext as _

def view(request):
    message = _("Welcome")   # 이 줄이 실행되는 순간의 활성 언어로 번역됨
    return HttpResponse(message)
  • 함수/뷰 내부처럼 요청 처리 중(runtime) 에 호출하면 대체로 기대한 대로 동작합니다.
  • 하지만 모듈 import 시점에 호출하면 문제가 생깁니다.

흔한 함정: 모듈 상수에서 gettext() 쓰기

# app/constants.py
from django.utils.translation import gettext as _

WELCOME = _("Welcome")  # ❌ 서버 시작/모듈 import 시점의 언어로 "고정"될 수 있음

이 경우 WELCOME는 “나중에” 언어가 바뀌어도 이미 번역된 문자열로 고정돼버립니다(특히 import가 한 번만 일어나는 환경에서).


gettext_lazy() : “나중에” 번역될 프록시(Proxy)를 반환



from django.utils.translation import gettext_lazy as _

WELCOME = _("Welcome")  # ✅ 실제 문자열이 아니라, 필요할 때 번역되는 객체

gettext_lazy()가 돌려주는 건 보통 “lazy object(프록시)”입니다.

  • 폼/템플릿/admin이 값을 문자열로 렌더링하는 시점
  • 그때 활성화된 언어로 번역됩니다.

한 줄 요약: “렌더링 시점에 언어가 정해지는 곳”에서는 lazy가 정답인 경우가 많다.


어디에 뭘 쓰면 되나: 실전 규칙

1) “지금 당장 화면/응답을 만들고 있다” → gettext

  • 뷰/서비스 로직에서 즉시 문자열을 만들어 응답하거나 로그를 찍는 경우
from django.utils.translation import gettext as _

def signup_done(request):
    return JsonResponse({"message": _("Signup completed.")})

2) “클래스 속성/모델 메타/폼 정의처럼, import 시점에 평가될 수 있다” → gettext_lazy

  • 모델의 verbose_name, help_text
  • 폼 필드의 label, help_text
  • DRF serializer 필드의 label (비슷한 이유)
  • admin의 list_display 설명 등 “정의는 미리, 렌더링은 나중”인 곳
from django.db import models
from django.utils.translation import gettext_lazy as _

class Article(models.Model):
    title = models.CharField(_("title"), max_length=100)  # verbose_name 위치
    status = models.CharField(
        _("status"),
        max_length=20,
        choices=[
            ("draft", _("Draft")),
            ("published", _("Published")),
        ],
        help_text=_("Visibility of the article."),
    )

3) “모듈 레벨 상수/choices 같은 재사용 값” → 보통 gettext_lazy

  • 재사용되는 상수는 특히 lazy로 두는 게 안전합니다.
from django.utils.translation import gettext_lazy as _

STATUS_CHOICES = [
    ("draft", _("Draft")),
    ("published", _("Published")),
]

4) “외부 시스템으로 보내는 문자열(로그/서드파티 API/헤더 등)” → gettext 또는 lazy를 강제 평가

lazy 객체를 그대로 넘기면 예상치 못한 타입/직렬화 문제를 만들 수 있습니다.

from django.utils.translation import gettext_lazy as _
from django.utils.encoding import force_str

msg = _("Welcome")
logger.info(force_str(msg))  # ✅ 실제 문자열로 변환해서 사용

5) “문자열 합치기/포매팅이 포함된다” → lazy 전용 도구도 고려

lazy 메시지에 f-string/str.format()을 섞으면 평가 시점이 꼬일 수 있습니다. Django는 이를 위해 format_lazy를 제공합니다.

from django.utils.translation import gettext_lazy as _
from django.utils.text import format_lazy

title = format_lazy("{}: {}", _("Error"), _("Invalid token"))

또는 파이썬 % 포맷을 쓰면 번역 문자열 관리가 편합니다.

from django.utils.translation import gettext as _

message = _("Hello, %(name)s!") % {"name": user.username}

가장 많이 하는 실수 3가지

실수 1) 모듈 import 시점에 gettext()로 “고정 번역” 만들어버리기

  • “상수/choices”가 있으면 우선 lazy를 의심하세요.

실수 2) lazy 객체를 JSON 직렬화/로그에 그대로 넣기

  • force_str()로 문자열로 바꾼 뒤 사용하세요.

실수 3) f-string으로 번역 문자열을 조립하기

# ❌ 비추천
_("Hello") + f" {user.username}"
  • 번역 단위가 쪼개지고(언어마다 어순이 달라짐), 평가 시점도 더 복잡해집니다.
  • 번역 문자열 안에서 변수 치환하세요.
# ✅ 추천
_("Hello, %(name)s!") % {"name": user.username}

헷갈림을 줄이는 Tips

헷갈림을 줄이는 가장 효과적인 방법은 파일 성격에 따라 _의 의미를 통일하는 겁니다.

  • models.py, forms.py, admin.py 같이 “정의가 먼저인 파일”: from django.utils.translation import gettext_lazy as _
  • views.py, services.py 같이 “실행이 먼저인 파일”: from django.utils.translation import gettext as _

이렇게 하면 “여기서는 lazy가 기본”이라는 기준이 생겨서 실수가 줄어듭니다.


간단 정리

  • 즉시 평가가 안전한 곳(런타임 로직): gettext
  • 나중에 렌더링될 것(정의/메타/상수/choices/라벨): gettext_lazy
  • 외부로 나가는 문자열: 필요하면 force_str
  • lazy + 포매팅: format_lazy 또는 번역 문자열 내부에서 치환

이 기준만 잡히면 “개념이 정립되지 않아서 헷갈리는” 상태에서 벗어나, 거의 자동으로 선택할 수 있습니다.


관련 글 링크
- gettext_lazy를 JSON 키에 쓰면 생기는 문제와 해결 방법