홈으로 이동컴포넌트 라이브러리로 이동이모티콘 스토리지로 이동
GitHub 프로필 새 창으로 열기
TOPIA Live를 세계로 - i18n 전환기 배너
2026년 8월 2일·읽는 데 약 8분

TOPIA Live를 세계로 - i18n 전환기

관련 글

AI로 학생의 성장을 피드백하기까지: TOPIA Growth 시스템 개발기

2026. 6. 21.

유료 라이브러리 PSPDFKit 대체 :: PDF 에디터 내재화 여정

2026. 3. 31.

OpenClaw로 hotfix 대응팀 구축하기 (TOPIA Harness)

2026. 3. 6.

문제상황

국내 초등, 중등 저학년을 대상으로 서비스하던 :topialive: TOPIA Live가 해외로 진출을 계획하고 있습니다. 유력 후보는 베트남이지만 어느 나라로 언제 나갈지에 대한 구체적인 계획은 확정되지 않았어요. 그래도 미리 준비돼 있어야 원하는 시점에 바로 확장할 수 있기 때문에, 작업을 먼저 해두는 편이 좋다고 판단하여 작업에 착수하게 되었습니다.

작업에 들어가려고 살펴보니, 우리 서비스 코드는 처음부터 i18n을 고려하고 구축된 프로젝트는 아니더라고요. 화면에 보이는 대부분의 텍스트가 컴포넌트에 하드코딩돼 있었고, 서버가 코드값이 아니라 완성된 한국어 문장을 내려주는 API도 존재했거든요. 예를 들어 과제 API의 buttonText에는 사진 등록하기, PDF 제출하기, 온라인학습에서 과제하기 같은 문구가 그대로 내려오고 있었죠.

타임존 문제 또한 안고 있었어요. 남아공과 필리핀에서 접속하는 외국인 강사와 해외에서 수업을 듣는 학생이 존재하는데도 수업과 과제 마감기한이 전부 KST 기준으로 나가고 있었거든요. 예를 들어, 시간표에 오후 7시라고 적혀 있으면, 남아공 강사는 그게 자기 시간으로 몇 시인지 매번 직접 계산해야 되는거죠. 이번 기회에 반드시 해결해야 되는 문제였습니다.

작업의 오버뷰를 보면 결코 간단한 작업은 아니었어요. 프론트엔드와 백엔드, DB까지 총 6개의 repo를 동시에 건드려야 하는 작업이었으며, 작업량이 많아 충돌 방지를 고려하여 운영 서버에 올리는 전략 또한 중요했습니다.

i18n 라이브러리 선택하기

기억나실 지 모르겠지만 기존 TOPIA Live의 프론트엔드 소스는 CRA와 Craco로 동작하던 클라이언트 사이드 렌더링 프로젝트였습니다. 사실 그당시 i18next와 react-i18next와 같은 i18n 라이브러리가 이미 설치되어 있긴 했었어요.

한국어와 영어 번역 JSON이 합쳐서 460줄쯤 됐지만, 실제로 키를 읽던 화면은 아이디 찾기, 비밀번호 재설정, 회원가입, 학생 홈 시간표 정도의 초창기에 만들어진 컴포넌트 일곱 개뿐이었습니다. 나머지 화면은 여전히 하드코딩된 텍스트를 직접 쓰고 있어서 사실상 사용하지 않는 기능이었습니다.

더군다나 우리 프로젝트는 Next.js App Router로 마이그레이션을 했었죠. 이에 따라 이제는 다국어 처리 시, 클라이언트 사이드 뿐만 아니라 RSC와 generateMetadata 처럼 서버에서 먼저 실행되는 부분까지 고려되어야 합니다.

Vercel에서 Next.js를 위해 자체적으로 만든 다국어 라이브러리는 없지만, App Router, Page Router에 따라 라이브러리를 추천해주고 있었는데요.

https://nextjs.org/docs/app/guides/internationalization

