배달의민족이 다국어 지원을 위해 @lib/i18n을 도입하면서, 번역 API를 빠뜨린 한글 문자열을 사람도 AI도 안정적으로 못 잡는 문제에 부딪혔다. LLM은 확률론적이라 미탐과 오탐이 반복됐다. 탐지는 결정론적인 ESLint 규칙에, 교정은 AI에 맡기는 하이브리드로 전환해 번역 누락 182건을 운영 배포 전에 찾아낸 과정을 다룬다.
핵심 포인트- 탐지를 AI에 맡기면 미탐·오탐이 반복되고 느리며 유료다. 린트는 결정론적이고 에디터에서 즉시 피드백을 주며 무료다.
- 자동 교정은 포기했다. 린트는 위반을 찾고, 실제 문구 수정은 AI가 반자동으로 하는 역할 분담이다.
- no-literal-string은 AST를 순회하며 "깊은 안전지대 / 얕은 안전지대 / 위험지대"를 스코프 스택으로 추적해 한글 리터럴을 판정한다.
- 초기 규칙은 일부러 엄격하게 잡아 미탐을 없애고, 이후 사이클을 돌며 오탐을 Allow List로 완화했다.
- 결과: 번역 누락 182건, 기수·서수 오역 5건, 불필요한 Trans 사용 22건을 배포 전에 검거했다.
상세 정리- 지켜야 할 컨벤션 세 가지: 노출되는 한글은 반드시 t()로 감싸고, 수량은 tCardinal()·순서는 tOrdinal()을 쓰며, Trans 컴포넌트는 자식에 엘리먼트가 있을 때만 쓴다.
- 판정의 어려움: "사용자에게 노출되는가"를 알려면 코드 맥락을 깊이 이해해야 한다. AI로 기존 코드를 훑었더니 한글 탐지에서 미탐과 오탐이 함께 나왔고, 기수·서수 적합성과 Trans 자손 구조 판단도 어긋났다.
- 도구 선택의 근거: 탐지 정확도(결정론 대 확률론), 피드백 시점(에디터 즉시 대 수동 지연), 속도, 비용 네 축에서 린트가 앞섰다.
- 개발 원칙 다섯 가지: 엄격한 초기 규칙, 프로젝트 코드에서 테스트 케이스 추출, 사이클마다 점진 개선, Allow List 중심의 세밀한 옵션, AI 주도 교정.
- 규칙 1 설계: import 선언이나 번역 API 함수 내부는 내부 문자열까지 허용하는 깊은 안전지대, switch case나 객체 속성은 직접 자식만 검사하는 얕은 안전지대, 나머지는 위험지대로 나눴다.
- 규칙 1 구현: AST 순회 중 안전·위험·더미 플래그를 스코프 스택에 쌓고, 문자열 노드에 닿으면 스택을 역순으로 확인한 뒤 부모 노드와 한글 여부, 타입 예외를 차례로 통과시켜 최종 위반을 보고한다.
- 규칙 1 특례: 한글로 된 타입명의 문자열 리터럴은 실제 노출 가능성이 없어 허용 대상으로 뒀다.
- 규칙 2 설계: 수량·순서 표현을 잡기 위해 속성명 키워드(count·score·point·day)와 단위 키워드(개·점·일·번째)를 휴리스틱으로 매칭한다.
- 규칙 2 검사 범위: 함수 호출은 첫 인자의 보간과 단위 키워드, 두 번째 인자 객체의 속성명을 보고, Trans 컴포넌트는 자손 문자열과 values·count prop, JSX 표현식 내 객체 속성까지 확인한다.
- 규칙 3: Trans 컴포넌트에 JSXElement인 자식이 없으면 위반으로 보고 t() 사용을 권한다. 로직 자체는 단순하다.
- 반복 사이클: AI가 테스트 케이스를 모으고 탐지 로직을 개선하면, 린트 에러를 정탐·오탐으로 분류하고, 사람이 오탐을 분석해 케이스를 보충한 뒤 다음 사이클로 넘어간다.
- 부수 효과: no-literal-string이 includes('한글') 같은 분기 로직의 한글까지 잡아내, 다른 언어 환경에서 터질 수 있는 잠재 버그를 미리 막았다.
- 트레이드오프: 키워드 매칭이라 드물게 오탐이 나는데, 탐지 로직을 정교하게 만들수록 유지보수 부담이 커진다. 저자는 특례를 추가하거나 린트를 부분 비활성화하는 쪽을 택했다.
- 정리한 원칙: AI는 자연어 교정, 린트는 결정론적 탐지, 사람은 오탐 분석과 예외 처리를 맡는다. 위반의 형태와 교정 방법에서 패턴을 뽑아 각자 잘하는 일을 배분하는 것이 핵심이다.