---
createdAt: 2025-11-24
updatedAt: 2025-11-24
title: 컴파일러 기반 i18n 대 선언적 i18n
description: '"매직" 컴파일러 기반 국제화와 명시적 선언적 콘텐츠 관리 간의 아키텍처적 트레이드오프 탐구.'
keywords:
  - Intlayer
  - 국제화
  - 블로그
  - Next.js
  - JavaScript
  - React
  - i18n
  - 컴파일러
  - 선언적
slugs:
  - blog
  - compiler-vs-declarative-i18n
---

# 컴파일러 기반 i18n에 대한 찬반 논쟁

10년 이상 웹 애플리케이션을 개발해왔다면, 국제화(i18n)가 항상 마찰 지점이었음을 알 것입니다. 문자열 추출, JSON 파일 관리, 복수형 규칙 처리 등 아무도 하고 싶어 하지 않는 작업인 경우가 많습니다.

최근에, 이 고통을 사라지게 하겠다고 약속하는 **"컴파일러 기반" i18n 도구**의 새로운 물결이 등장했습니다. 그 제안은 매력적입니다: **컴포넌트에 텍스트만 작성하면, 빌드 도구가 나머지를 처리해줍니다.** 키도, 임포트도 없이, 그저 마법처럼요.

하지만 소프트웨어 공학의 모든 추상화가 그렇듯, 마법에는 대가가 따릅니다.

이 블로그 포스트에서는 선언적 라이브러리에서 컴파일러 기반 접근법으로의 전환, 그들이 도입하는 숨겨진 아키텍처 부채, 그리고 왜 "지루한" 방식이 여전히 전문 애플리케이션에 가장 좋은 방법일 수 있는지 탐구할 것입니다.

## 목차

<TOC/>

## 국제화의 간략한 역사

우리가 현재 위치를 이해하려면, 시작점을 되돌아봐야 합니다.

2011년에서 2012년경, JavaScript 환경은 매우 달랐습니다. 우리가 알고 있는 번들러들(Webpack, Vite)은 존재하지 않았거나 초기 단계에 불과했습니다. 우리는 브라우저에서 스크립트를 붙여 사용하는 방식이었습니다. 이 시기에 **i18next**와 같은 라이브러리가 탄생했습니다.

이들은 당시 가능한 유일한 방법으로 문제를 해결했습니다: **런타임 사전(Runtime Dictionaries)**. 거대한 JSON 객체를 메모리에 로드하고, 함수가 실행 중에 키를 찾아내는 방식이었습니다. 이는 신뢰할 수 있고 명확하며 어디서나 작동했습니다.

시간이 흘러 오늘날에 이르렀습니다. 우리는 밀리초 단위로 추상 구문 트리(Abstract Syntax Trees, AST)를 파싱할 수 있는 강력한 컴파일러들(SWC, Rust 기반 번들러)을 갖추게 되었습니다. 이 힘은 새로운 아이디어를 탄생시켰습니다: _왜 우리가 수동으로 키를 관리해야 할까? 컴파일러가 "Hello World"라는 텍스트를 보고 바로 교체해주면 안 될까?_

이렇게 해서 컴파일러 기반 i18n이 탄생했습니다.