패키지주간 다운로드
next-intl약 484만
@lingui/core약 134만
@lingui/react약 95만
@inlang/paraglide-js약 44만
@tolgee/react약 10만
intlayer약 8만
next-i18n-router약 7.6만
next-intlayer약 5.3만
next-international약 5만
gt-next약 2.3만

보시다싶이 next-intl가 다른 패키지에 비해 압도적인 다운로드를 자랑하고 있습니다. 다운로드가 많다는 건 유저들이 많이 사용하게 되면서 참고할 수 있는 리소스도 많이 생성하게 되는 효과가 있으므로, 자연스럽게 관심이 갔죠.

아래 특징들로 인해 최종적으로 next-intl 라이브러리를 최종적으로 선택하게 되었습니다.

  1. Server Component 안에서도 Client Component와 비슷하게 번역이 바로 동작한다고 해요. react-i18next였다면 번역 때문에 컴포넌트를 'use client'로 바꾸거나 서버용 인스턴스를 직접 만들어야 했을 텐데, 그 비용이 사라지는 거죠.
  2. 다국어 URL을 위해 모든 경로 앞에 [locale]을 붙이는 게 App Router의 일반적인 방식입니다. 하지만 next-intl은 URL 대신 쿠키로 locale을 정하는 방식도 공식 지원해서, 기존 URL을 그대로 사용할 수 있어요.
  3. 강조 색이나 줄바꿈이 들어간 JSX 문장은 이를 조각내서 조립해야 되는데, 그러면 어순이 다른 영어·베트남어에서 문장을 다시 짜기가 곤란해요. next-intl에서 제공하는 t.rich는 문장 전체를 번역 파일에 두고 태그 위치만 언어마다 옮기면 되니, 화면 구조를 건드리지 않고 번역할 수 있습니다.
  4. 기존 헬퍼는 존재하지 않는 번역 키를 만나면 빈 문자열을 돌려줘서, 번역이 빠지면 에러 없이 빈 화면이 나왔어요. 하지만, next-intl은 키 자동완성과 타입 검증을 지원해서, 잘못된 키를 런타임이 아니라 빌드 단계에서 잡을 수 있습니다.

locale 정본을 어디로 둘 것인가?

다음은 locale의 정본을 어디에 둘지 정할 차례입니다. 사실 이 부분에서 고민을 많이 했는데요. 보통 다국어를 지원하는 사이트를 보면 아래와 같이 언어별로 URL을 다르게 가져가는 경우를 볼 수 있습니다.

형태예시쓰는 곳
경로 prefix (sub-path)example.com/ko/pricing, example.com/en/pricing가장 흔함. App Router [locale] 세그먼트가 이 방식
서브도메인ko.example.com, en.example.com위키피디아 등
국가별 도메인 (ccTLD)example.co.kr, example.jp아마존, 구글 등 국가 단위 운영
쿼리 파라미터example.com/pricing?lang=koSEO에 불리해서 요즘은 잘 안 씀

기존 TOPIA Live의 페이지 주소는 locale 정보가 없는 단일 URL로 구성돼 있습니다. 성적표나 Monthly Report 발급 소식도 알림톡으로 이 URL을 직접 보내 접속을 유도하고 있고요.

물론 URL 구조를 바꾸더라도 next config에 영구 redirect를 걸어 두면 기존 링크와 호환 자체는 됩니다만, 조금 다른 방법을 써보고 싶었어요.

우선, SEO 이점을 가져가려면 어쩔 수 없이 언어별 URL이 필요합니다. 크롤러는 URL 단위로 색인하고, 쿠키를 들고 다니지 않으니까요.

