여는 글
대 AI의 시대를 맞이하며 배달의민족 앱에 다국어 지원의 바람이 불었습니다. AI의 빠른 번역 속도는 몇 년 동안 잊혀진 다국어 지원의 실현을 앞당겼고, B마트와 장보기쇼핑을 비롯한 커머스 서비스도 예외는 아니었습니다.
이에 맞춰 커머스 웹프론트에서는 한글 문구를 체계적으로 관리 및 번역하고자, 유명한 다국어 프레임워크 i18next와 react-i18next 생태계에 기반한 커스텀 라이브러리 @lib/i18n을 구현했습니다. 그리고 빈틈없는 번역과 개발 생산성을 위해 라이브러리 사용 컨벤션도 수립했습니다.
하지만 AI를 활용해도 컨벤션 위반을 탐지하고 따르기 어려워서 번역 누락 및 오역, 코드 품질 저하를 초래했습니다. 이에 관하여 사람과 AI 검수의 한계를 체감하고, 컨벤션을 지키게하는 커스텀 린트(ESLint) 플러그인을 구현하여 극복한 경험과 결과를 이 글에서 공유하고자 합니다.
이 글은 다국어 라이브러리를 사용하거나, 팀의 까다로운 컨벤션 유지를 위해 AI 및 린트를 활용하는 개발자를 대상으로 합니다. 린트 플러그인을 구현한 경험이 없어도 쉽게 읽을 수 있게 정리했습니다.
@lib/i18n과 세 가지 컨벤션
먼저 문제의 출발점이 된 @lib/i18n 라이브러리와 컨벤션부터 살펴보겠습니다.
편의상 "화면에 렌더링되어 사용자에게 노출되는 문자열"을 "문구"라고 하겠습니다.
라이브러리는 문구를 여러 언어로 번역해주는 함수와 컴포넌트, 그리고 여기서 파생되어 수량을 고려하는 기수형(Cardinal Plurals)과 순서를 고려하는 서수형(Ordinal Plurals)을 제공합니다. 이들을 모두 묶어서 "번역 API"라고 표현하겠습니다.
| 일반형 | t() | <Trans> |
| 기수형 | tCardinal() | <TransCardinal> |
| 서수형 | tOrdinal() | <TransOrdinal> |
표 1. 라이브러리가 제공하는 번역 API 목록
t() 계열 함수는 인자의 문구를 번역하며, <Trans> 계열 컴포넌트는 자손의 React 노드들을 고려하면서 문구를 번역한다는 차이가 있습니다. 그리고 유형별로는 문구의 포맷 차이가 있는데, 나중에 살펴보겠습니다.
여기서 번역 API의 내부 동작이 궁금할 수 있겠지만 딴 길로 새지 맙시다. 중요한 것은 아래 세 가지 컨벤션을 반드시 지켜야 한다는 점입니다. 컨벤션을 무시하고 API 사용을 빼먹거나 대충 감각적으로 쓰면 누락·오역 같은 번역 품질 및 코드 품질 저하가 발생합니다.
1. 한글 문구에 번역 API를 사용해 번역 누락 막기
번역을 위해 한글 문구는 모두 잡아내어 반드시 번역 API를 적용해야 합니다. 물론 코드 내의 모든 한글 문자열을 탄압하는 것은 과합니다. 문법적으로 주석, 타입명, 변수명 등은 애당초 화면에 노출되지 않으니까 번역이 필요 없고, 테스트 코드나 로그 데이터의 문자열처럼 내부용으로 쓰는 경우도 마찬가지입니다.
const message = '번역 누락되는 메시지' // ❌ 그냥 한글 문구. 번역 누락됨. const message = t('번역되는 메시지') // ✅ t()를 사용했으니 번역됨. const 한글변수명 = ... // ✅ 변수명은 번역 필요 없음. console.log('내부 테스트') // ✅ 내부 디버깅용이니까 번역 필요 없음.2. 기수·서수에 맞게 번역 API를 사용해 오역 방지하기
문구가 변수에 의존하고 수량이나 순서를 표현한다면, 번역 품질 향상을 위해 각각 기수형이나 서수형 번역 API를 사용해야 합니다. 또한 일반형과 다르게 변수를 중괄호 {}로 보간(Interpolation)하도록 문구를 바꿔야 합니다.
내부에서 기수와 서수 번역에 특화된 국제 표준 문법인 ICU 메시지 포맷을 사용하다 보니 비슷하게 문구 포맷을 맞췄습니다.
반대로, 수량이나 순서와 관련 없거나 변수가 없는 문자열은 단수·복수나 서수 분기가 없으니까 일반형을 사용해야 합니다.
const message = t(`${count}개를 담았습니다.`) // ❌ tCardinal()이 아님. 단수·복수 번역이 제대로 안 됨. const message = tCardinal(`${count}개를 담았습니다.`) // ❌ 맞는 포맷이 아님. 단수·복수 번역이 제대로 안 됨. const message = tCardinal('{count}개를 담았습니다.', count) // ✅ 수량에 맞춰 "item(s)"로 정상 번역됨. const message = tCardinal('단복수 상관없는 메시지') // ❌ 기수와 상관없음. const message = t('단복수 상관없는 메시지') // ✅ 일반형이 적절.3. <Trans> 계열 컴포넌트는 필요할 때만 사용해 코드 복잡도 낮추기
<Trans> 계열 컴포넌트는 자손에 엘리먼트나 컴포넌트가 있는 경우에만 사용해야 합니다. 당연히 단순하게 문자열만 자식으로 가져도 기능에 문제 없지만, t() 계열 함수에 비하면 지나치게 들여쓰기와 줄바꿈이 생기므로 코드 가독성이 저하됩니다. 그래서 가급적 t() 계열 함수로 대체해야 합니다.
/* ✅ 올바른 <Trans> 사용례. <b> 엘리먼트가 포함된 문구를 번역. */ <Trans> <b>복잡한</b> 메시지 </Trans> /* ❌ children에 단순히 문자열만 있으니 굳이 <Trans> 쓰지 말자. 공백이 많이 생긴다. */ <p> <Trans>단순한 메시지</Trans> </p> /* ✅ 이때는 그냥 t()를 쓰자. */ <p>{t('단순한 메시지')}</p>험난한 컨벤션 준수의 길
위 컨벤션들은 머리로는 이해하기 쉽지만 은근히 맞추기 까다롭습니다. 그래서 AI 에이전트에게 기존 코드의 컨벤션 검사를 부탁하고, 코드 작성 규칙과 AI 코드 리뷰 솔루션의 규칙에도 컨벤션을 명시했으니, AI가 데우스 엑스 마키나인 양 컨벤션 위반을 알아서 뚝딱뚝딱 해결해 줄 것이라 믿고 @lib/i18n호는 출항했습니다.
사람과 AI 검수의 한계
하지만 바로 침몰했습니다. "사용자에게 노출되는 문자열인가?"에 대한 판단은 코드 컨텍스트에 대한 깊은 이해가 필요한데, 높은 집중력이 요구되는 작업에서 사람은 실수 덩어리이고 LLM은 확률적이다 보니 판단력이 다소 아쉬웠습니다. 그래서 기존 코드에서 많은 한글 문구들이 탐지되지 않아 번역이 누락되었고, 내부 개발용 코드의 한글 문자열이 잘못 잡히는 문제도 있었습니다. 일반형·기수형·서수형의 적합성 판단이나 <Trans> 계열 컴포넌트의 자손 구조 판단도 역시 완벽하지 않다 보니, 미탐과 오탐이 생겨 번역 품질과 코드 품질 하락을 초래했습니다.
또한 사람(AI를 포함한다)이 매번 컨벤션을 숙지하면서 코드를 작성하기에 무리가 있다 보니, 미래에 한글 문구가 추가될 때도 동일한 이슈가 발생할 것은 당연하였습니다.
린트로 위반 탐지하고 AI로 교정하기
AI를 향한 무한한 숭배심은 던져버렸습니다. 더 깐깐한 컨벤션 경찰이 필요합니다. AI 만능론의 시대에도 복잡한 컨벤션을 강제하려면, 확률론적인 AI보다 기존의 결정론적인 정적 분석 도구인 린트가 아래 표처럼 여러 측면에서 여전히 효과적입니다.
| 위반 탐지 | 잦은 미탐·오탐. 확률론적. | ✅ 정탐. 결정론적. |
| 위반 교정 | ✅ 자연어도 잘 교정함 | 기계적인 교정은 잘하지만 자연어는 어려움. |
| 수행 시점 | 수동, pre-commit 단계, PR push 등 | ✅ 언제든. 에디터와 연동하여 코드 작성 즉시 에러 피드백을 받을 수도 있음. |
| 속도 | 느림 | ✅ 빠름 |
| 비용 | 유료 | ✅ 무료 |
표 2. 컨벤션 위반 탐지와 교정에 대한 AI와 린트 비교
비록 주어진 컨벤션들이 자연어를 다루더라도, 위반의 형태를 패턴화할 수 있어서 탐지에 린트를 도입할 만합니다. 물론 린트가 자동 교정까지 해주면 가장 이상적이겠지만 자연어를 기계적으로 교정하기는 어렵다 보니, 역할을 나눠서 린트가 탐지하고 AI가 교정하는 하이브리드 접근법으로 가겠습니다.
커스텀 린트 플러그인 개발과 적용
처음에는 이상적인 린트 플러그인을 섭외하여 공수를 절감하려 했지만 눈앞의 생태계는 상당히 척박했습니다. 두 번째와 세 번째 컨벤션에 적합한 매물은 아예 존재하지 않았고, eslint-plugin-i18next가 제공하는 "문자열 리터럴 제한" 규칙은 첫 번째 컨벤션에 일부 맞지만, 옵션 조절로도 해결이 어려운 문제가 다수 있어서 도입하기 무리였습니다. 예를 들어 이 규칙은 함수 파라미터의 기본값으로 문자열 리터럴이 들어가는 것을 허용하는데, 프로젝트에서는 최종적으로 이런 기본값이 노출될 수도 있으니 제한해야 합니다.
쉬운 길은 없었습니다. 결국 컨벤션들을 충족시키는 커스텀 린트 규칙들과 이들을 포함하는 플러그인을 직접 개발하기로 했습니다. 당시에 타입스크립트의 AST(Abstract Syntax Tree) 분석과 typescript-eslint에 무지했고 러닝 커브가 있었지만, AI와 함께라면 두렵지 않았습니다. 배경 지식에 대한 속성 과외부터 린트 플러그인 개발과 프로젝트 적용까지 거의 전부 AI에게 맡겼습니다. 게다가 적용 후 린트 에러 해결, 즉 컨벤션 위반 교정도 부탁했습니다.
이때 린트 규칙의 위반 탐지 정확도를 높이고, 사람의 개입을 최소화하여 개발과 교정을 가속하기 위한 전략을 고심 끝에 수립하였습니다.
- 엄격한 초기 규칙: 미탐을 없애기 위해서 규칙을 엄격하게 잡고 나중에 완화해가며 오탐을 줄이자.
- 테스트 주도 개발: 프로젝트 코드에서 테스트 케이스 추출하기, 테스트 통과 때까지 로직 사포질하기 모두 AI의 특기.
- 점진적 접근: 한 번에 완벽히 하기 어려움. 조금씩 구현해서 정탐 에러는 교정하고, 오탐 에러는 분석해 테스트 케이스를 보충하고 탐지 로직을 개선하는 과정을 반복하자.
- 세밀한 옵션: 다양한 옵션을 두어서 오탐 해결 시에 로직 변경을 최소화하고, 특정 프로젝트의 요구사항에 맞춰 커스터마이징을 가능하게 함.
- AI 주도 교정: 교정은 AI 에이전트에게 위임하고 판단이 어려운 상황에는 사람에게 전달함.
- 상세한 에러 메시지: AI 에이전트가 제대로 위반을 교정하도록 유도함.
- 린트 자동 교정은 버리자: AI 반자동 교정으로 충분. 자연어의 기계적 교정은 정확도가 떨어지고 오버엔지니어링임.
- 정확도와 복잡도의 트레이드오프: 오탐을 줄일수록 규칙의 정확도와 프로젝트 개발 편의성이 상승하나, 탐지 로직이 난해해져 유지보수가 부담됨. 그래서 상황에 따라 탐지 로직을 보완하거나, 과감하게 오탐을 감수하고 프로젝트에서 부분 린트 비활성화를 적극 사용함.
전략을 토대로 아래 그림의 과정처럼 점진적으로 개발 및 교정하였습니다. AI 에이전트는 자기가 수집한 테스트 케이스를 소화하면서 탐지 로직을 다듬고 린트 에러를 교정하고, 사람은 전반적으로 감독하면서 오탐을 분석하고 에이전트가 포기한 에러를 처리하는 것을 반복했습니다. 확실히 사이클을 돌 때마다 플러그인이 점점 진화하고 프로젝트가 정화되어 가는 것을 느꼈습니다.

