# Django에서 `gettext` vs `gettext_lazy` 헷갈림 끝내기 (평가 시점으로 이해하기) Django i18n을 쓰다 보면 `gettext()`와 `gettext_lazy()` 중 뭘 써야 하는지 매번 흔들립니다. 대부분의 혼란은 “둘의 차이”를 용어로 외우려 해서 생깁니다. 핵심은 딱 하나입니다. * **`gettext`는 지금 번역한다 (즉시 평가, eager)** * **`gettext_lazy`는 나중에 번역한다 (지연 평가, lazy)** 이 “평가 시점” 하나로 거의 모든 케이스가 정리됩니다. --- ## 번역이 “언제” 결정되는지가 왜 중요할까? {#sec-cd5e4068943f} Django는 보통 요청(request)마다 언어가 바뀝니다. ![Django에서 번역시점을 비교하는 플로우](/media/editor_temp/6/29f9dbe8-44b7-47ed-bef6-4cc0cc62bb79.png) * 미들웨어가 요청을 보고 `activate("ko")` / `activate("en")` 같은 걸로 **현재 스레드/컨텍스트의 언어를 활성화** * 템플릿 렌더링, 폼 렌더링, admin 화면 렌더링 시점에 그 언어로 번역이 되어야 함 즉, **문자열을 “언제” 번역해버리느냐**가 결과를 갈라요. --- ## `gettext()` : “지금” 번역해 문자열을 반환 {#sec-9ff53b7dc3ad} ```python from django.utils.translation import gettext as _ def view(request): message = _("Welcome") # 이 줄이 실행되는 순간의 활성 언어로 번역됨 return HttpResponse(message) ``` * 함수/뷰 내부처럼 **요청 처리 중(runtime)** 에 호출하면 대체로 기대한 대로 동작합니다. * 하지만 **모듈 import 시점**에 호출하면 문제가 생깁니다. ### 흔한 함정: 모듈 상수에서 `gettext()` 쓰기 {#sec-7c331db329fb} ```python # app/constants.py from django.utils.translation import gettext as _ WELCOME = _("Welcome") # ❌ 서버 시작/모듈 import 시점의 언어로 "고정"될 수 있음 ``` 이 경우 `WELCOME`는 “나중에” 언어가 바뀌어도 이미 번역된 문자열로 고정돼버립니다(특히 import가 한 번만 일어나는 환경에서). --- ## `gettext_lazy()` : “나중에” 번역될 프록시(Proxy)를 반환 {#sec-a90df9111315} ```python from django.utils.translation import gettext_lazy as _ WELCOME = _("Welcome") # ✅ 실제 문자열이 아니라, 필요할 때 번역되는 객체 ``` `gettext_lazy()`가 돌려주는 건 보통 “lazy object(프록시)”입니다. * 폼/템플릿/admin이 값을 **문자열로 렌더링하는 시점**에 * 그때 활성화된 언어로 번역됩니다. > 한 줄 요약: **“렌더링 시점에 언어가 정해지는 곳”에서는 lazy가 정답인 경우가 많다.** --- ## 어디에 뭘 쓰면 되나: 실전 규칙 {#sec-4a8a3cbed930} ### 1) “지금 당장 화면/응답을 만들고 있다” → `gettext` {#sec-197c23c83765} * 뷰/서비스 로직에서 즉시 문자열을 만들어 응답하거나 로그를 찍는 경우 ```python from django.utils.translation import gettext as _ def signup_done(request): return JsonResponse({"message": _("Signup completed.")}) ``` ### 2) “클래스 속성/모델 메타/폼 정의처럼, import 시점에 평가될 수 있다” → `gettext_lazy` {#sec-b545464ef5b7} * 모델의 `verbose_name`, `help_text` * 폼 필드의 `label`, `help_text` * DRF serializer 필드의 `label` (비슷한 이유) * admin의 `list_display` 설명 등 “정의는 미리, 렌더링은 나중”인 곳 ```python 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` {#sec-9ea28af08fa3} * **재사용되는 상수는 특히** lazy로 두는 게 안전합니다. ```python from django.utils.translation import gettext_lazy as _ STATUS_CHOICES = [ ("draft", _("Draft")), ("published", _("Published")), ] ``` ### 4) “외부 시스템으로 보내는 문자열(로그/서드파티 API/헤더 등)” → `gettext` 또는 lazy를 강제 평가 {#sec-f04525bb1d99} lazy 객체를 그대로 넘기면 예상치 못한 타입/직렬화 문제를 만들 수 있습니다. ```python from django.utils.translation import gettext_lazy as _ from django.utils.encoding import force_str msg = _("Welcome") logger.info(force_str(msg)) # ✅ 실제 문자열로 변환해서 사용 ``` ### 5) “문자열 합치기/포매팅이 포함된다” → lazy 전용 도구도 고려 {#sec-67d42a88924b} lazy 메시지에 f-string/`str.format()`을 섞으면 평가 시점이 꼬일 수 있습니다. Django는 이를 위해 `format_lazy`를 제공합니다. ```python from django.utils.translation import gettext_lazy as _ from django.utils.text import format_lazy title = format_lazy("{}: {}", _("Error"), _("Invalid token")) ``` 또는 파이썬 `%` 포맷을 쓰면 번역 문자열 관리가 편합니다. ```python from django.utils.translation import gettext as _ message = _("Hello, %(name)s!") % {"name": user.username} ``` --- ## 가장 많이 하는 실수 3가지 {#sec-273d53d27224} ### 실수 1) 모듈 import 시점에 `gettext()`로 “고정 번역” 만들어버리기 {#sec-7f4b5a25d9d0} * “상수/choices”가 있으면 우선 lazy를 의심하세요. ### 실수 2) lazy 객체를 JSON 직렬화/로그에 그대로 넣기 {#sec-7a741de5c1d8} * `force_str()`로 문자열로 바꾼 뒤 사용하세요. ### 실수 3) f-string으로 번역 문자열을 조립하기 {#sec-76316b53a754} ```python # ❌ 비추천 _("Hello") + f" {user.username}" ``` * 번역 단위가 쪼개지고(언어마다 어순이 달라짐), 평가 시점도 더 복잡해집니다. * **번역 문자열 안에서 변수 치환**하세요. ```python # ✅ 추천 _("Hello, %(name)s!") % {"name": user.username} ``` --- ## 헷갈림을 줄이는 Tips {#sec-79205bb8a9a7} 헷갈림을 줄이는 가장 효과적인 방법은 **파일 성격에 따라 `_`의 의미를 통일**하는 겁니다. * `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가 기본”이라는 기준이 생겨서 실수가 줄어듭니다. --- ## 간단 정리 {#sec-3aa43e207283} * **즉시 평가가 안전한 곳(런타임 로직)**: `gettext` * **나중에 렌더링될 것(정의/메타/상수/choices/라벨)**: `gettext_lazy` * **외부로 나가는 문자열**: 필요하면 `force_str` * **lazy + 포매팅**: `format_lazy` 또는 번역 문자열 내부에서 치환 이 기준만 잡히면 “개념이 정립되지 않아서 헷갈리는” 상태에서 벗어나, 거의 자동으로 선택할 수 있습니다. --- **관련 글 링크** - [gettext_lazy를 JSON 키에 쓰면 생기는 문제와 해결 방법](/ko/whitedec/2025/4/26/gettext-lazy-json-key-problem-solution/)