TOPIA Live는 모든 페이지가 동일한 중요도를 가지고 있지 않습니다. 메인 서비스들이 모두 로그인 이후 결제를 하고 나서 제공되기 때문에, 결제하지 않은 사용자가 리텐션을 유지할 만한 페이지가 딱히 없어요. 즉, 여러 나라 사용자에게 노출해야 하는 페이지가 몇 개로 한정돼 있다는 뜻이죠. 게다가 국가별로 컨텐츠를 따로 제공할 건 아니어서 로그인 이후 페이지들은 굳이 URL까지 나눠가면서 분리할 필요는 없습니다.

이에 따라 랜딩, 로그인, 공지사항, FAQ처럼 로그인 이전에 접근하는 메인 계열 페이지에는 언어별 URL 방식을 살리고, 로그인 이후 페이지는 쿠키를 locale의 정본으로 두고 운영하기로 했습니다. 이렇게 하면 검색에 필요한 페이지만 언어별 URL로 색인하면서도, 이미 공유된 학생·학부모·강사 링크는 그대로 둘 수 있거든요.

지원하는 locale은 ko-KR, en-US, vi-VN 세 언어로 우선 고정하고 진행했어요.

위에서 언급한대로, 메인 계열 페이지의 언어 기준은 URL입니다. /vi/notice처럼 locale이 붙은 주소로 들어오면 화면을 베트남어로 렌더링하고, 그 값을 TOPIA_LOCALE 쿠키에도 넣어둬요.

다만 locale이 포함되지 않은 /notice 같은 주소로 진입하는 경우도 고려해야 합니다. 이때는 해당 주소에서 바로 화면을 그리지 않고, 아래 우선순위로 locale을 결정한 뒤 언어가 명시된 URL로 리다이렉트하도록 설계했습니다.

plain text
Copied
1TOPIA_LOCALE 쿠키 → Accept-Language → ko-KR

이전에 언어를 선택한 이력이 있다면 쿠키를 따르고, 첫 방문이라면 브라우저의 언어 설정을 참고합니다. 어느 쪽도 없을 때 기본값인 한국어로 안내하는 수순이죠.

로그인 이후에는 사용자 프로필의 locale을 정본으로 두게 됩니다. 로그인 시점에 유저 데이터의 locale을 읽어 쿠키와 동기화하고, 값이 달라졌을 때만 라우터를 새로고침해요.

사용자가 언어를 바꾸면 쿠키를 갱신한 뒤 라우터를 리프레쉬하여 서버·클라이언트가 같은 locale로 맞춰지게 했고, 메인 계열로 돌아가는 링크에도 현재 locale을 붙여 두 영역이 어긋나지 않게 했습니다. 또한, 로그아웃하더라도 쿠키는 남겨서, 같은 브라우저라면 선택한 언어가 그대로 이어져요.

이렇게 하면 어느 상황에서든 locale의 정본이 하나로 고정됩니다. 비로그인 메인 계열에서는 URL, 그 밖의 경로에서는 쿠키, 로그인 후에는 프로필로 말이죠. 전체적인 구조는 아래와 같습니다.