이미지 1. 린트 플러그인 개발과 프로젝트 적용 및 교정 과정
세 가지 린트 규칙과 위반 탐지 과정
이번에는 그렇게 탄생한 규칙들을 깊게 파보겠습니다. 린터의 원리는 AST 노드를 순회하면서 설정된 규칙 기반으로 패턴을 찾아내는 것입니다. 그러므로 각 규칙이 위반·허용 패턴을 정의하는 방식과, 특정 노드의 진입·퇴장 이벤트에서 패턴을 찾아내고 처리하는 로직을 중심으로 살펴보겠습니다.
AST 노드 이야기가 많이 나오는데, 노드 종류는 공식 문서를 참조하세요. 또한 각 규칙의 위반 예시는 이전 섹션에서 소개된 예시 코드를 참조하세요.
규칙 1. no-literal-string: 한글 문구 제한
번역 API를 사용하지 않은 한글 문구를 잡아내어 번역 누락을 방지하는 규칙입니다. 조금 더 정확하게는 사용자에게 노출될 가능성이 있는 한글 문자열은 모두 컨벤션 위반입니다.
초기에는 화끈하게 한글이 아닌 문자열 리터럴까지 모두 골라내는 규칙으로 시작하다 보니 규칙 이름이 살벌한데, 최종적으로 꽤 자비로워졌습니다.
노출될 수 있는 한글 문자열 식별하기
먼저 문법적으로 접근해보면, 노출될 수 있는 문자열은 문자열 리터럴, 템플릿 리터럴, JSX 텍스트 세 가지 타입 중 하나입니다. 이들이 항상 노출되는 것은 아니고, 예를 들어 문자열 리터럴이 import 선언문 안에 위치하면 노출되지 않으므로 안전한데, 이러한 구문 및 스코프를 "안전지대"라고 표현하겠습니다. 그리고 안전지대의 범위는 구문에 따라 다른데, 예컨대 import문은 자손 문자열이 모두 안전한 "깊은 안전지대"이지만 case문은 자식까지만 안전한 "얕은 안전지대"입니다.
내부용 테스트 코드나 데이터 같은 파일도 일종의 깊은 안전지대라 볼 수 있는데, 이런 파일들은 플러그인의 ignores 옵션에 파일명 패턴을 넣어서 아예 린트 대상에서 제외했습니다. 또한 규칙의 정의에 따라 번역 API는 성역이므로 t() 계열 함수는 깊은 안전지대입니다. 다만 컴포넌트는 하위에 다양한 종류의 구문을 포함할 수 있다 보니 언뜻 보면 무법지대입니다. 그래서 <Trans> 계열 컴포넌트는 일단 유사 안전지대로 보고, 내부에서 사용하는 속성이 모두 안전한지 꼼꼼히 확인한 뒤에야 비로소 깊은 안전지대라 말할 수 있습니다.
<Trans>텍스트<img alt="한글" src="..." />텍스트</Trans> // ❌ alt 속성은 <Trans>로는 번역이 안 되므로 위험지대임. <Trans>가 마냥 깊은 안전지대가 되면 절대 안 됨. <Trans><b data-testid="한글">텍스트</b> 텍스트</Trans> // ✅ 내부에서 data-testid같이 안전한 속성만 쓴다면 이 <Trans>는 깊은 안전지대임.요컨대 위반을 찾으려면, 안전지대 밖에 있으면서 세 가지 타입에 맞는 한글 문자열을 골라내면 됩니다. 추가로 프로젝트 개발 편의를 위해 특례를 두었습니다. 가독성을 위해 한글 타입 사용이 잦았는데, 타입에 맞춘 문자열 리터럴 값은 문법상 노출 위험이 있으나 결과적으로 노출된 경우는 없어서 문제 삼지 않았습니다.
function move(page: '홈지면' | '리뷰지면') // 한글 타입. move('홈지면') // ✅ 예외적으로 허용. "홈지면"이 노출 가능성 있는 값이긴 하지만, 실제로 이 값이 렌더링까지 가지는 않음.스택으로 안전지대 표시하기
이제 린터에 빙의해 AST 노드를 깊이 우선 탐색하며 위반을 찾아보겠습니다. 말단의 문자열 노드로 향하는 동안, 먼저 중간의 조상 노드들이 깊은 안전지대인지 표시하는 "스코프 스택"을 하나 두고, 노드에 진입·퇴장하는 이벤트마다 스택에 삽입·회수할 플래그들을 정의합니다. 스택을 뒤에서부터 읽어서 위험 플래그 하나 없이 안전 플래그를 만난다면, 현재 스코프는 깊은 안전지대에 속합니다.
- 안전 플래그: 노드가 깊은 안전지대임을 의미.
- 위험 플래그: 노드가 더 이상 안전지대가 아님을 의미.
- 더미 플래그: 아무 의미 없음. 노드 퇴장 시 짝을 맞춰 회수하기 위한 자리 채움.
다음으로 노드에 진입하면 노드의 타입에 따라 적절한 플래그를 삽입합니다.
- 깊은 안전지대가 되는 노드
- 예시: ImportDeclaration, ExportAllDeclaration, TSEnumMember 등
- 내부의 문자열이 모두 노출되지 않으므로, 이런 노드를 만나면 무조건 스택에 안전 플래그 삽입.
- 얕은 안전지대가 되는 노드
- 예시: SwitchCase, MemberExpression 등
- 스택을 건드리지 않고 넘어감. 말단 문자열 노드가 스코프가 안전한지 판단할 때, 직접 부모 노드를 참조하는 것이 더 간결함.
- 조건에 따라 깊은 안전지대가 되는 노드
- 예시: CallExpression, NewExpression, Property, VariableDeclarator 등
- 함수 호출인 CallExpression 노드를 예로 들면, 안전한 함수라면 스택에 안전 플래그 삽입. 아니라면 더미 플래그 삽입.
- 안전한 함수: t, tCardinal, tOrdinal, console.*, Error, require, addEventListener 등
- 컴포넌트 관련 노드
- JSXElement: 안전한 컴포넌트면 스택에 안전 플래그를 삽입. 아니라면 더미 플래그 삽입.
- 안전한 컴포넌트: Trans, TransCardinal, TransOrdinal.
- JSXAttribute: 안전한 속성이 아니라면 스택에 위험 플래그를 삽입. 맞다면 더미 플래그 삽입.
- <Trans>의 자손 엘리먼트가 한글 속성 값을 가질 수 있으므로, 이 노드에서는 엄격한 위험 플래그를 사용.
- 안전한 속성: key, id, data-testid, className 등
- JSXElement: 안전한 컴포넌트면 스택에 안전 플래그를 삽입. 아니라면 더미 플래그 삽입.
- 그 외 안전지대가 될 수 없는 노드
- 관심을 줄 필요 없습니다.
로직 변경 최소화를 위해, 안전한 함수, 컴포넌트, 속성에 대한 살생부는 로직에서 빼내서 Allow List 형식의 규칙 옵션으로 두고 관리했습니다.
그러면 아래 예시 이미지와 같이 스코프 스택에 플래그가 쌓이게 됩니다.