> **컴파일러 기반 i18n의 예:**
>
> - Paraglide (각 메시지를 작은 ESM 함수로 컴파일하는 트리 쉐이킹 모듈로, 번들러가 사용하지 않는 로케일과 키를 자동으로 제거할 수 있습니다. 문자열 키 조회 대신 메시지를 함수로 가져옵니다.)
> - LinguiJS (`<Trans>`와 같은 메시지 매크로를 빌드 시점에 일반 JS 함수 호출로 재작성하는 매크로-투-함수 컴파일러입니다. 매우 작은 런타임 크기로 ICU/MessageFormat 구문을 제공합니다.)
> - Lingo.dev (React 애플리케이션 빌드 중에 번역된 콘텐츠를 직접 주입하여 로컬라이제이션 파이프라인을 자동화하는 데 중점을 둡니다. AI를 사용해 번역을 자동 생성할 수 있으며 CI/CD에 직접 통합할 수 있습니다.)
> - Wuchale (Svelte 우선 전처리기로 .svelte 파일 내 인라인 텍스트를 추출하여 래퍼가 없는 번역 함수로 컴파일합니다. 문자열 키를 피하고, 콘텐츠 추출 로직을 메인 애플리케이션 런타임과 완전히 분리합니다.)
> - Intlayer (컴파일러 / 추출 CLI로 컴포넌트를 파싱하고, 타입이 지정된 사전을 생성하며, 선택적으로 명시적인 Intlayer 콘텐츠를 사용하도록 코드를 재작성할 수 있습니다. 목표는 선언적이고 프레임워크에 구애받지 않는 코어를 유지하면서 컴파일러를 통한 속도를 높이는 것입니다.)
>
> **선언적 i18n의 예:**
>
> - i18next / react-i18next / next-i18next (런타임 JSON 사전과 광범위한 플러그인 생태계를 사용하는 성숙한 업계 표준)
> - react-intl (FormatJS 라이브러리의 일부로, 표준 ICU 메시지 구문과 엄격한 데이터 포맷팅에 중점을 둠)
> - next-intl (Next.js에 최적화되어 App Router 및 React Server Components와 통합됨)
> - vue-i18n / @nuxt/i18n (컴포넌트 수준의 번역 블록과 강력한 반응성 통합을 제공하는 표준 Vue 생태계 솔루션)
> - svelte-i18n (반응형 런타임 번역을 위한 Svelte 스토어를 감싼 경량 래퍼)
> - angular-translate (빌드 타임 병합 대신 런타임 키 조회에 의존하는 레거시 동적 번역 라이브러리)
> - angular-i18n (빌드 시 XLIFF 파일을 템플릿에 직접 병합하는 Angular의 네이티브 선행 처리 방식)
> - Tolgee (선언적 코드를 UI 내에서 직접 "클릭하여 번역" 편집이 가능한 인컨텍스트 SDK와 결합)
> - Intlayer (컴포넌트별 접근 방식으로, 네이티브 트리 쉐이킹과 TypeScript 검증을 가능하게 하는 콘텐츠 선언 파일 사용)

## Intlayer 컴파일러

**Intlayer**는 본질적으로 콘텐츠에 대해 **선언적 접근법**을 권장하는 솔루션이지만, 개발 속도를 높이거나 빠른 프로토타이핑을 돕기 위해 컴파일러를 포함하고 있습니다.

Intlayer 컴파일러는 React, Vue, Svelte 컴포넌트뿐만 아니라 기타 JavaScript/TypeScript 파일의 AST(추상 구문 트리)를 탐색합니다. 그 역할은 하드코딩된 문자열을 감지하여 전용 `.content` 선언으로 추출하는 것입니다.

