배경: 해외 확장과 함께 시작된 다국어 대응
한국딥러닝(지금 다니고 있는 회사)이 해외 확장을 고려하기 시작하면서, 빠른 다국어 대응이 필요해졌다. 한창 프로덕트를 개발하고 있던 어느 날, 대표님이 오셔서 한마디 던지셨다.
우리 대표님 슬랙 프로필은 양파쿵야 ㅋ.ㅋ
“지금 만드는 프로덕트 영어 지원하는 데 얼마나 걸릴 것 같아요? ^0^”
이전 회사에서는 엘살바도르 다국어 지원을 했던 경험이 있었다. 규모가 꽤 큰 서비스였고, 당시에는 프론트 10명 전원이 서포트하고 1~2명이 몇 달동안 유지보수했던 기억이 난다. 그래서 머릿속에서는 자연스럽게 “이번에도 꽤 걸리겠는데…”라는 계산이 돌아가고 있었다.
문제는 현재 서비스의 상태였다. 기존 서비스는 한국어 단일 언어를 전제로 만들어져 있었고, 텍스트는 컴포넌트 안, 유틸 함수 안, 비즈니스 로직 안까지 아주 골고루 퍼져 있었다. JSX 안에 박혀 있는 안내 문구, 공통 에러 메시지, 성공 토스트 문구까지 전부 한국어였다. 이쯤 되면 다국어는 번역 문제가 아니라 거의 노가다 작업에 가깝다.
스크립트를 돌려서 확인해보니, 현재 코드베이스에 존재하는 한국어 텍스트는 총 698개였다.
“흠… 하루면 될 것 같아요.”
실제로 혼자서 하루도 걸리지 않았다. 몇 시간 만에 끝냈다.
돌이켜보면 문제는 “한국어를 영어로 바꾸자”가 아니었다.
진짜 문제는 “이 많은 문자열을 어떤 기준으로 관리할 것인가”였다.
이번 작업의 목표는 비교적 명확했다.
•
기존 코드는 최대한 건드리지 않는다.
이미 잘 돌아가는 코드를 대규모로 수정하는 순간, 다국어보다 버그가 먼저 국제화된다.
•
기계번역으로 최소 2~3개 언어를 빠르게 지원한다.
완벽한 번역보다, 실제로 해외 사용자가 쓸 수 있는 상태가 우선이다.
•
개발자 경험(DX)을 해치지 않는다.
다국어를 붙였다고 빌드가 느려지거나, 코드가 괴상해지는 일은 피하고 싶었다.
“그래서 i18n 라이브러리 뭐 쓸 건데?”
이 글은 그 다국어 라이브러리 선택 및 설계 과정에서 겪은 시행착오의 기록이다.
Next15 특징과 다국어 설계에 미치는 영향
다국어 라이브러리를 고르기 전에 먼저 짚고 넘어가야 할 게 있다.
이번 프로젝트는 Next.js 15를 사용하고 있다는 점이다. 이게 왜 중요하냐면, Next 15부터는 “대충 예전 방식대로 해도 되겠지”라는 생각이 잘 통하지 않기 때문이다.
Next 15의 가장 큰 변화는 App Router가 사실상 기본 전제가 되었다는 점이다. Pages Router 시절에는 다국어를 어떻게든 우회해서 붙일 수 있었다. 전역 상태로 locale을 들고 있거나, i18n 미들웨어로 억지로 처리하는 방식도 가능했다. App Router에서는 이런 접근이 하나씩 불편해진다. 대신 프레임워크가 권장하는 길은 굉장히 명확해졌다.
바로 라우팅 자체로 언어를 표현하라는 것이다.
/ko/page, /en/page, /ja/page 같은 구조는 App Router에서 매우 자연스럽다. locale이 URL에 포함되면, 서버에서도 지금 어떤 언어 컨텍스트인지 바로 알 수 있고, 클라이언트에서 따로 언어 상태를 들고 있을 필요도 없다. 이 시점부터 다국어는 “상태 관리 문제”가 아니라 “라우팅 설계 문제”로 바뀐다.
여기에 Server Components가 기본이라는 점도 영향을 준다. 번역 메시지를 어디서, 언제 로딩할 것인지가 중요해진다. 클라이언트에서만 동작하는 번역 라이브러리는 점점 쓰기 애매해진다. 서버에서 바로 번역 메시지를 가져와서 렌더링할 수 있는 구조가 훨씬 자연스럽다.
그리고 하나 더 고려할 것은 Turbopack이다.
Next 15에서는 Turbopack이 사실상 기본 빌드 도구다. 빌드 속도, HMR 체감 속도 모두 이전과 비교가 안 된다. 문제는 여기서 Babel이 등장하는 순간이다. Babel이 필요한 라이브러리를 붙이면, Turbopack을 포기해야 하는 상황이 생긴다. 다국어를 얻는 대신 빌드 속도를 잃는 트레이드오프를 고려해야한다.
Next 15 환경에서 다국어를 설계할 때 고려해야 할 요소들 정리.
•
App Router와 자연스럽게 어울리는가
•
locale 기반 라우팅을 전제로 설계되어 있는가
•
Server Components에서 사용 가능한가
•
Turbopack을 포기하지 않아도 되는가
“기능이 제일 많은 i18n 라이브러리는 뭐지?”가 아니라,
“Next 15가 싫어하지 않는 i18n 라이브러리는 뭐지?”가 된다.
이 배경을 깔고 나서야, Lingui와 next-intl을 제대로 비교할 수 있는 상태가 된다.
Next에서 사용할 수 있는 다국어 번역 라이브러리들과 차이점
Next에서 다국어를 지원한다고 하면, 생각보다 선택지는 많아 보인다. npm에 react i18n 정도만 검색해도 후보가 우르르 나온다. 문제는 이 라이브러리들 중 상당수가 Next 15 + App Router 기준에서는 애매해다는 점이다.
Lingui
Lingui는 번역 키를 다루는 방식이 굉장히 개발자 친화적이다. flat key 구조를 기본으로 가져가고, 문자열을 코드 단위에서 명확하게 관리할 수 있다. React 컴포넌트 안뿐 아니라 유틸 함수, 비즈니스 로직에서도 동일한 번역 모델을 사용할 수 있다는 점도 강점이다. 다만 macro 기반 사용 시 Babel 의존성이 생기고, 이게 Next 15 환경에서는 꽤 치명적으로 작용한다.
Next-intl
이 라이브러리는 처음부터 App Router를 전제로 설계된 느낌이 강하다. locale path 기반 라우팅과 잘 맞고, Server Components에서도 자연스럽게 사용할 수 있다. Babel 없이도 동작하고, Next의 메타데이터 API나 정적 생성 흐름과도 궁합이 좋다. 대신 메시지 키 구조는 기본적으로 중첩을 전제로 하고 있어서, 기존 flat key를 그대로 가져오기에는 부담이 있다.
react-i18next
react-i18next도 많이 쓰이는 선택지다. React 생태계에서는 사실상 표준에 가깝다. 다만 Next 환경에서는 별도의 설정이 필요하고, App Router 및 Server Components와의 궁합이 썩 좋다고 보기는 어렵다. 특히 클라이언트 중심 사용 패턴이 강해, Next 15의 방향성과는 약간 어긋난다.
next-i18next는 이름에서 느껴지듯 Next 전용처럼 보이지만, 실제로는 Pages Router 시절에 최적화된 라이브러리다. App Router에서도 사용할 수는 있으나, 구조적으로 자연스럽다는 느낌은 아니다. 이미 Next가 공식적으로 다른 방향을 밀고 있는 상황에서 굳이 선택할 이유는 줄어든다.
react-intl(FormatJS 계열)
react-intl(FormatJS 계열)은 메시지 포맷과 ICU 문법을 강력하게 지원한다. 복잡한 포맷이 필요한 서비스라면 여전히 매력적이다. 다만 설정과 사용 방식이 다소 무겁고, App Router 기준에서는 오버엔지니어링처럼 느껴질 수 있다.
라이브러리 비교 요약
라이브러리 | App Router 궁합 | Server Components | Turbopack 사용 | 키 구조 | 특징 |
Lingui | 보통 | 가능 | Babel 필요 시 불가 | flat | 키 재사용, 검색성 좋음 |
next-intl | 매우 좋음 | 가능 | 가능 | nested 기본 | Next 15 친화적 |
react-i18next | 애매 | 제한적 | 가능 | nested | React 중심 |
next-i18next | 나쁨 | 제한적 | 가능 | nested | Pages Router 중심 |
react-intl | 애매 | 제한적 | 가능 | nested | 강력한 포맷, 설정 무거움 |
Next 15 + App Router + Turbopack을 기준으로 보면, 실제로 고민할 만한 후보는 Lingui와 next-intl 두 개 정도로 압축된다.
Next15와 Lingui
Next 15에 Lingui를 붙였던 이유
Next 15에서 다국어를 붙이기로 결정하고, 여러 선택지를 놓고 고민한 끝에 Lingui를 처음 선택했다. 이유는 단순했다. Lingui는 지금 이 코드베이스에 가장 덜 공격적인 선택처럼 보였기 때문이다.
가장 큰 이유는 소스코드에서 번역 key를 자동으로 추출해주는 기능(cli extract)과 flat key 기반 구조였다.
Lingui는 번역 키를 중첩 구조로 강요하지 않는다. 키를 의미 단위로 나누지 않아도 되고, 문자열 자체를 기준으로 관리하는 것도 가능하다. 이게 왜 중요하냐면, 기존 코드베이스에 이미 한국어 텍스트가 잔뜩 박혀 있었기 때문이다.
t("파일 업로드에 실패했습니다")
TypeScript
복사
이런 형태는 기존 코드를 크게 흔들지 않는다. JSX 안에 있던 텍스트를 그대로 감싸기만 하면 된다. “user.error.uploadFailed” 같은 의미 기반 키를 새로 정의하고, 파일 구조를 고민하고, 네이밍 회의를 하는 과정이 없다. 다국어 작업이 기획 회의로 번지는 걸 막아준다.
두 번째 이유는 검색성과 재사용성이다.
flat key 구조에서는 IDE 검색이 굉장히 강력해진다. 특정 문구가 어디서 쓰이는지 바로 찾을 수 있고, 기존에 쓰던 텍스트를 그대로 재활용하기도 쉽다. 번역 키가 코드와 분리된 추상적인 이름이 되지 않기 때문에, “이 키가 뭐였더라” 같은 상황이 거의 발생하지 않는다.
세 번째는 React 바깥에서도 동일한 모델로 사용 가능하다는 점이다.
Lingui는 컴포넌트 안에서만 쓰라고 강요하지 않는다. 유틸 함수, 비즈니스 로직, 서버 코드에서도 같은 방식으로 메시지를 다룰 수 있다. 실제로 에러 메시지나 토스트 문구처럼 UI 바깥에서 생성되는 문자열이 많은 서비스에서는 이 점이 꽤 크게 느껴진다.
정리하면, Lingui는 이런 요구에 잘 맞아 보였다.
•
기존 코드 수정 범위를 최소화할 수 있다.
•
다국어 도입으로 코드 구조가 복잡해지지 않는다.
•
“번역 키 설계”라는 부가 작업을 거의 하지 않아도 된다.
이 시점에서는 Lingui가 거의 정답처럼 보였다. 다만 Next 15 환경이라는 점만 잠시 잊고 있었다는 게 문제였다…
Next 15에서 Lingui를 다시 제거한 이유
Lingui를 붙이고 나서, 초반 작업은 굉장히 순조로웠다. 맛보기로 회원가입 폼 컴포넌트만 번역해봤는데 t()로 감싸는 작업도 생각보다 빠르게 끝났고, 번역 파일 구조도 깔끔했다.
하지만 HTML 속성 placeholder, alt 등 컴포넌트가 아닌 텍스트는 Trans 컴포넌트를 사용하기 어렵고 t 매크로를 명시적으로 사용해야 하는 번거로움, 번역 파일을 런타임에 원격으로 가져오기보다 빌드 시점에 컴파일(lingui compile)하는 방식에 최적화되어 있어 번역 콘텐츠만 즉시 업데이트하는 것이 어렵다는 점들이 아쉬웠다.
그리고 Lingui를 제대로 사용하려면 macro를 쓰게 된다. 그리고 이 macro를 쓰기 위해서는 Babel 설정이 필요하다. Next 15에서 Babel을 붙이는 순간, Turbopack을 포기해야 한다.
Turbopack은 Next 15에서 체감이 가장 큰 변화 중 하나다. 개발 서버를 띄우는 속도, HMR 반응 속도, 작은 수정 하나 했을 때의 피드백 주기까지 전반적으로 “이제 예전으로는 못 돌아가겠다”는 느낌을 준다. 그런데 Babel 설정이 들어가는 순간, 이 모든 걸 내려놓아야 하는 상황이 된다.
다국어를 얻는 대신 빌드 속도를 잃는 선택지였다.
결국 Lingui를 더 파고들기 전에, 방향을 틀자라는 판단을 내렸다.
다국어를 위해 Next 15의 가장 큰 장점을 포기하는 건, 장기적으로 봤을 때 좋은 선택이 아니라고 판단했다.
Next 15와 next-intl
next-intl은 “Next에서 다국어를 하려면 이렇게 하세요”라는 답안지에 가까운 라이브러리다. locale을 전역 상태로 관리하라고 강요하지도 않고, 별도의 트릭을 쓰지 않아도 된다. App Router 구조 자체를 그대로 받아들이고, 그 위에 번역 레이어를 얹는 방식이다.
next-intl의 장점
next-intl을 붙이면서 가장 먼저 체감한 건, 다국어 전환이 생각보다 아주 단순한 문제로 바뀌었다는 점이었다. 그 이유는 locale을 상태로 다루지 않고, 라우팅의 일부로 다루기 때문이다.
/ko, /en, /ja처럼 locale이 URL에 포함되면, 언어는 더 이상 “어디선가 관리해야 하는 값”이 아니다. 그냥 지금 보고 있는 경로 그 자체가 언어다. 이 구조가 주는 장점은 생각보다 많다.
먼저 서버 입장에서 굉장히 편해진다. 요청이 들어오는 순간 어떤 언어로 렌더링해야 하는지가 명확하다. 쿠키를 뒤질 필요도 없고, 헤더를 파싱할 필요도 없다. URL만 보면 된다. Server Components 기본 구조와도 잘 맞는다.
클라이언트에서도 마찬가지다. 언어 상태를 전역 상태로 들고 있을 필요가 없다. 언어를 바꾸고 싶으면 그냥 라우팅만 바꾸면 된다. 토글이든 드롭다운이든, 결국은 /en에서 /ja로 이동시키는 문제다. 상태 동기화나 초기화 같은 고민이 사라진다.
이 방식은 UX뿐 아니라 운영 측면에서도 이점이 있다. URL에 언어가 드러나 있기 때문에 공유하기도 쉽고, SEO 측면에서도 명확하다. 검색 엔진 입장에서도 “이 페이지는 어떤 언어 페이지다”를 파악하기가 훨씬 수월하다.
이 지점에서 next-intl의 장점이 하나 더 겹친다.
next-intl은 이런 locale path 구조를 전제로 설계된 라이브러리다. 억지로 맞추는 느낌이 아니라, “아, 이렇게 쓰라고 만든 거구나”라는 느낌이 든다.
Next 15 기준으로 보면 추가로 체감되는 장점들도 있다.
Server Components에서 번역을 바로 사용할 수 있다. 서버에서 getTranslations로 메시지를 가져와 렌더링하고, 클라이언트에는 필요한 결과만 내려보낸다. 번역 데이터 전체를 클라이언트로 들고 와서 처리하던 예전 방식과는 확실히 다르다.
정적 생성과의 궁합도 좋다. locale별로 페이지를 나누는 구조가 명확하기 때문에, generateStaticParams를 통해 언어별 페이지를 정적으로 생성하기도 쉽다. “이 페이지는 ko, en, ja 세 버전이 있다”라는 사실이 코드 구조에 그대로 드러난다.
그리고 빼놓을 수 없는 포인트가 하나 있다.
타입 안정성이다. 번역 키를 사용할 때 타입 힌트를 받을 수 있고, 잘못된 키 접근을 컴파일 타임에 잡아낼 수 있다. 다국어가 늘어날수록 이런 안전장치의 체감은 점점 커진다.
정리해보면 next-intl은 Next 15 환경에서 이런 장점을 준다.
•
locale path 기반 라우팅과 자연스럽게 결합됨
•
언어를 상태가 아니라 URL로 관리 가능
•
Server Components에서 바로 사용 가능
•
정적 생성, 메타데이터 API와 궁합이 좋음
•
타입 안정성을 확보할 수 있음
•
Babel 없이 Turbopack을 그대로 유지 가능
하지만 그럼에도 불편한 게 없지는 않았다.
기본적으로 중첩 키 구조라는 점, 그리고 공식적으로 지원하는 key extraction 기능이 없어서 아쉬웠다.
next-intl 단점과 한계 극복
단점 1. 중첩 키 구조는 생각보다 피곤하다
next-intl은 기본적으로 중첩 키 구조를 전제로 한다.
{"common":{"error":{"uploadFailed":"파일 업로드에 실패했습니다"}}}
JSON
복사
이 구조 자체가 나쁜 건 아니다. 의미 단위로 잘 정리된 메시지를 만들기에는 분명 장점이 있다. 문제는 기존 코드베이스가 이미 그런 구조를 전혀 고려하지 않고 만들어졌다는 점이다.
기존에는 JSX 안에 그냥 문자열이 있었다. 그걸 다국어로 바꾸면서 갑자기 “이 문구는 common이야? error야? upload 쪽이야?” 같은 질문이 생긴다. 다국어 작업이 번역이 아니라 폴더 설계 회의로 변질된다. 게다가 기존에 쓰던 문구를 재활용하기도 쉽지 않다. 키 이름을 정확히 기억하지 않으면 IDE 검색도 잘 안 된다.
이 지점에서 문득 처음 Lingui를 붙였을 때의 경험이 떠올랐다.
flat key 구조였다. 문자열 자체가 키였다. 검색이 됐다. 재사용이 쉬웠다.
그래서 Lingui의 flat key 방식을 가져와서 next-intl에 적용해보았다.
{"파일 업로드에 실패했습니다":"파일 업로드에 실패했습니다"}
JSON
복사
의미 기반 키 대신 문자열 기반 키를 사용하니, 기존 코드 수정 범위가 급격히 줄어들었다. IDE 검색도 다시 살아났고, “이 문구 어디서 쓰이지?”라는 질문에 바로 답할 수 있게 됐다. 다국어 작업이 다시 개발 작업으로 돌아왔다.
물론 주의할 점은 있다.
next-intl에서는 .을 중첩 키 구분자로 사용하기 때문에, key에 마침표를 쓸 수 없다. 그래서 내부 규칙으로 ; 같은 문자를 사용해 구분했다. 완벽하진 않지만, 팀 규칙으로 정해두면 크게 문제 되지는 않는다.
단점 2. key extraction 기능이 없다
두 번째로 부딪힌 문제는 key extraction이다.
Lingui에는 메시지를 자동으로 추출해주는 개념이 있다. 반면 next-intl에는 그런 기능이 없다. (next-intl-scanner라는 라이브러리가 있긴 하지만 공식 지원은 아님) 결국 모든 키를 사람이 직접 찾아서 옮겨야 한다.
문자열이 몇 개 안 되면 모르겠지만, 이 프로젝트에는 한국어 텍스트가 698개 있었다. 이걸 손으로 하나씩 찾는 순간, 다국어는 다시 노가다 작업이 된다.
그래서 여기서 LLM을 썼다. 다만 번역기로 쓰지는 않았다. 그러면 월급을 토큰 비용으로 쓰게 될 수도 있다.. ㅎㅎ
•
자동화 흐름
◦
한국어 문자열 detect 스크립트 실행
◦
일반 텍스트만 t("텍스트") 적용하는 스크립트 실행
◦
ko.json 자동 생성 → ko.json 기반으로 en.json, ja.json 자동 생성 스크립트 실행
◦
en.json, ja.json을 기계번역하는 스크립트 실행 (이때는 어떤 번역기를 써도 된다. Claude든, ChatGPT든, DeepL이든 상관없다. 이미 키가 정리된 상태라서, 순수 번역 작업만 남는다.)
•
특수 케이스
◦
변수 포함 텍스트
◦
컴포넌트가 포함된 메시지
◦
직접 수동 처리
“일반적인 텍스트”만 자동화했고 문자열 안에 변수가 있거나, 컴포넌트가 섞여 있는 경우는 일부러 제외했다. 이런 특수 케이스 많지 않았고 AST로 각기 다른 케이스를 처리하는 스크립트를 만드는 것보다 직접 처리하는 게 더 빠르고 정확했기 때문이다.
next-intl 사용 방법 정리
next-intl을 실제로 써보면, 사용 패턴은 크게 네 가지로 나뉜다.
1. 가장 기본적인 사용: 단순 텍스트
가장 흔한 케이스다. JSX 안에 있던 한국어 텍스트를 t()로 감싸는 방식이다.
const t =useTranslations();
return (
<p>{t("파일 업로드에 실패했습니다")}</p>
);
TypeScript
복사
flat key를 사용하고 있기 때문에 키 이름을 고민할 필요가 없다. 기존 텍스트를 그대로 감싸면 된다. 다국어 적용의 진입 장벽이 가장 낮은 형태다.
2. 변수가 포함된 메시지
사용자 이름, 숫자, 상태값처럼 동적으로 바뀌는 값이 들어가는 경우다.
const t =useTranslations();return (<p>
{t("총 {count}개의 파일이 업로드되었습니다", {
count: files.length,
})}</p>
);
TypeScript
복사
메시지 안에 {}로 변수를 선언하고, 두 번째 인자로 값을 전달한다.
이 방식은 JSON 번역 파일에서도 그대로 유지되기 때문에, 번역 품질 관리도 비교적 쉽다.
3. 메시지 안에 컴포넌트가 들어가는 경우: t.rich
링크, 강조 텍스트, 버튼처럼 React 컴포넌트가 메시지 일부로 들어가야 할 때는 t.rich를 사용한다.
const t =useTranslations();
return t.rich("자세한 내용은 {link}를 확인해주세요", {
link:(chunks) =><ahref="/docs">{chunks}</a>,
});
TypeScript
복사
텍스트 구조는 번역 파일에 유지하면서, 표현은 컴포넌트로 분리할 수 있다.
다만 이 케이스는 자동화하기 어렵다. 앞에서 말한 LLM 스크립트에서도 이런 경우는 일부러 제외했고, 사람이 직접 처리했다.
4. React 컴포넌트 밖에서 메시지가 필요한 경우
유틸 함수, 에러 핸들러, 서버 로직에서도 메시지가 필요하다. 이럴 때는 두 가지 방법으로 번역이 가능하다.
4-1. 메시지를 파라미터로 전달
functionshowError(message:string) {
toast.error(message);
}
TypeScript
복사
showError(t("파일 업로드에 실패했습니다"));
TypeScript
복사
가장 단순한 방식이다. 호출하는 쪽에서 번역을 책임진다.
4-2. t 함수 자체를 전달
function createErrorMessage(t: (key:string, values?: Record<string,any>) => string
) {
return t("파일 업로드에 실패했습니다");
}
TypeScript
복사
const t =useTranslations();
const message =createErrorMessage(t);
TypeScript
복사
이 방식은 유틸 함수 안에서 번역 키를 관리하고 싶을 때 유용하다.
특히 에러 메시지처럼 여러 곳에서 재사용되는 경우에 많이 쓰게 된다.
후기
이번 다국어 작업을 하면서 느낀 점은 꽤 단순했다. 다국어는 번역 문제가 아니었다. 구조와 도구 선택의 문제였다.
처음에 Lingui를 선택했던 이유도, 다시 next-intl로 옮기게 된 이유도 결국은 같다. 기존 코드베이스를 얼마나 덜 흔들면서, 프레임워크가 밀어주는 방향을 얼마나 잘 탈 수 있느냐의 문제였다.
Lingui는 여전히 좋은 라이브러리다. 특히 flat key 기반 철학은 대규모 코드베이스에서 굉장히 강력하다. 키 재사용이 쉽고, 검색성이 좋고, 다국어 도입이 곧 코드 리팩토링으로 이어지지 않는다는 점은 큰 장점이다. 이 철학 덕분에 기존 서비스에 다국어를 빠르게 얹을 수 있었다.
반면 next-intl은 Next 15와의 궁합이 정말 좋다. Next 15의 기본 전제를 거의 그대로 받아들인다. 다국어를 붙이기 위해 프레임워크 설정을 비틀 필요가 없다는 점에서, 장기적으로 유지하기 훨씬 편하다.
결국 이 프로젝트에서는 둘 중 하나를 고르는 대신, 두 라이브러리의 장점을 섞었다.
프레임워크 레벨에서는 next-intl을 사용하고, 번역 키 관리 방식은 Lingui의 flat key 구조를 가져왔다. 그리고 key extraction처럼 빠진 퍼즐은 LLM을 이용한 자동화로 메웠다.
그 결과, 한국어 단일 서비스였던 코드베이스에 영어와 일본어를 하루 만에 얹을 수 있었다. 다국어 지원이 더 이상 무거운 프로젝트가 아니라, “이번 스프린트에서 같이 처리할 수 있는 작업” 정도로 내려왔다.
만약 Next 15 환경에서 다국어를 고민하고 있다면, 라이브러리 이름부터 고르기보다는 이런 질문부터 던져보는 게 도움이 된다.
•
우리 서비스는 locale을 상태로 관리해야 할까, 아니면 라우팅으로 풀 수 있을까?
•
다국어 때문에 빌드 전략이나 개발 경험을 희생해도 괜찮을까?
•
번역 키는 의미 중심으로 관리하는 게 나을까, 문자열 중심이 나을까?
이 질문들에 대한 답이 정리되면, Lingui든 next-intl이든 선택은 훨씬 쉬워진다. 그리고 꼭 하나만 고를 필요도 없다. 상황에 맞게 섞어 쓰는 것도 충분히 현실적인 선택이다.
듀오링고로 일본어를 공부한지 400일이 넘었는데 나중에는 기계번역된 일본어를 검수할 수 있는 날이 오기를…! ^0^
_(1).jpeg&blockId=0e552736-74f0-4f5a-89e1-328d4931ca7c)