이미지 2. 안전지대 밖에 있는 문자열 리터럴 노드의 스코프 스택 예시
노출될 수 있는 한글 문자열 골라내기
계속 탐색하다 보면 어느새 말단에서 세 가지 타입의 문자열에 각각 상응하는 Literal, TemplateLiteral, JSXText 노드를 마주합니다. 드디어 각 노드가 조상님 덕을 보는지 심판할 시간입니다.
- 스택의 뒤부터 플래그 확인. 노드가 깊은 안전지대에 속하면 검사 종료.
- 부모 노드를 확인. 노드가 얕은 안전지대에 속하면 검사 종료.
- 노드의 문자열이 한글이 아니라면 검사 종료.
- 타입체크를 하여 문자열이 타입에 맞춰 사용되면 검사 종료.
- 여기까지 뚫고 온 문자열, 축하합니다. 위반입니다. 번역 API를 사용하도록 응원의 메시지를 담아 에러로 보고하면 끝입니다.

이미지 3. 번역 API를 사용하지 않는 한글 문구에 대한 린트 에러 메시지
규칙 2. prefer-plural: 기수형·서수형 사용 선호
기수 및 서수 문구에 기수형과 서수형 번역 API 사용을 유도하여 오역을 방지하는 규칙입니다. 이전 규칙에 비해 역할이나 위반 패턴이 비교적 명확합니다. 예를 들어 t('리뷰 {count}개', { count })처럼 변수에 의존하는 기수 문자열이 일반형 번역 API를 사용하면 잡아냅니다. 또한 AI 에이전트가 방어적으로 모든 일반형을 기수형이나 서수형으로 대체하는 것을 막기 위해서 역방향 검사도 수행합니다.
키워드 매칭으로 기수·서수 판별
기계적인 린터에게 정확한 자연어 분석은 어려우니, 기수와 서수의 판별은 휴리스틱하게 갔습니다. 위의 예시를 다시 보면 count와 개에서 실마리가 보입니다. 중괄호 {} 보간에 사용하는 속성명이나 그 뒤에 붙는 단위 단어로 추론할 수 있습니다. 그래서 프로젝트 코드에서 수량과 순서에 관련된 키워드를 수집해서 목록을 만들었습니다.
- 기수·서수 속성명 키워드: count, score, point, day 등
- 단위 키워드: 개, 점, 일, 번째 등
마찬가지로 키워드 목록도 로직에서 규칙 옵션으로 분리해서, 로직 변경을 최소화하고 프로젝트별로 커스터마이징 가능하도록 하였습니다.
전략이 생겼으니 위반 탐지 로직은 간단합니다. AST 탐험 중에 아래처럼 번역 API에 해당하는 두 가지 노드를 만나면, 키워드 매칭으로 적합한 유형을 도출하고, 실제 유형과 불일치하면 에러로 보고합니다. 그리고 자연어 처리에 능숙한 AI 에이전트가 올바르게 교정할 수 있도록, 친절하게 키워드 정보와 교정 가이드를 에러 메시지에 담아줍니다.
- CallExpression 노드
- t() 계열 함수가 아니면 검사 종료.
- 다음 중 하나에 해당하면 기수형이나 서수형이 적합하고 아니면 일반형이 적합함.
- 함수의 첫 번째 인자가 문자열 리터럴 혹은 템플릿 리터럴이면서 {} 보간 뒤에 단위 키워드가 있음.
- 함수의 두 번째 인자가 객체이면서 키워드와 일치하는 기수·서수 속성명이 있음.
- JSXElement 노드
- <Trans> 계열 컴포넌트가 아니면 검사 종료.
- 다음 중 하나에 해당하면 기수형이나 서수형이 적합하고 아니면 일반형이 적합함.
- 자손들이 가진 문자열을 추출하고 평탄화해서 이어봄. 여기에 {} 보간 뒤에 단위 키워드가 있음.
- values prop을 사용하고 값이 객체이면서 키워드와 일치하는 기수·서수 속성이 있음.
- count prop을 사용함.
- JSX 표현식인 자손에 객체가 있고 키워드와 일치하는 기수·서수 속성이 있음.
단순 키워드 매칭이다 보니 오탐이 드물게 발생하는데, 시원하게 린트를 부분 비활성화하였습니다.

