기존 서비스 국제화(i18n) 작업 쉽게 덜어내기: t 함수 자동 래핑 스크립트 만들기
안녕하세요.
인프랩의 랠릿 셀 프론트엔드 개발자 고슈, 도니, 루시입니다.
2024년 초, 연간 플래닝에서 CTO인 향로에게서 예상치 못한 발표를 듣게 됩니다.
올해 인프런 글로벌 서비스를 출시할 예정이에요. 랠릿 셀도 함께 투입됩니다.
네? 저희도요?
이렇게 랠릿을 포함한 인프랩 개발파트는 레거시 코드 정리부터 국제화 작업까지, 인프런의 글로벌 서비스 출시를 위한 새로운 도전을 시작하게 되었습니다.
프롤로그
i18n을 서비스에 적용하는 전체 과정보다는, 기존 서비스를 다국어 서비스로 전환하는 과정에서 i18n 을 효율적으로 적용하는 방법에 대해 공유하고자 합니다.
이후 적용 과정에 대한 상세한 내용은 후속 글에서 다루도록 하겠습니다.
i18n은
internationalization
의 줄임말입니다. ‘i’와 ‘n’ 사이에 18개의 글자가 있어서 i18n이라고 부릅니다.
또한, 소프트웨어나 웹사이트를 다양한 언어와 지역에 맞게 적용할 수 있도록 설계하고 개발하는 과정을 의미합니다.
Next.js 다국어 처리
들어가기 앞서, Next.js 기반의 프로젝트에서 다국어 지원을 위해 일반적으로 사용되는 next-i18next
라이브러리에 대해 먼저 살펴보겠습니다.
next-i18next는 Next.js 애플리케이션에서 i18n 기능을 구현하는 라이브러리입니다.
주요 특징
- useTranslation hook을 통해 번역 함수(t) 제공
- 언어 파일을 JSON으로 관리
- 서버사이드 렌더링 지원
- 동적 번역 및 네임스페이스 지원
간단한 사용 예시를 보면 다음과 같습니다.
// public/en/sample.json
{
"안녕하세요": "hi"
}
import { useTranslation } from 'next-i18next';
const Component = () => {
const { t } = useTranslation();
return (
<p>{t('안녕하세요')}</p> // hi 라고 노출
);
}
이제 이 라이브러리를 활용하여 어떻게 효율적으로 기존 서비스에 국제화를 적용했는지 살펴보도록 하겠습니다.
언어 자원의 키
기존 인프런 담당 셀에서 레거시 코드의 React 마이그레이션이 진행을 진행중에 있어서 랠릿 셀은 이미 React로 전환된 코드들에 대한 i18n 적용을 가장 먼저 담당하게 되었습니다. 따라서 다른 셀과 함께 작업 하면서 사용할 효율적인 컨벤션을 먼저 결정할 필요가 있었습니다. 이 과정에서 가장 중요한 초기 결정 사항 중 하나는 다음과 같았습니다.
언어 자원(번역을 위한 JSON 파일)의 키를 어떤 방식으로 정의할 것인가?
이 문제에 대한 해답을 찾기 위해 다양한 개발자들과의 네트워킹을 하며 인사이트를 얻었습니다. 그 중 기억에 남는 의견이 있었습니다.
다음에 국제화를 진행한다면, 반드시 키값은 한글로 설정할 거예요.
이 의견에 많은 개발자들이 동의했고, 그 이유를 물어보니 다음과 같은 공통된 피드백을 얻을 수 있었습니다.
- 영문 키를 사용할 경우 대응되는 번역 키를 찾는 과정이 번거로움
- 이로 인한 개발 생산성 저하 발생
이러한 인사이트를 바탕으로, 랠릿셀은 함께 언어 자원의 키 정의에 대한 세 가지 컨벤션을 수립하고, 각각의 장단점을 분석하여 PM과 PD에게 제안하게 되었습니다.
컨벤션 종류
서비스의 국제화를 위한 언어 자원 키 정의 방식을 세 가지로 분류하여 분석했습니다.
1. 한글 키 방식
한국어 텍스트를 그대로 키로 사용하는 방식입니다.
{
"MY 카테고리": "MY 카테고리",
"나만의 강의 카테고리를 구성해보세요!": "나만의 강의 카테고리를 구성해보세요!"
}
// 구현 예시
<Text>{t("MY 카테고리")}</Text> // MY 카테고리
<Text>{t("나만의 강의 카테고리를 구성해보세요!")}</Text> // 나만의 강의 카테고리를 구성해보세요!
장점
- 키 정의 과정 생략 가능
- 빠른 다국어 전환 가능
- 직관적인 코드 가독성
단점
- 텍스트 중복 가능성
- 원본 텍스트 변경 시 키도 함께 수정 필요
- 언어별 1:1 매칭이 어려운 경우 존재 (개행 등)
2. 역할군 기반 키 방식
{페이지 | 도메인}.{섹션}.{역할}.{서브}
형태의 구조화된 한글 키
{
"계정정보.마이카테고리.제목": "MY 카테고리",
"계정정보.마이카테고리.설명": "나만의 강의 카테고리를 구성해보세요!"
}
// 구현 예시
<Text>{t("계정정보.마이카테고리.제목")}</Text> // MY 카테고리
<Text>{t("계정정보.마이카테고리.설명")}</Text> // 나만의 강의 카테고리를 구성해보세요!
장점
- 체계적인 키 구조
- 명확한 네이밍 규칙
단점
- 수동 키 정의 필요
- 컨벤션 러닝 커브 존재
- UI 변경 시 키 수정 필요
3. 영어 역할군 기반 키 방식
{page|domain}.{section}.{role}.{sub}
형태의 구조화된 영문 키
{
"my.settings.account.mycategory.title": "MY 카테고리",
"my.settings.account.mycategory.description": "나만의 강의 카테고리를 구성해보세요!"
}
// 구현 예시
<Text>{t("my.settings.account.mycategory.title")}</Text> // MY 카테고리
<Text>{t("my.settings.account.mycategory.description")}</Text> // 나만의 강의 카테고리를 구성해보세요!
장점
- 체계적인 키 구조
- 텍스트 변경에 독립적인 키 관리
- 표준화된 네이밍
단점
- 수동 키 정의 필요
- 컨벤션 학습 필요
- UI 변경 시 키 수정 필요
- 비개발 직군의 낮은 직관성 (URL 구조 이해 필요)
결정 사항
최종적으로 1. 한글 키 방식
을 채택했습니다.
주요 결정 요인은 다음과 같습니다.
- 개발 및 디자인 팀의 빠른 작업 속도 확보
- 직관적인 키 관리로 인한 생산성 향상
- 프로젝트의 시간 제약 상황 고려
이러한 결정은 빠른 구현과 팀 전체의 효율성을 우선시한 결과였습니다.
자동화 스크립트
배경과 아이디어
i18next-parser나 i18next-scanner는 i18n이 적용된 코드에서 번역 키를 추출하여 JSON 파일을 생성해주는 기능을 제공합니다.
컨벤션도 정해졌으니, 이제 기존에 있던 한글들을 모두 t로 감싸주면 됩니다.
…언제 다 하죠?
이 한숨 앞에서 괜찮은 생각이 스쳐 지나갔습니다.
한글만 쏙 뽑아서 t로 말아주는 스크립트 짜면 되게 편하지 않을까요?
결국 우리는 반복 작업을 줄이려고 개발을 시작한 사람들이니까요.
AST를 활용한 코드 분석
그렇다면… 기나긴 전체 서비스 소스에서 번역 대상이 될 한글 텍스트를 어떻게 찾아내고, 수정할 수 있을까요?
AST 분석으로 해당하는 부분을 찾으면 되겠네요.
백엔드 개발자인 우드가 던진 이 한마디가 스크립트의 출발점이 되었습니다.
AST(Abstract Syntax Tree)는 코드를 의미 단위로 분해하여 트리 구조로 만든 것입니다.
각 노드는 코드의 특정 부분을 나타내며, 그 관계를 계층적으로 표현합니다.
Babel 라이브러리
저희는 Babel에서 제공하는 라이브러리를 이용해 AST 기반으로 소스 코드를 파싱하고, 탐색하고, 수정하기로 했습니다. 뒤에서 스크립트의 구체적인 내용을 살펴보기 전에, 이해를 돕기 위해 이 라이브러리들에 대해서 간단하게 살펴보도록 하겠습니다.
1. @babel/parser
@babel/parser
는 소스 코드를 파싱하여 AST를 생성하는 역할을 합니다.
import * as parser from '@babel/parser';
const code = `
const add = (a, b) => a + b;
`;
const ast = parser.parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
});
다음은 이렇게 생성된 AST 트리의 일부입니다.
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "add"
},
"init": {
"type": "ArrowFunctionExpression",
"id": null,
"expression": true,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"name": "a"
},
{
"type": "Identifier",
"name": "b"
}
],
"body": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"name": "b"
}
}
}
}
],
"kind": "const"
}
],
}
2. @babel/traverse
@babel/traverse
는 @babel/parser
로 생성한 AST를 탐색하고, 노드를 수정할 수 있도록 도와줍니다.
위에서 파싱한 트리를 보면, 단순한 코드임에도 트리 구조가 복잡해 보입니다. 이 트리를 탐색하고 수정하는 작업을 도와주는 것이 @babel/traverse
입니다.
traverse
는 Visitor 패턴을 사용하여 특정 노드타입에 대해 실행할 함수를 정의할 수 있습니다.
import traverse from '@babel/traverse';
traverse(ast, {
FunctionDeclaration: function(path) {
path.node.id.name = "x";
},
});
3. @babel/generator
@babel/generator
는 AST를 소스 코드로 변환하는 역할을 합니다. traverse
를 활용해 수정한 트리를 다시 저장할 수 있도록 만들어줍니다.
import * as generator from '@babel/generator';
const output = generator(ast, {
retainLines: true,
concise: true,
}).code;
스크립트 설계
이제 본격적으로 자동화 스크립트 구축 과정을 설명드리겠습니다.
아이디어를 구체화하면서 필요한 기능을 크게 두 가지로 분리했습니다.
1. T Wrapper
- 컴포넌트 내의 한글 텍스트를 감지하고 자동으로 t 함수로 래핑
- 수작업으로는 며칠이 걸릴 작업을 자동화
2. Extractor
- js, jsx, ts, tsx 파일에 존재하는 한글 값들을 추출
- i18next 설정에 필요한 언어 자원 파일 자동 생성
이제 각각의 스크립트가 어떤 방식으로 동작하는지 자세히 살펴보겠습니다.
1. T Wrapper
AST를 활용해 한글 텍스트를 찾고, t 함수를 래핑해주는 스크립트입니다.
1. 전체 변환 과정
들어가기 앞서 t 함수를 래핑하는 개괄적인 흐름을 살펴보겠습니다.
wrapByT(code: string) {
const ast = parser.parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
});
const functionLikes = this.getFunctionLikes(ast);
[
this.makeBlockStatement,
this.wrapStringLiteral,
this.wrapJSXText,
this.wrapConditionalExpression,
this.wrapTemplateLiteral,
this.insertTranslationHook,
].forEach((process) => process(functionLikes));
this.insertImportDeclaration(ast);
const output = generator(ast, {
retainLines: true,
concise: true,
}).code;
return { code: output };
}
- babel parser를 사용해 AST를 만들어 줍니다.
- AST에서
ArrowFunctionExpression
,FunctionDeclaration
,FunctionExpression
노드를 추출합니다. - process들을 실행하여 적용되야할 노드(FunctionLikes)에 t 함수를 래핑합니다.
- t로 래핑된 컴포넌트가 존재한다면, 파일 최상단에 import 구문을 추가합니다.
- 마지막으로 변환된 코드를 반환합니다.
2. 변환 대상
FunctionLike
t 함수를 래핑하려면 next-i18next의 (혹은 다른 i18n 라이브러리의) useTranslation
훅을 호출해야 합니다. 리액트에서 훅은 컴포넌트 또는 커스텀 훅에서만 사용될 수 있습니다.
따라서 t 함수를 래핑하기 위해 확인해야 하는 타겟은 단순합니다. node에 JSXElement이 존재하거나, 커스텀 훅이라면 자식 노드들을 탐색하며 t 래핑 과정을 진행하고, 아니라면 무시합니다.
private needTranslation(path: NodePath<PreFunctionLike>): boolean {
const isJSXElement = has(path, (node): node is t.JSXElement => t.isJSXElement(node));
const isHook = t.isIdentifier(path.node.id) && /use[A-Z][a-zA-Z0-9]/.test(path.node.id.name);
return isJSXElement || isHook;
}
따라서 이러한 함수들을 FunctionLike
라고 명명하고, FunctionLike
노드들만 타겟팅해 t 함수를 래핑합니다.
(부록) 화살표 함수의 암시적 반환문을 명시적 반환문으로 변환
앞에서 FunctionLike를 추출했으니 이제 FunctionLike에 useTranslation 훅을 호출해야 하는데요.
기존에 사용하고 있던 eslint rule에 따라 ReactElement를 바로 반환하는 함수들은 대부분 BlockStatement 없이 바로 반환하는 형태로 작성되어 있습니다.
화살표 함수의 암시적 반환문의 경우 훅을 호출할 수 없기 때문에 명시적 반환문인 블록문으로 변환해야 합니다.
훅을 호출하기 위한 사전 작업으로 FunctionLike 노드들을 순회하면서 모두 블록문으로 변환합니다.
// as-is: Implicit Return (암시적 반환)
const Component = () => <p>안녕하세요</p>;
// to-be: Block Statement with Explicit Return (블록문과 명시적 반환)
const Component = () => {
return <p>안녕하세요</p>;
}
3. 동작 방식
1. 한글을 찾아 t 함수 래핑
이제 사전 준비는 모두 마쳤으니 본격적으로 한글을 찾아 t 함수를 래핑하게 됩니다.
1. StringLiteral, JSXText
컴포넌트 내부에 한글이 작성된 문자열 리터럴, JSX 속성값, 일반적인 문자열 함수(isStringLiteral) 모두 찾아 t 함수로 래핑합니다.
// as-is
const Component = () => (
const message = "안녕하세요";
<div>
{message}
<span>반갑습니다</span>
<input type="text" placeholder="안녕하세요" />
</div>
)
// to-be
const Component = () => (
const message = t("안녕하세요");
<div>
{message}
<span>{t("반갑습니다")}</span>
<input type="text" placeholder={t("안녕하세요")} />
</div>
)
2. ConditionalExpression
컴포넌트 내부엔 삼항연산자를 쓰는 경우도 많은데요, 이 경우에는 ConditionalExpression을 찾아 t 함수로 래핑해주어야 합니다.
ConditionalExpression 노드에는 consequent와 alternate 두 개의 자식 노드가 존재합니다. 두 개의 노드를 검사해 노드의 value에 한글이 포함된 경우 t 함수로 래핑해줍니다.
// as-is
const message = isKorean ? "안녕하세요" : "hello";
// to-be
const message = isKorean ? t("안녕하세요") : "hello";
3. TemplateLiteral
마지막으로 템플릿 리터럴이 남았습니다!
이 경우는 앞선 경우들과 다르게 별도의 로직이 추가로 필요한데요, 설명하기에 앞서 i18n의 interpolation에 대해 간략하게 소개드리겠습니다.
i18n의 interpolation은 동적인 값을 번역에 적용할 수 있는 기능입니다.
예를 들어 다음과 같은 코드가 있다고 가정해봅시다.
동적으로 name을 넘길 수 있고, name이 변경될 때마다 번역 값도 동적으로 변경됩니다.
{
"introduce": "My name is {{name}}"
}
const doniIntroduction = t("introduce", { name: "Doni" });
const gohsueIntroduction = t("introduce", { name: "Gohsue" });
const lucyIntroduction = t("introduce", { name: "Lucy" });
console.log(doniIntroduction); // My name is Doni
console.log(gohsueIntroduction); // My name is Gohsue
console.log(lucyIntroduction); // My name is Lucy
TemplateLiteral의 경우 번역에 동적인 값이 적용되어야 합니다. 따라서 t 함수로 래핑할때, 2번째 인자에 동적인 값들을 넣어주어야 합니다.
이번 다국어 전환의 목표는 빠른 적용과 생산성이므로 2번째 인자로 넘기는 동적 값들의 key는 코드에서 사용한 그대로 사용합니다.
// as-is
const message = `${user.name}님 ${time}에 만나요`;
// to-be
const message = t("{{user.name}}님 {{time}}에 만나요", { "user.name": user.name, time });
2. useTranslation 훅 적용
한글을 포함한 모든 문자열을 찾아 t로 래핑했으니, 이제 useTranslation 훅을 호출해야 합니다.
이때 조건이 2가지가 있는데
- 최상위 함수에서만 useTranslation 훅을 호출할 수 있도록 처리해야 합니다.
- 함수 내부에 t로 래핑된 노드가 있는 경우에 useTranslation 훅을 호출해야 합니다.
2가지 조건을 모두 통과한 FunctionLike 노드들에 대해 useTranslation 훅을 호출합니다.
functionLikes
.filter(this.isTopLevelFunction.bind(this))
.filter((path) =>
has(path, (node): node is t.Identifier => t.isIdentifier(node) && node.name === 't'),
)
// as-is
const Component = () => {
const handleClick = () => {
alert(t('반갑습니다'));
}
return (
<button onClick={handleClick}>t('안녕하세요')</button>
);
}
// to-be
const Component = () => {
const {t} = useTranslation();
const handleClick = () => {
// const {t} = useTranslation(); ❌
alert(t('반갑습니다'));
}
return (
<button onClick={handleClick}>t('안녕하세요')</button>
);
}
3. import 구문 추가
wrapByT 함수의 마지막 단계로, 파일 최상단에 import 구문을 추가합니다.
find(ast, t.isProgram)
.filter(
(path) =>
!has(
path,
(node): node is t.ImportDeclaration =>
t.isImportDeclaration(node) && node.source.value === this.importModuleName,
),
)
.forEach((path) => {
this.isWrappedByT = true;
const importDeclaration = t.importDeclaration(
[t.importSpecifier(t.identifier('useTranslation'), t.identifier('useTranslation'))],
t.stringLiteral(this.importModuleName),
);
path.unshiftContainer('body', importDeclaration);
});
import { useTranslation } from 'next-i18next';
4. i18n-automation.config.yaml
앞의 코드에서 생소한 부분이 있는데요. 바로 this.importModuleName
입니다.
t wrap
, extractor
로 생산성을 챙겼지만, 셀 마다 상황이 달랐기 때문에 추가적인 설정이 필요했습니다.
기존 t-wrap은 원하는 경로는 설정할 수 있었으나, t-wrap을 실행시키지 않을 경로에 대해선 지정할 수 없었습니다.
이 부분을 개선하기 위해 레포지토리별 설정이 가능한 yaml 파일을 추가했습니다.
각 패키지 root에 i18n-automation.config.yaml
을 만들어 필요한 설정을 추가해주면 됩니다.
# React react-i18next를, Next.js라면 next-i18next를 import합니다.
runOn: react # default: next
extract:
targetPaths:
- ./src
tWrap:
targetPaths: #default: [./src]
- ./src
ignorePaths: #default: []
- utils
- pages # pages가 포함되면 모두 무시 ./src/pages를 무시하고 싶으면 `src/pages`로 명시
- test
여기서 runOn
에 따라 importModuleName이 react-i18next
인지 next-i18next
인지 결정되고, 이때 결정된 importModuleName을 이용해 import 구문이 추가될 때 각 레포에 맞는 모듈을 import하게 됩니다.
이젠 targetPaths
이외에 ignorePaths
까지 추가로 설정할 수 있게 되어 팀원들의 생산성을 더욱 향상시킬 수 있게 되었습니다.
4. 명령어 처리
이렇게 만들어진 스크립트를 팀원들이 편리하게 사용할 수 있도록 명령어를 추가해 아래처럼 script에 추가해 사용할 수 있습니다.
"scripts": {
"t-wrap": "i18n-automation t-wrap",
}
pnpm t-wrap
5. Demo
2. Extractor
T Wrapper로 변환된 코드에서 번역이 필요한 텍스트를 추출하여 언어 자원을 파일을 생성하는 스크립트입니다.
1. 변환 대상
Extractor의 타겟은 T Wrapper와 조금 다릅니다.
- T Wrapper
- React의 훅 규칙에 따라 FunctionLike 노드만을 처리
- Extractor
- 프로젝트 전체 파일을 대상으로 한글 텍스트 추출
- 컴포넌트나 훅 외에도 상수, 변수 등에 포함된 모든 한글 처리
이렇게 Extractor의 범위가 더 넓은 이유는, 실제 서비스에서 한글이 사용되는 곳이 예상보다 훨씬 다양하기 때문입니다.
예를 들어, 다음과 같은 컴포넌트가 있다고 가정해봅시다.
const DEFAULT_PROFILE = {
name: "랠릿셀 FE",
description: "랠릿셀 FE 개발자입니다."
}
const ERROR_MESSAGE = "오류가 발생했습니다.";
const Component = () => {
const { t } = useTranslation();
const handleClick = () => {
alert(ERROR_MESSAGE);
}
return (
<button onClick={handleClick}>t('안녕하세요')</button>
);
}
이 컴포넌트에서 Extractor는 t 함수에 전달된 한글 텍스트 이외에, 모든 한글 텍스트도 추출합니다.
// public/locales/ko/common.json
{
"랠릿셀 FE": "랠릿셀 FE",
"랠릿셀 FE 개발자입니다.": "랠릿셀 FE 개발자입니다.",
"오류가 발생했습니다.": "오류가 발생했습니다.",
"안녕하세요": "안녕하세요"
}
2. 동작 방식
Extractor는 AST를 순회하면서 다음과 같은 노드들을 분석합니다.
- StringLiteral: 일반 문자열
- JSXText: JSX 내부 텍스트
- JSXAttribute: JSX 속성값
- TemplateLiteral: 템플릿 리터럴
찾아낸 한글 텍스트는 next-i18next.config.js
에 설정된 각 언어 폴더에 JSON 파일로 생성됩니다.
이때 한글 텍스트는 키와 값이 동일하게 설정되어, 추후 다른 언어로의 번역 작업이 용이하도록 구성됩니다.
다른 노드들은 모두 한글을 그대로 추출하면 되지만 TemplateLiteral의 경우 동적인 값이 포함되어 있기 때문에 추가적인 처리가 필요합니다.
const message = `${user.name}님 ${time}에 만나요`;
이런 TemplateLiteral이 존재하는 경우 앞서 소개했던 i18next의 interpolation 기능을 활용하기 위해 다음과 같은 포맷으로 변환해야 합니다.
// public/locales/ko/common.json
{
"{{user.name}}님 {{time}}에 만나요": "{{user.name}}님 {{time}}에 만나요"
}
이렇게 만들어진 Key로 추후 동적인 값을 번역에 쉽게 적용할 수 있게 됩니다. 🙌
3. 명령어 처리
T wrapper
와 동일하게 처리합니다.
"scripts": {
"extract": "i18n-automation extract",
}
pnpm extract
4. Demo
에필로그
extract
로 서비스에서 지원하는 언어 자원 파일을 생성하고, t-wrap
으로 t 함수를 래핑해 번역 작업을 완료합니다.
단 두번의 스크립트 실행으로 국제화가 적용되었습니다. ✨
AST를 활용한 자동화 스크립트 개발로, 수작업으로 며칠이 걸렸을 i18n 적용 작업을 획기적으로 단축할 수 있었습니다. 실제로 한 동료는 이 스크립트를 활용해 특정 도메인 전체의 번역을 단 5시간 만에 완료했다고 합니다. 수작업으로 진행했다면 며칠이 걸렸을 작업입니다.
이런 성과를 가능하게 한 핵심 요소들을 정리해보면 다음과 같습니다.
- 효율적인 키 정의 방식
- 한글을 키로 사용하여 직관성 확보
- 개발과 디자인 팀의 생산성 향상
- 불필요한 키 정의 과정 생략
- 자동화된 변환 프로세스
- AST를 활용한 정확한 코드 분석
- 다양한 형태의 한글 텍스트 자동 변환
- React 컴포넌트의 특성을 고려한 처리
- 체계적인 자원 관리
- 자동화된 언어 자원 파일 생성
- 일관된 형식의 번역 키 관리
- 향후 확장성을 고려한 구조 설계
이러한 자동화 도구의 개발로 우리는 기존에 예상했던 것보다 훨씬 빠르게 서비스의 국제화를 진행할 수 있었습니다. 현재는 언어 자원 관리 방식에 locize를 도입하여 언어 자원을 효율적으로 관리하고 있습니다.
이번 프로젝트를 통해, 적절한 자동화 도구의 개발이 얼마나 큰 생산성 향상을 가져올 수 있는지 다시 한번 확인할 수 있었습니다.
인프런 글로벌 페이지가 궁금하다면
👉 구경하러 가기