plain text
Copied
1요청 진입2│3├─ [1] 메인 계열 경로 (랜딩·로그인·공지·FAQ)4│   │5│   ├─ [1-1] URL에 locale 있음 (/en/notice)6│   │    ├─ 지원 locale (ko·en·vi) → 정규화(en → en-US) 후 URL locale로 렌더7│   │    │    ├─ TOPIA_LOCALE 쿠키를 URL 값으로 갱신8│   │    │    └─ 로그인 중이고 프로필 locale과 달라도 → URL 우선9│   │    │         (공유 링크의 의도 존중, 프로필은 사용자가 직접 바꿀 때만)10│   │    └─ 미지원 locale (/fr/notice) → 기본 locale URL로 리다이렉트11│   │12│   └─ [1-2] URL에 locale 없음 (/notice)13│        ├─ 로그인 상태 → 프로필 locale의 URL로 리다이렉트 (/en/notice)14│        └─ 비로그인 → 쿠키 → Accept-Language → ko-KR 순으로 결정 후15│                      해당 locale URL로 리다이렉트16│17├─ [2] 로그인 이후 경로 (locale prefix 없음, /student/... 등)18│   │19│   ├─ [2-1] 로그인 상태 → 프로필 locale이 정본20│   │    └─ 쿠키와 다르면 쿠키 동기화 + 라우터 새로고침21│   │22│   └─ [2-2] 세션 없음/만료 → 로그인 페이지로 이동23│        └─ 그 화면의 언어는 쿠키 → Accept-Language → ko-KR24│25└─ [3] 상태 전환 이벤트26    │27    ├─ [3-1] 언어 변경 (메인 계열에서)28    │    └─ URL locale 교체 (/ko/... ↔ /en/...) + 쿠키 갱신29    │         └─ 로그인 중이어도 프로필 locale은 갱신하지 않음30    │31    ├─ [3-2] 언어 변경 (로그인 이후 화면에서)32    │    └─ 프로필 + 쿠키 갱신 → 라우터 새로고침으로 즉시 반영33    │34    ├─ [3-3] 로그인 성공35    │    └─ 프로필 locale을 쿠키에 동기화 (이후 프로필이 정본)36    │37    └─ [3-4] 로그아웃38         └─ 프로필 정본은 소멸, 쿠키는 유지39              → 같은 브라우저에서는 선택한 언어가 이어짐

프론트엔드 i18n

기반이 잡혔으니 이제 본격적으로 프론트엔드에 박혀있는 한국어를 걷어낼 차례입니다. 먼저 작업을 시작했던 버튼이나 라벨처럼 짧은 문구는 양이 많을 뿐, 번역 키로 옮기는 작업 자체는 단순했어요.

typescript
Copied
1// before2<button>과제 제출</button>34// after5<button>{t('homework.submit')}</button>
json
Copied
1// ko-KR.json2{3  "homework.submit": "과제 제출"4}56// en-US.json7{8  "homework.submit": "Submit homework"9}

어려웠던 건 한글 어순을 전제로 조립된 문장이었는데요. 예를 들어 제목 안에 줄바꿈과 강조 색상이 섞여 있으면, 기존에는 JSX를 여러 조각으로 나눠 렌더링하고 있었어요.

typescript
Copied
1// Before2<h3>3  전국에서, 해외에서4  <br />5  <span className='text-primary'>실력 있는 아이들</span>은 모두 모이고 있습니다6</h3>

위 구조에서 강조할 단어와 줄바꿈 위치까지 코드에 고정하면 영어와 베트남어에서는 어순을 바꿀 수 없어 재활용할 수 없는 구조가 됩니다. 그래서 문장 전체를 메시지로 옮기고, 필요한 표현 요소만 태그로 남기는 방식으로 대응하게 됩니다.

json
Copied
1// ko-KR.json2{3  "headline": "전국에서, 해외에서<br></br><highlight>실력 있는 아이들</highlight>은 모두 모이고 있습니다"4}56// en-US.json7{8  "headline": "<highlight>Talented students</highlight> from across the country and abroad<br></br>are all gathering here"9}
typescript
Copied
1// After2<h3>3  {t.rich('headline', {4    br: () => <br />,5    highlight: (chunks) => <span className='text-primary'>{chunks}</span>,6  })}7</h3>

영어 번역은 <highlight>의 위치를 문장 앞으로 옮길 수 있어요. 컴포넌트는 같은 구조를 유지하면서도 언어마다 자연스러운 어순을 쓸 수 있게 된 거죠. 반복되는 br, highlight, strong 매핑은 공통 rich text 헬퍼로 분리했습니다.

문장을 조립하여 이어붙이는 코드도 next-intl을 통해 쉽게 대응할 수 있었습니다. 변수를 포함한 문장 전체를 하나의 메시지로 두고, 변수 부분만 선언적으로 입력을 받아서 채워서 대응했습니다.