> 자세한 내용은 문서를 참조하세요: [Intlayer Compiler Docs](https://github.com/aymericzip/intlayer/blob/main/docs/docs/ko/compiler.md)

## 컴파일러의 매력 (일명 "마법" 접근법)

이 새로운 접근법이 유행하는 데는 이유가 있습니다. 개발자 입장에서 경험이 놀랍게 느껴집니다.

### 1. 속도와 "흐름"

집중하고 있을 때 의미 있는 변수 이름(`home_hero_title_v2`)을 고민하느라 멈추면 흐름이 끊깁니다. 컴파일러 방식을 사용하면 `<p>Welcome back</p>`를 입력하고 계속 진행할 수 있습니다. 마찰이 전혀 없습니다.

### 2. 레거시 구조 구출 미션

거대한 코드베이스를 상속받아 5,000개의 컴포넌트가 있지만 번역이 전혀 없는 상황을 상상해 보세요. 수동 키 기반 시스템으로 이를 개조하는 것은 몇 달이 걸리는 악몽과 같습니다. 컴파일러 기반 도구는 구출 전략으로 작동하여, 단 한 파일도 수동으로 건드리지 않고 수천 개의 문자열을 즉시 추출합니다.

### 3. AI 시대

이것은 우리가 간과해서는 안 될 현대적인 이점입니다. AI 코딩 어시스턴트(예: Copilot 또는 ChatGPT)는 자연스럽게 표준 JSX/HTML을 생성합니다. 이들은 여러분의 특정 번역 키 스키마를 알지 못합니다.

- **선언적(Declarative):** AI의 출력을 텍스트에서 키로 교체하기 위해 다시 작성해야 합니다.
- **컴파일러(Compiler):** AI 코드를 복사-붙여넣기만 하면 바로 작동합니다.

## 현실 점검: 왜 "마법"이 위험한가

"마법" 같은 기능이 매력적이지만, 추상화가 누수됩니다. 빌드 도구에 인간의 의도를 이해하도록 의존하는 것은 아키텍처적 취약성을 초래합니다.

### 휴리스틱 취약성 (추측 게임)

컴파일러는 무엇이 콘텐츠이고 무엇이 코드인지 추측해야 합니다. 이로 인해 도구와 "싸우게" 되는 엣지 케이스가 발생합니다.

다음 시나리오를 고려해 보십시오:

- `<span className="active"></span>`가 추출되나요? (문자열이지만, 아마도 클래스입니다).
- `<span status="pending"></span>`가 추출되나요? (프로퍼티 값입니다).
- `<span>{"Hello World"}</span>`가 추출되나요? (JS 표현식입니다).
- `<span>Hello {name}. How are you?</span>`가 추출되나요? (보간법이 복잡합니다).
- `<span aria-label="Image of cat"></span>`가 추출되나요? (접근성 속성은 번역이 필요합니다).
- `<span data-testid="my-element"></span>`는 추출되나요? (테스트 ID는 번역해서는 안 됩니다).
- `<MyComponent errorMessage="An error occurred" />`는 추출되나요?
- `<p>This is a paragraph{" "}\n containing multiple lines</p>`는 추출되나요?
- `<p>{getStatusMessage()}</p>` 함수 결과는 추출되나요?
- `<div>{isLoading ? "The page is loading" : <MyComponent/>} </div>`는 추출되나요?
- `<span>AX-99</span>`와 같은 제품 ID는 추출되나요?

결국 애플리케이션 로직이 깨지지 않도록 특정 주석(`// ignore-translation` 같은)이나 특정 props(`data-compiler-ignore="true"` 같은)를 추가하게 됩니다.

### Intlayer는 이 복잡성을 어떻게 처리하나요?

Intlayer는 필드가 번역을 위해 추출되어야 하는지 감지하기 위해 혼합 방식을 사용하며, 오탐(false positives)을 최소화하려고 시도합니다:

1.  **AST 분석:** 요소 타입을 확인합니다 (예: `reactNode`, `label`, 또는 `title` prop을 구분).
2.  **패턴 인식:** 문자열이 대문자로 시작하거나 공백을 포함하는지 감지하여, 코드 식별자보다는 사람이 읽을 수 있는 텍스트일 가능성이 높다고 판단합니다.

### 동적 데이터의 한계

컴파일러 추출은 **정적 분석**에 의존합니다. 안정적인 ID를 생성하기 위해 코드 내에서 리터럴 문자열을 반드시 확인해야 합니다.
API가 `server_error`와 같은 에러 코드 문자열을 반환하는 경우, 컴파일러는 빌드 시점에 해당 문자열이 존재하는지 알 수 없기 때문에 컴파일러로 번역할 수 없습니다. 이 경우 동적 데이터 전용의 별도 "런타임 전용" 시스템을 구축해야 합니다.

### 청킹 부족

일부 컴파일러는 페이지별로 번역을 청킹하지 않습니다. 만약 컴파일러가 언어별로 큰 JSON 파일(e.g., `./lang/en.json`, `./lang/fr.json` 등)을 생성한다면, 단일 방문 페이지에 대해 모든 페이지의 콘텐츠를 로드하게 될 가능성이 큽니다. 또한, 콘텐츠를 사용하는 각 컴포넌트는 필요 이상으로 많은 콘텐츠로 하이드레이션되어 성능 문제를 일으킬 수 있습니다.

번역을 동적으로 로드할 때도 주의해야 합니다. 만약 이렇게 하지 않으면, 현재 언어뿐만 아니라 모든 언어의 콘텐츠를 함께 로드하게 됩니다.

> 문제를 설명하기 위해, 10개의 페이지와 10개의 언어(모두 100% 고유한 경우)를 가진 사이트를 생각해 보세요. 이 경우 99개의 추가 페이지 콘텐츠를 로드하게 됩니다 (10 × 10 - 1).

### "청크 폭발"과 네트워크 워터폴

청킹 문제를 해결하기 위해, 일부 솔루션은 컴포넌트별 또는 키별로 청크를 나누는 방식을 제공합니다. 하지만 이 문제는 부분적으로만 해결됩니다. 이러한 솔루션의 판매 포인트는 종종 "당신의 콘텐츠가 트리 쉐이킹된다"는 점입니다.

실제로 콘텐츠를 정적으로 로드하면, 솔루션은 사용하지 않는 콘텐츠를 트리 쉐이킹하지만, 여전히 모든 언어의 콘텐츠가 애플리케이션과 함께 로드됩니다.

그렇다면 왜 동적으로 로드하지 않을까요? 네, 그 경우 필요한 것보다 더 많은 콘텐츠를 로드하게 되지만, 이는 단점이 전혀 없는 것은 아닙니다.

콘텐츠를 동적으로 로드하면 각 콘텐츠 조각이 자체 청크로 분리되어 해당 컴포넌트가 렌더링될 때만 로드됩니다. 이는 텍스트 블록당 하나의 HTTP 요청을 하게 된다는 의미입니다. 페이지에 1,000개의 텍스트 블록이 있다면? → 서버에 1,000개의 HTTP 요청을 보내게 됩니다. 그리고 피해를 줄이고 애플리케이션의 첫 렌더링 시간을 최적화하려면 여러 개의 Suspense 경계나 Skeleton Loader를 삽입해야 합니다.

> 참고: Next.js와 SSR을 사용하더라도 컴포넌트는 로딩 후에 하이드레이션되므로 HTTP 요청은 여전히 발생합니다.

해결책? `i18next`, `next-intl`, 또는 `intlayer`와 같이 범위가 지정된 콘텐츠 선언을 할 수 있는 솔루션을 채택하는 것입니다.

> 참고: `i18next`와 `next-intl`은 번들 크기를 최적화하기 위해 각 페이지마다 네임스페이스/메시지 임포트를 수동으로 관리해야 합니다. `rollup-plugin-visualizer`(vite), `@next/bundle-analyzer`(next.js), 또는 `webpack-bundle-analyzer`(React CRA / Angular 등)와 같은 번들 분석기를 사용하여 사용하지 않는 번역으로 번들이 오염되고 있는지 감지해야 합니다.

### 런타임 성능 오버헤드

번역을 반응형으로 만들어(언어를 전환할 때 즉시 업데이트되도록) 컴파일러가 종종 모든 컴포넌트에 상태 관리 훅을 주입합니다.

- **비용:** 5,000개의 아이템 리스트를 렌더링할 경우, 텍스트만을 위해 5,000개의 `useState`와 `useEffect` 훅을 초기화하게 됩니다. React는 모든 5,000명의 소비자를 동시에 식별하고 다시 렌더링해야 합니다. 이로 인해 대규모의 "메인 스레드" 블록이 발생하여 언어 전환 시 UI가 멈추게 됩니다. 이는 선언형 라이브러리(일반적으로 단일 Context 제공자를 사용하는)가 절약하는 메모리와 CPU 사이클을 소모합니다.

> React 외의 다른 프레임워크에서도 유사한 문제가 발생한다는 점에 유의하세요.

## 함정: 벤더 락인(Vendor Lock-in)

번역 키의 추출 또는 마이그레이션이 가능한 i18n 솔루션을 선택할 때 주의하세요.

선언적 라이브러리의 경우, 소스 코드는 명확하게 번역 의도를 포함합니다: 이것이 바로 키이며, 사용자가 이를 제어합니다. 라이브러리를 변경하고 싶다면 일반적으로 import만 업데이트하면 됩니다.

컴파일러 접근법에서는 소스 코드가 단순한 영어 텍스트일 수 있으며, 번역 로직의 흔적이 없습니다: 모든 것이 빌드 도구 설정에 숨겨져 있습니다. 만약 해당 플러그인이 유지보수되지 않거나 솔루션을 변경하고 싶다면, 꼼짝 못할 수 있습니다. “eject”할 쉬운 방법이 없으며, 코드 내에 사용할 수 있는 키가 없고, 새로운 라이브러리를 위해 모든 번역을 다시 생성해야 할 수도 있습니다.

일부 솔루션은 번역 생성 서비스를 제공하기도 합니다. 크레딧이 없으면? 번역도 없습니다.

컴파일러는 종종 텍스트를 해시합니다(예: `"Hello World"` -> `x7f2a`). 번역 파일은 `{ "x7f2a": "Hola Mundo" }`와 같이 보입니다. 함정: 라이브러리를 변경하면 새 라이브러리는 `"Hello World"`를 보고 해당 키를 찾으려고 합니다. 하지만 번역 파일이 해시(`x7f2a`)로 가득 차 있기 때문에 찾을 수 없습니다.

### 플랫폼 종속성

컴파일러 기반 접근 방식을 선택하면 기본 플랫폼에 종속되게 됩니다. 예를 들어, 특정 컴파일러는 모든 번들러(Vite, Turbopack, Metro 등)에서 사용할 수 없습니다. 이로 인해 향후 마이그레이션이 어려워질 수 있으며, 모든 애플리케이션을 커버하기 위해 여러 솔루션을 채택해야 할 수도 있습니다.

## 반대편: 선언적 접근 방식의 위험

공정하게 말하면, 전통적인 선언적 방식도 완벽하지 않습니다. 자체적인 "함정"이 존재합니다.

1.  **네임스페이스 지옥:** 어떤 JSON 파일을 로드할지 수동으로 관리해야 하는 경우가 많습니다(`common.json`, `dashboard.json`, `footer.json`). 하나라도 잊으면 사용자는 원시 키를 보게 됩니다.
2.  **과도한 로드:** 신중한 설정 없이 초기 로드 시 모든 페이지의 모든 번역 키를 실수로 불러오는 일이 매우 쉽기 때문에 번들 크기가 불필요하게 커질 수 있습니다.
3.  **동기화 불일치:** 컴포넌트가 삭제된 후에도 해당 키가 JSON 파일에 남아 있는 경우가 흔합니다. 이로 인해 번역 파일이 무한히 커지며 "좀비 키"로 가득 차게 됩니다.

## Intlayer의 중간 지점

이것이 바로 **Intlayer**와 같은 도구들이 혁신을 시도하는 이유입니다. Intlayer는 컴파일러가 강력하지만 암묵적인 마법은 위험하다는 점을 이해합니다.

Intlayer는 선언적 콘텐츠 관리의 장점과 개발 시간을 절약할 수 있는 컴파일러 호환성을 모두 누릴 수 있는 혼합 방식을 제공합니다.

그리고 Intlayer 컴파일러를 사용하지 않더라도, Intlayer는 `transform` 명령어를 제공합니다 (VSCode 확장 기능을 통해서도 접근 가능). 숨겨진 빌드 단계에서 마법처럼 처리하는 대신, 실제로 **컴포넌트 코드를 재작성**할 수 있습니다. 텍스트를 스캔하여 코드베이스 내에서 명시적인 콘텐츠 선언으로 대체합니다.

이로써 두 가지 접근법의 장점을 모두 누릴 수 있습니다:

1.  **세분화:** 번역을 컴포넌트 가까이에 유지하여 모듈화와 트리 쉐이킹을 향상시킵니다.
2.  **안전성:** 번역이 숨겨진 빌드 타임 마법이 아닌 명시적인 코드가 됩니다.
3.  **락인 없음:** 코드가 선언적 구조로 변환되어, 탭 키를 누르거나 IDE의 코파일럿을 사용해 콘텐츠 선언을 쉽게 생성할 수 있으며, webpack 플러그인에 로직을 숨기지 않습니다.

## 결론

그렇다면, 어떤 방식을 선택해야 할까요?

**MVP를 구축 중이거나 빠르게 진행하고 싶다면:**
컴파일러 기반 접근법이 유효한 선택입니다. 매우 빠르게 작업할 수 있습니다. 파일 구조나 키에 대해 걱정할 필요가 없습니다. 그냥 빌드하면 됩니다. 기술 부채는 "미래의 당신"이 해결할 문제입니다.

**주니어 개발자이거나 최적화에 신경 쓰지 않는다면:**
수동 관리가 가장 적은 방식을 원한다면, 컴파일러 기반 접근법이 아마도 최선입니다. 키나 번역 파일을 직접 다룰 필요 없이 텍스트만 작성하면 컴파일러가 나머지를 자동화합니다. 이는 설정 노력을 줄이고 수동 단계에서 발생하는 일반적인 i18n 실수를 감소시킵니다.

**이미 수천 개의 컴포넌트를 리팩토링해야 하는 기존 프로젝트를 국제화하는 경우:**
컴파일러 기반 접근 방식은 여기서 실용적인 선택이 될 수 있습니다. 초기 추출 단계는 수주 또는 수개월에 걸친 수작업을 절약할 수 있습니다. 그러나 Intlayer의 `transform` 명령어와 같은 도구를 사용하는 것을 고려해 보세요. 이 도구는 문자열을 추출하고 명시적인 선언적 콘텐츠 선언으로 변환할 수 있습니다. 이를 통해 선언적 접근 방식의 안전성과 이식성을 유지하면서 자동화의 속도를 얻을 수 있습니다. 두 가지 장점을 모두 누릴 수 있습니다: 장기적인 아키텍처 부채 없이 빠른 초기 마이그레이션.

**전문적이고 엔터프라이즈급 애플리케이션을 구축하는 경우:**
마법 같은 방법은 일반적으로 좋지 않습니다. 제어가 필요합니다.

- 백엔드에서 오는 동적 데이터를 처리해야 합니다.
- 저사양 장치에서의 성능을 보장해야 합니다 (훅 폭발 방지).
- 특정 빌드 도구에 영원히 종속되지 않도록 해야 합니다.

전문적인 애플리케이션의 경우, **선언적 콘텐츠 관리**(Intlayer 또는 검증된 라이브러리와 같은)가 여전히 최고의 표준입니다. 이는 관심사를 분리하고 아키텍처를 깔끔하게 유지하며, 애플리케이션이 여러 언어를 지원하는 능력이 "블랙박스" 컴파일러가 의도를 추측하는 것에 의존하지 않도록 보장합니다.