이미지 4. 수량을 표현하는 문구에 부적합한 번역 API 사용에 대한 린트 에러 메시지
규칙 3. strict-trans-component: <Trans> 계열 컴포넌트 엄격하게 사용하기
<Trans> 계열 컴포넌트 남발로 인한 코드 복잡도 상승을 억제하는 규칙입니다. 내부에 JSX 텍스트만 있고 엘리먼트나 컴포넌트가 없는 <Trans> 컴포넌트를 위반으로 잡을 뿐이라서 탐지 로직은 매우 간단합니다.
AST를 순회하다가 JSXElement 노드를 만나면 <Trans> 계열 컴포넌트이면서 JSXElement인 자식이 있는지 확인하고, 맞다면 위반 당첨입니다. t() 계열 함수를 쓰도록 축하 메시지를 담아 에러를 보고합니다.

이미지 5. 잘못된 Trans 사용에 대한 린트 에러 메시지
린트 플러그인의 성과
숫자 없이 복잡한 글만으로는 효능이 마음에 와닿지 않는 듯하니, 제작한 플러그인의 규칙이 적발한 컨벤션 위반 개수를 확인해봅시다. 정확한 측정을 위해 "AI의 컨벤션 검사 완료 후 ~ 린트 플러그인 교정 전" 시점의 코드 형상으로 회귀해서 린트를 수행하니 아래 표처럼 결과가 나왔습니다. 사실 사람과 AI의 검수가 놓친 위반을 조금만 잡아도 성공적이라고 밑밥을 깔려 했는데, 그럴 필요가 없을 정도로 기대를 훨씬 뛰어넘는 만족스러운 수치였습니다.
| 1. no-literal-string | 번역 누락 | 182개 |
| 2. prefer-plural | 기수·서수 오역 | 5개 |
| 3. strict-trans-component | 코드 복잡도 상승 | 22개 |
표 3. 세 린트 규칙의 에러 탐지 건수
덕분에 운영 배포 전에 182개의 번역 누락과 5개의 오역을 교정하여 초동 진화에 성공했고, 22개의 복잡한 코드를 정화했습니다. 게다가 이 모든 탐지가 즉각적이고 무료라서 검사 시간과 비용 부담도 덜어줬고, 앞으로 작성할 코드들도 잘 단속해 줄 테니 든든한 안전망을 얻었습니다.
덤으로 no-literal-string 규칙이 includes('한글')처럼 분기 로직에 사용된 한글 문자열도 검거해줘서, 번역 누락 방지뿐만 아니라 다른 언어 환경에서의 잠재적 버그에 대처한 효과도 보았습니다.
맺는 글
지금까지 커머스 웹프론트에서 다국어 지원을 위해 도입한 @lib/i18n의 컨벤션 준수 이슈와 그 해결 과정을 살펴봤습니다. 사람은 실수를 하고 AI는 확률론적이다 보니 컨벤션 위반의 미탐과 오탐이 빈번했기 때문에, 결정론적인 린트 규칙을 구현해 탐지하고 교정은 자연어에 능숙한 AI에게 맡기는 하이브리드 접근을 택했습니다. 그 결과 다량의 번역 누락과 오역을 방지하고 코드 복잡도도 낮췄습니다.
AI는 요술램프가 아닙니다. 컨벤션을 지키게 하려면 무작정 AI에게 떠넘기지 말고, 위반의 형태와 교정 방법에서 패턴을 도출해보고 사람·AI·린트에게 각자가 잘하는 일을 맡기면 되며, 설령 컨벤션이 까다로워서 마땅한 린트 플러그인이 없다면 AI로 직접 구현하는 것도 해볼 만하니 추천합니다. 비단 컨벤션뿐만 아니라 모든 분야에서, 결정론과 확률론의 경계를 구분하고 적재적소에 일을 맡기는 안목이 더욱 중요해지고 있습니다. 이 글이 비슷한 고민을 하시는 분들에게 도움이 되면 좋겠습니다.
커머스 서비스에서 고객과 운영자가 보는 것을 맡고 있습니다.





![[헬스캡슐]은행잎 추출물, ‘베타아밀로이드 응집 억제’ 효과 확인 外](https://dimg.donga.com/wps/NEWS/IMAGE/2026/05/26/133978263.3.jpg)




!['꽃청춘' 3인방, 무계획 제주의 높은 벽..결국 티켓 구하기 실패[별별TV]](https://image.starnewskorea.com/21/2026/05/2026052421091553722_1.jpg)

![[오피셜] ‘불꽃슈터’ 전성현, KT서 ‘퍼펙트 10’ 파트너 문성곤과 재회…서민수도 3년 계약](https://pimg.mk.co.kr/news/cms/202605/28/news-p.v1.20260528.c55346b19e8f45bfb362482843760fb3_R.png)
English (US) ·