typescript
Copied
1// Before2<p>{studentName} 학생은 {levelName} 레벨입니다</p>
typescript
Copied
1// ko-KR.json2{3  "levelResult": {4    "description": "{studentName} 학생은 {levelName} 레벨입니다"5  }6}78// en-US.json9{10  "levelResult": {11    "description": "{studentName} is at the {levelName} level"12  }13}
typescript
Copied
1// After2<p>3  {t('levelResult.description', { studentName, levelName })}4</p>

이 외에도 손봐야할 요소들은 무수히 많았어요. '출결체크' 처럼 한글이 상태값으로 들어가있으면서 동시에 렌더링까지 이어지는 안티 패턴을 모두 끊어놔야 했으며, 이미지 안에 한글 텍스트가 포함되어 있는 부분, 다국어 번역을 하면 레이아웃이 깨지는 컴포넌트들, 눈에는 보이지 않지만 접근성을 위해 고려되었던 부분들까지 거의 모든 컴포넌트를 방문한 느낌이었습니다.

백엔드 i18n

프론트엔드 다국어 작업을 마무리하고 뿌듯한 마음으로 페이지를 전체적으로 둘러보니, 많은 부분이 처리되어 있음에 뿌듯함을 느낄 수 있었습니다. 하지만, 부분부분 한국어가 여전히 튀어나오는 곳이 있으며, 이제 이런 부분은 백엔드의 책임이겠죠.

한국어로 보이는 대부분은 한국어 string이 그대로 서버에서 내려오고 있는 상황이었고, 각 API를 전수조사해본 결과, 백엔드 쪽에서의 작업도 데이터의 성격에 따라 작업방식을 카테고리로 분류해서 작업을 해야 했습니다. 백엔드 i18n의 핵심은 각 데이터를 다국어로 어떻게 보여줄 지 정책을 결정하는 것이라고 생각해요.

예를 들어, 모든 부분을 다 귀찮으니 번역 버튼을 달아버릴 수도 있는 것이고, 모든 부분을 관리자가 여러 언어로 하도록하여 입력이 된 언어만 보이게 하는 방법도 있잖아요. 하지만, 한 방향으로만 진행하는 것은 한 쪽 사이드의 유저의 UX를 크게 침해하는 방법이라 지양하고 싶었어요.

저희 서비스에서 데이터의 성격에 따라 분류한 작업 방식은 아래와 같습니다.

분류예시처리 방식
고정된 화면 문구와 상태버튼, 에러, 출결 상태서버는 코드만 전달하고 프론트 메시지 카탈로그에서 표시
운영자가 언어별로 직접 작성하는 콘텐츠공지, 약관, FAQlocale별 본문과 공개 여부를 별도 저장하고 어드민에서 관리
하나의 데이터에 같은 의미의 번역을 붙이는 콘텐츠시험명, 콘텐츠 설명, 과제 버튼공통 데이터와 번역 데이터를 분리하거나 *I18n 맵으로 저장
미리 번역할 수 없는 사용자 생성 콘텐츠문의, 답변, 강사 코멘트, 학생 제출물원문을 보존하고 필요할 때만 번역 기능 제공
값은 같고 표현만 달라지는 데이터날짜, 시간, 숫자, 점수, 퍼센트원시 값을 전달하고 클라이언트에서 locale에 맞게 포맷

공지사항, 약관, FAQ는 관리자가 직접 입력하고 관리해야 하는 중요한 데이터입니다. 그래서 관리자 화면에 한국어·영어·베트남어 입력을 각각 제공했어요. 입력되지 않은 언어가 있다면 그 데이터는 프론트엔드에서 보이지 않습니다. Fallback으로 한국어나 영어를 대신 채워 내려줄 수도 있었지만, 이는 입력된 언어에만 노출하는 것 자체가 의도된 동작이니까요. 특정 국가에만 보여주고 싶은 안내를 데이터 입력만으로 통제할 수 있게 되는 겁니다.

DB에는 다국어 전용 테이블을 따로 만들어서 연결하고, 언어별 데이터를 row로 쌓았습니다. 지원 언어가 늘어날수록 언어마다 데이터가 선택적으로 쌓이게 되는데, 컬럼을 늘리는 방식보다 row로 쌓는 쪽이 안전하다고 판단했어요.

반면 단어 학습 콘텐츠의 뜻이나 입학시험의 시험명처럼 서비스 동작에 꼭 필요한 데이터는 성격이 조금 다릅니다. 번역된 데이터가 없다고 화면에서 숨겨버리면 유저는 상당히 어색한 경험을 하게 됩니다. 따라서 이런 계열의 데이터는 기존에 한글이 들어 있던 컬럼이 title이라면 그 옆에 titleI18n 컬럼을 새로 만들고, 언어별 데이터를 JSON 구조로 담았어요. 만약 조회할 때 요청한 언어의 값이 없으면 기본 언어로 fallback하게 했습니다.

typescript
Copied
1// entranceExamSchedules.titleI18n2{3  "ko-KR": "2026년 1학기 TOPIA 입학시험",4  "en-US": "2026 Spring TOPIA Entrance Exam"5}

마지막은 번역본을 DB에 미리 저장하지 않고, 필요할 때 번역 API로 제공하는 방식입니다. 문의와 답변, 강사 코멘트 같은 데이터는 중앙에서 만들어 뿌리는 콘텐츠가 아니라 유저끼리 주고받는 대화거든요. 미리 번역해 둬야 할 데이터의 양이 감당이 안 될뿐더러, 언어 조합도 유저마다 달라서 일일이 준비하는 건 한계가 있었어요.

그래서 읽는 시점에 번역하기로 했습니다. 원문의 언어는 예측할 수 없으니 번역 API의 source는 자동 감지로 두고, destination은 읽는 사람 기준으로 정했어요. 학생과 학부모는 자신이 설정한 locale로, 강사는 영어로 번역됩니다.

번역 도구는 데이터 성격에 따라 나눴어요. 문의하기(채팅)처럼 앞뒤 맥락이 있어야 자연스러운 데이터는 LLM API를 써서 이전 대화를 함께 넣고, 시스템 프롬프트로 주어가 어색해지지 않게 잡아줬습니다. 반대로 맥락 없이 문장 단위로 번역해도 충분한 데이터는 Google Translate API로 처리했죠. 같은 원문을 반복해서 번역하지 않도록 번역 결과는 캐싱해 뒀습니다.

마이그레이션 전략

여러 레포의 코드베이스 전체를 바꾸는 작업이라, 언제 끝날지 모르는 다국어 작업을 한 번에 머지했다가는 지옥 같은 충돌을 마주할 게 분명했습니다. 그래서 작은 단위로 쪼개 운영 서버에 미리 올리되, 본격적인 다국어 전환 전까지 기존 유저가 변화를 전혀 눈치채지 못하게 만들어야 했어요.

프론트엔드, 백엔드, DB 모두 작은 단위로 운영 서버까지 배포하고, 그때마다 사이드 이펙트가 없는지 확인하는 흐름으로 진행했습니다. 예를 들어 프론트엔드의 일부 API는 이미 locale을 파라미터로 넘기고 있지만, 운영 서버에서는 ko-KR을 강제하는 플래그를 걸어 기존과 완전히 같은 응답이 나가게 했어요. 나중에 이 플래그만 풀면 준비해 둔 다국어 동작이 그대로 살아나는 구조죠.

DB 마이그레이션은 특히 신중해야 했습니다. 순서 자체는 일반적인 마이그레이션과 크게 다르지 않았고, 아래 다섯 단계로 진행했어요.

  1. 공통 데이터와 locale 데이터를 분리한 새 스키마를 만든다.
  2. 기존 한국어 데이터를 ko-KR localization으로 이관한다.
  3. 검수가 끝난 영어, 베트남어 번역만 추가한다.
  4. 어드민의 입력 경로와 Live의 조회 경로를 새 구조로 바꾼다.
  5. 운영 데이터와 rollback 가능성을 확인한 뒤 legacy 컬럼과 테이블을 정리한다.

마지막 퍼즐, Timezone 정책

정책을 정하고 나서부턴 대부분이 단순 반복과 테스트의 연속이었던 재미없는 작업이 드디어 마무리가 되었습니다. 기존 서비스의 타임존은 한국에서 운영 중인 서비스이기 때문에 당연히 KST를 기준으로 삼았지만, 아쉬운 점은 서버에 저장되는 타임존도 KST였다는 점입니다. 이를 다국어 처리를 위해 절대 시점을 나타내는 시간은 UTC로 저장하고, ISO 8601 형태로 주고받도록 변경했어요.

다음으로는 시간대 변환 시점에 대해 신중해야 했습니다. UTC로 저장되어 있는 타임스탬프를 백엔드 서버나 Next.js 서버 쪽에서 파싱해 로컬 시간으로 바꾸면, 서버가 어느 리전에 떠 있느냐에 따라 결과가 달라지기 때문이에요. 그래서 서버는 UTC 원본만 다루고, 클라이언트가 브라우저의 time zone을 읽어 사용자 위치 기준으로 변환해 표시하도록 했어요. 서버 HTML과 hydration 이후 화면이 어긋나지 않도록, 로컬 시간 포맷은 hydration 이후에만 적용했어요.

물론 기존 기준이던 KST는 버리지 않았습니다. 변환된 시간 옆에 아이콘을 두고, 툴팁으로 KST 기준 시간을 함께 보여줬어요. 수업 운영의 기준 시간이 궁금할 때 바로 확인할 수 있게요. 덕분에 남아공의 강사와 베트남의 학생이 같은 수업을 각자의 시간으로 볼 수 있게 됐습니다.

회고

돌이켜보니 작업량이 꽤 됐습니다. 그리고 그 대부분이 단순 변경의 반복이었고요. AI 에이전트가 없던 시절에 i18n을 고려하지 않고 짜인 프로젝트를 통째로 다국어 처리하려 했다면, 꽤나 흥미롭지 않은 나날의 연속이었을 것 같아요.

기반 작업은 나름 탄탄하게 마무리했습니다. 이제 어떤 언어가 추가되더라도 번역 데이터만 채우면 되는 구조예요. 지금 분위기로 보아하니 다음은 일본어와 중국어가 될 것 같네요.

사실 해외 진출에서 다국어만큼 중요한 게 해외 결제입니다. 국내야 토스페이먼츠로 정리돼 있지만, 해외 결제 PG는 따로 마련해야 하더라고요. 이 부분은 팀의 시니어 개발자분이 맡아주셨는데, 한국 법인이 해외 PG를 쓰는 게 만만치 않아 보였습니다. 실제로 Stripe 연동을 일반 결제, 자동 결제, 환불, 카드 등록 워크플로에 다중 통화 결제까지 통째로 구현해 뒀는데, 다른 나라에는 다 열려 있는 기능이 한국에만 풀리지 않는 식의 난관이 이어졌고, 결국 한 달이 채 되지 않아 그 코드를 전부 걷어내는 걸 옆에서 지켜봤어요. 토스페이먼츠가 제공하는 해외 결제로 일단락되는 분위기인데, 수수료가 무려 4~5%나 된다니.. 충격적입니다.

TOPIA Live가 국내를 넘어, 대한민국 사교육을 해외에 널리 알리는 첫 발걸음이 되었으면 좋겠습니다.

  • 문제상황
  • i18n 라이브러리 선택하기
  • locale 정본을 어디로 둘 것인가?
  • 프론트엔드 i18n
  • 백엔드 i18n
  • 마이그레이션 전략
  • 마지막 퍼즐, Timezone 정책
  • 회고