Communitygithub.com

Paraglide JS + Svelte 5 모범 사례

Paraglide JS 2.x(컴파일러 기반, 트리 쉐이커블)와 Svelte 5(runes)를 사용하여 타입 안전하고 성능 좋은 i18n 앱을 구축하기 위한 전문 가이드입니다. 설정, 메시지 작성, 로케일 전략, i18n 라우팅, SSR/SSG, Svelte 5 스니펫을 활용한 리치 텍스트 마크업, 반응성 패턴, 테스트 및 문제 해결을 다룹니다.

Paraglide JS + Svelte 5 모범 사례란 무엇인가요?

이 문서는 Paraglide JS 2.x와 Svelte 5를 사용한 국제화(i18n) 앱 구축에 대한 종합적인 모범 사례 가이드입니다. Paraglide JS는 `messages/{locale}.json` 파일을 트리 쉐이커블하고 완전히 타입화된 ESM 함수로 컴파일하여, 런타임 라이브러리 대비 최대 약 70% 더 작은 i18n 번들(5개 로케일/200개 메시지 기준 약 47 KB vs 약 205 KB)을 제공하며, 비동기 워터폴이나 별도의 i18n 프로바이더 없이 Svelte 5 컴포넌트와 통합됩니다. 주요 내용으로는 `npx sv add`를 통한 빠른 설정, URL과 쿠키 기반 로케일 전략, SvelteKit 라우팅 통합, `<ParaglideMessage>`와 Svelte 5 스니펫을 활용한 리치 텍스트 처리, Svelte 5 runes와의 반응성 패턴, SSR/SSG 설정, 성능 최적화 및 문제 해결 방법을 다룹니다. Paraglide가 로케일을 문서/요청 상태로 관리하고 Svelte 5 runes가 UI 상태를 관리한다는 핵심 개념을 강조하며, 로케일 변경 시 전체 문서 탐색을 수행하는 것이 권장됩니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/agentmemoir/skills/tree/HEAD/skills/frontend/paraglide-svelte5-best-practices

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Paraglide JS + Svelte 5 모범 사례은(는) 무엇을 하나요?

로케일을 반응형 앱 상태가 아닌, 컴파일러가 해석하는 요청/빌드 상태로 취급하세요. Paraglide의 전체 문서 탐색을 통해 로케일 전환을 처리하고, Svelte 5 runes가 나머지 모든 것을 제어하도록 하십시오. Paraglide JS는 SvelteKit의 공식 i18n 통합 도구입니다. messages/{locale}.json 파일을 트리 쉐이커블하고 완전히 타입화된 ESM 함수로 컴파일하여, 런타임 라이브러리보다 최대 약 70% 더 작은 i18n 번들(공개된 벤치마크 기준, 5개 로케일/200개 메시지에서 약 47 KB 대 약 205 KB)을 제공하며, 비동기 워터폴이 없고 Svelte 5 컴포넌트에 i18n 프로바이더를 연결할 필요가 없습니다.


1. 빠른 참조 결정표

결정 사항권장 기본값이유
설치 경로새 SvelteKit 앱에서 npx sv add paraglide="languageTags:en,de"Vite 플러그인, 훅, app.html 속성, .gitignore를 한 번에 스캐폴딩합니다
패키지@inlang/paraglide-js v2.xv1 어댑터 @inlang/paraglide-js-adapter-sveltekit는 사용 중단됨 — 코어 패키지를 직접 사용하세요
전략 (SSR + 지역화된 URL)["url", "cookie", "baseLocale"]URL이 SEO의 소스 정보(source of truth)입니다. 쿠키는 서버에서 볼 수 있는 사용자 선택을 유지합니다. baseLocale은 항상 마지막으로 해석됩니다
전략 (저장된 재정의 추가)["localStorage", "cookie", "preferredLanguage", "url", "baseLocale"]localStorage만으로는 첫 SSR 요청에서 보이지 않습니다. 쿠키가 서버/클라이언트를 연결합니다
로케일 전환 동작기본 setLocale(locale) (전체 문서 탐색)프레임워크 반응성은 <html lang>, dir, <title> 또는 지역화된 URL을 다시 렌더링하지 않습니다. 탐색이 이를 수행합니다
AsyncLocalStorage활성화 유지 (기본값)Vercel Edge 및 Cloudflare Workers (Node 호환)에서 지원합니다. 비활성화하면 동시 요청 간에 로케일이 유출될 위험이 있습니다
TS 선언emitTsDeclarations: true (TypeScript ≥ 5.6)개발 중 키가 변경될 때 Svelte + TS 언어 서버의 동기화를 유지합니다
메시지 키평평하고, 안정적이며, 사람이 읽을 수 있는 무작위 키 (calm_green_otter)공식 권장 사항입니다. 중첩 키도 작동하지만 평평한 키가 선호됩니다
사전 렌더링export const prerender = true + 로케일별 entries()SSG는 각 로케일이 고유한 URL 파일로 확인되어야 합니다
로케일에 대한 반응성기본적으로 컴포넌트에 없음getLocale()은 요청 범위입니다. 새 문서가 로케일별로 렌더링되므로 runes가 이를 추적할 필요가 없습니다

2. 설정 및 구성 (Paraglide 2.x + SvelteKit + Svelte 5)

2.1 가장 빠른 경로

npx sv create my-app
npx sv add paraglide="languageTags:en,de,demo:yes"

Svelte CLI 애드온은 inlang 프로젝트, Vite 플러그인, SvelteKit handle/reroute 훅, app.html의 %lang%/%dir%, 그리고 선택적 데모 페이지를 구성합니다.

2.2 수동 설정 (전체 보일러플레이트)

프로젝트를 설치하고 스캐폴딩한 후, 네 개의 파일을 연결하십시오:

vite.config.ts — 명시적 전략이 포함된 플러그인:

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { paraglideVitePlugin } from '@inlang/paraglide-js';

export default defineConfig({
  plugins: [
    sveltekit(),
    paraglideVitePlugin({
      project: './project.inlang',
      outdir: './src/lib/paraglide',
      emitTsDeclarations: true,
      strategy: ['url', 'cookie', 'baseLocale'],
    }),
  ],
});

emitTsDeclarations: true는 메시지를 편집할 때 .d.ts를 다시 생성하여 Svelte 및 TypeScript 서버가 동기화되도록 합니다. TypeScript 5.6+가 필요합니다(더 빠른 JSDoc 추론 경로를 위해 false로 설정).

src/app.html — 하드코딩된 lang을 대체:

<!doctype html>
<html lang="%lang%" dir="%dir%">

src/hooks.server.ts — 요청 범위 로케일이 있는 서버 미들웨어:

import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
import { getTextDirection } from '$lib/paraglide/runtime';

const paraglideHandle: Handle = ({ event, resolve }) =>
  paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => {
    event.request = localizedRequest;
    return resolve(event, {
      transformPageChunk: ({ html }) =>
        html.replace('%lang%', locale).replace('%dir%', getTextDirection(locale)),
    });
  });

export const handle: Handle = paraglideHandle;

미들웨어는 로케일을 감지하고(전략 순서대로), URL이 감지된 로케일과 일치하지 않으면 문서 요청을 리디렉션하며, /de/about → /about을 SvelteKit이 보기 전에 지역화 해제하고, 요청을 AsyncLocalStorage로 래핑하여 getLocale()이 동시 요청마다 올바른 로케일을 반환하도록 합니다.

src/hooks.ts — 라우팅 경계 (hooks.server.ts가 아닌 hooks.ts여야 함):

import type { Reroute } from '@sveltejs/kit';
import { deLocalizeUrl } from '$lib/paraglide/runtime';

export const reroute: Reroute = (request) => deLocalizeUrl(request.url).pathname;
// project.inlang/paraglide.config.ts
import { defineConfig } from '@inlang/paraglide-js';

export default defineConfig({
  outdir: './src/lib/paraglide',
  strategy: ['url', 'cookie', 'baseLocale'],
  urlPatterns: [
    { pattern: '/', localized: [['en', '/']] },
    {
      pattern: '/:path(.*)?',
      localized: [
        ['en', '/en/:path(.*)?'],
        ['de', '/de/:path(.*)?'],
      ],
    },
  ],
});

우선 순위: CLI 플래그 / 플러그인 옵션 > 구성 파일 > 기본 제공 기본값. 감시 모드는 구성을 핫 리로드합니다.


3. 핵심 사용 패턴 (메시지, 로케일, 라우팅)

3.1 메시지

import { m } from '$lib/paraglide/messages';
import type { LocalizedString } from '$lib/paraglide/runtime';

m.hello_world();                       // "Hello World!"
m.greeting({ name: 'Ada' });           // "Hello Ada!" — 매개변수는 타입화됨
m.greeting({ name: 'Ada' }, { locale: 'de' }); // 한 번 호출에 대해 로케일 강제
const title: LocalizedString = m.welcome_title(); // 브랜드 안전 문자열 타입

메시지는 messages/{locale}.json에 있습니다. 로케일은 project.inlang/settings.json (baseLocale + locales)에 선언됩니다. 평평하고 안정적인 키를 선호하세요. 중첩 키는 m['user.profile.title']()을 통해 지원됩니다. 동적 키 접근은 트리 쉐이킹을 방해하므로 동적 ID를 명시적 메시지 참조에 매핑하십시오.

3.2 로케일 가져오기 및 설정

import { getLocale, locales, setLocale, getTextDirection } from '$lib/paraglide/runtime';

getLocale();          // 예: "de" — 서버에서는 요청 범위
getTextDirection();   // 현재 로케일에 대한 "ltr" | "rtl"
setLocale('de');      // 지역화된 URL로 전체 문서 탐색

setLocale()은 구성된 전략을 업데이트한 다음 전체 문서 탐색을 수행합니다. url 전략이 활성화된 경우 지역화된 URL로 이동하고, 그렇지 않으면 다시 로드합니다. 이는 의도적인 것입니다. <html lang>, dir, <title>, 메타데이터 및 지역화된 URL은 프레임워크 반응성으로 업데이트할 수 없는 문서 수준 상태입니다.

setLocale(locale, { reload: false }) 탈출구는 복원할 수 없는 메모리 내 상태를 유지해야 하는 완전히 클라이언트에서 렌더링된 표면(임베디드 위젯, 확장 페이지)에만 존재합니다. 일반 로케일 선택기, SSR/SSG 문서 또는 전략에 url이 포함된 경로에서는 절대 사용하지 마십시오. 이는 기존 URL과 셸을 그대로 두어 getLocale()이 계속 이전 로케일을 확인하게 만듭니다.

3.3 지역화된 URL 및 링크

import { localizeHref, deLocalizeUrl, localizeUrl } from '$lib/paraglide/runtime';

localizeHref('/blog');                          // 현재 로케일의 경우 "/de/blog"
localizeHref('/blog', { locale: 'de' });        // 명시적
localizeUrl('https://example.com/about', { locale: 'de' }).href; // 전체 URL 형식

URL 패턴은 표준 URLPattern 구문을 사용합니다. 순서가 중요합니다 — 와일드카드 앞에 특정 패턴을 배치하고 urlpattern.com에서 테스트하십시오.

3.4 Svelte 5 로케일 전환기 (관용적)

<script lang="ts">
  import { page } from '$app/state';                       // Svelte 5: 객체, 스토어 아님
  import { resolve } from '$app/paths';
  import { locales, localizeHref } from '$lib/paraglide/runtime';
</script>

<nav aria-label="Languages">
  {#each locales as locale (locale)}
    <a
      href={resolve(localizeHref(page.url.pathname, { locale }))}
      data-sveltekit-reload
      aria-current={page.params.locale === locale ? 'page' : undefined}
    >
      {locale}
    </a>
  {/each}
</nav>

여기서 두 가지 Svelte 5 세부 사항이 중요합니다. $app/state는 page를 반응형 객체로 노출하고($page 스토어 및 $app/stores는 레거시 패턴임), 로케일 간 링크의 data-sveltekit-reload는 지역화된 URL, 서버 렌더링 문서 및 런타임 로케일이 동기화되도록 합니다. 사전 렌더링된 크롤링에도 필요합니다.


4. Svelte 5 스니펫을 사용한 리치 텍스트 (ParaglideMessage)

마크업을 사용하면 번역가가 구조를 제어하고 앱이 렌더링을 제어할 수 있습니다. 태그 이름은 앱에서 정의합니다. HTML에 자동으로 매핑되는 것은 없습니다.

// messages/en.json
{
  "cta": "{#link to=|/docs|}Read the docs{/link}",
  "welcome": "{#b}Hi {name}{/b}{#icon/}"
}
<script lang="ts">
  import { ParaglideMessage } from '@inlang/paraglide-js-svelte';
  import { m } from '$lib/paraglide/messages';
  const name = $props().name; // 또는 모든 $state
</script>

<ParaglideMessage message={m.welcome} inputs={{ name }}>
  {#snippet b({ children })}
    <strong>{@render children?.()}</strong>
  {/snippet}
  {#snippet icon({ attributes })}
    <Icon variant={attributes?.variant} />
  {/snippet}
</ParaglideMessage>

<ParaglideMessage>는 마크업이 있을 때 message.parts()를 사용하고, 일반 텍스트의 경우 message()로 폴백합니다. {#snippet tag({ children, options, attributes })} 구문은 정확히 Svelte 5 스니펫 패턴입니다. 슬롯, <slot> 레거시가 없습니다. 리터럴 구문 to=|/docs| (파이프로 구분) 및 m.welcome() 단독 호출은 마크업이 제거된 일반 텍스트를 반환합니다.


5. Svelte 5 Runes 통합: 반응형인 것과 그렇지 않은 것

핵심 개념 모델: Paraglide는 로케일을 문서/요청 상태로 소유하고, Svelte 5 runes는 UI 상태를 소유합니다. 로케일 변경은 새 문서를 제공하므로 getLocale()을 추적하기 위해 runes가 필요한 경우는 거의 없습니다. 실제로 나타나는 패턴:

단일 문서 내 로케일 종속 파생 값 (예: reload: false를 사용하는 클라이언트 전용 표면 또는 여러 로케일을 명시적으로 렌더링하는 미리보기 창):

<script lang="ts">
  import { getLocale, locales } from '$lib/paraglide/runtime';
  import { m } from '$lib/paraglide/messages';

  let { locale = getLocale() } = $props();          // 타입화된 props
  // $derived는 `locale` prop이 변경될 때 재평가됩니다 — Paraglide의
  // 내부 로케일이 변경될 때가 아닙니다(탐색 없이는 변경되지 않음):
  const greeting = $derived(m.greeting({ name: 'Ada' }, { locale }));
  const isRtl = $derived(getTextDirection(locale) === 'rtl');
</script>

전역 상태를 읽는 대신 강제된 로케일을 아래로 전달하는 것이 runes 친화적인 패턴입니다. 각 메시지 호출에 { locale } 옵션을 사용하면 컴포넌트가 props의 순수 함수가 되며, 이는 정확히 Svelte 5가 상태를 흐르도록 원하는 방식입니다. $effect는 진정한 부작용(예: 미리보기 로케일 유지)을 위해 남겨 두십시오. $derived에 절대 할당하지 말고, $effect 내부에서 $state를 작성하여 로케일을 "밀어내지" 마십시오. 대신 props를 전달하십시오.

전역 클라이언트 상태 (예: "마지막으로 미리 본 로케일" 앱 내 번역 도구의 경우):

// src/lib/locale-preview.svelte.ts — .svelte.ts 파일은 모듈 범위에서 runes를 지원합니다
export const preview = $state({ locale: 'en' as string | undefined });

.svelte.ts 파일의 모듈 수준 $state는 모든 가져오는 쪽에서 공유됩니다. 앱 전체 싱글톤에는 괜찮지만, 이것이 프로덕션 로케일이 작동하는 방식이 아님을 기억하십시오(전략 체인 + 문서 탐색임).


6. SSR, 수화, SSG

6.1 SSR 규칙

  • 하나의 handle 체인 순서: paraglideHandle을 먼저 배치하거나(로케일별 event.locals 키가 필요한 인증 핸들러보다 먼저) 다운스트림 훅이 지역화 해제된 요청을 볼 수 있도록 합니다.
  • TLS 종료 프록시 뒤에서는 URL 일치 및 리디렉션이 브라우저 측 원본을 사용하도록 effectiveRequestUrl을 전달하십시오.
  • AsyncLocalStorage를 켜 두십시오. 이것이 동시 요청 및 스트리밍 SSR에서 getLocale()을 안전하게 만드는 요소입니다. disableAsyncLocalStorage 폴백은 node:async_hooks가 없고 요청별 격리가 보장된 런타임에서만 사용하십시오. 다른 곳에서 사용하면 사용자 간에 로케일이 유출될 수 있습니다.

6.2 수화 정확성

서버와 클라이언트는 동일한 전략 순서로 로케일을 확인해야 합니다. 고전적인 불일치: 서버는 URL에서 확인하지만 클라이언트 전략은 먼저 SSR 시간에 존재하지 않는 localStorage를 읽습니다. 지속된 선택이 첫 번째 응답을 형성해야 하는 경우 서버에서 볼 수 있는 cookie 전략을 포함하여 수정하십시오.

6.3 SSG (adapter-static / prerender)

사전 렌더링은 미들웨어를 실행하지 않습니다. 빌드 중에 페이지별로 로케일을 설정하십시오:

// src/routes/[locale]/+layout.ts
import { setLocale, assertIsLocale } from '$lib/paraglide/runtime';
import type { LayoutLoad } from './$types';

export const prerender = true;
export function load({ params }: { params: { locale: string } }) {
  setLocale(assertIsLocale(params.locale));
}
// src/routes/[locale]/+page.ts
import { locales } from '$lib/paraglide/runtime';
export const prerender = true;
export function entries() {
  return locales.map((locale) => ({ locale }));
}

SSG는 모든 로케일이 URL 접두사를 전달하도록 요구합니다(urlPatterns를 구성하여 /en/… 및 /de/…가 모두 존재하도록). 그렇지 않으면 SvelteKit이 로케일별로 고유한 파일을 내보낼 수 없습니다. 정적 어댑터의 SPA 폴백의 경우 로케일 접두사가 붙은 자산 404를 방지하기 위해 kit.paths.relative = false를 설정하고 사이트맵에 generateStaticLocalizedUrls()를 사용하십시오.


7. 성능 및 트리 쉐이킹

  • 메시지 함수를 직접 호출하십시오. m.hello()는 하나의 작은 ESM 함수로 컴파일됩니다. 번들러는 사용되지 않는 메시지와 해당 번역을 자동으로 제거합니다. 카탈로그가 성장해도 번들 크기는 일정하게 유지됩니다(벤치마크: 프로젝트에 200개 또는 1,000개의 메시지가 있든 47 KB).

8. 문제 해결

8.1 "getLocale()이 항상 baseLocale을 반환합니다"

  • project.inlang/settings.json에서 로케일 태그가 정확한지 확인하십시오(lang 코드가 아닌 IETF BCP 47 태그).
  • 플러그인 옵션의 strategy가 array이고 올바른 순서인지 확인하십시오.
  • hooks.server.ts와 hooks.ts가 모두 존재하고 역할이 바뀌지 않았는지 확인하십시오.

8.2 "URL이 로케일을 업데이트하지 않습니다"

  • 로케일 전환 링크에 data-sveltekit-reload을 추가하십시오(기본 setLocale()에는 필요하지 않음).
  • URL 패턴 순서가 올바른지 확인하십시오(구체적인 패턴이 먼저).
  • SSR 미들웨어가 URL을 리디렉션하는지 확인하기 위해 브라우저 네트워크 탭을 확인하십시오.

8.3 "localStorage가 SSR에서 작동하지 않습니다"

  • 서버에서 localStorage를 읽을 수 없습니다. cookie을 먼저 포함하는 전략을 사용하여 선택 사항을 첫 번째 요청에 전달하십시오.

8.4 "번들에 사용되지 않은 모든 메시지 번역이 포함됩니다"

  • message.dynamicKey() 대신 m.myKey()를 사용하여 동적 접근을 피하십시오. 동적 접근은 트리 쉐이킹을 방해합니다.

9. 추가 자료

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

agentmemoir/skills

>- Build, migrate, or review server-rendered Svelte 5 apps using runes ($state, $derived, $effect, $props, $bindable) with SvelteKit SSR (adapter-auto/node/cloudflare/vercel/netlify) or plain Vite SSR using svelte/server render() and hydrate(). Use whenever I mention Svelte 5 SSR, server-side rendering, SvelteKit load functions, +page.server.js, +layout.server.js, hooks.server.js, form actions, streaming SSR, CSP nonces, hydration mismatches, or request-scoped state. Also use to review .svelte and server files for SSR-safety bugs: effects that never run on the server, module-level state leaking between users, non-deterministic render values, and secrets reaching the client. Trigger even on a bare pasted component that "renders differently on the server and client".

agentmemoir/skills

>- Build, migrate, or review static (no server) Svelte 5 apps using runes ($state, $derived, $effect, $props, $bindable) instead of legacy Svelte 4 reactivity (export let, reactive dollar-colon statements, stores-as-default). Use whenever I mention Svelte 5, runes, a "static Svelte site", "Svelte SPA", "SvelteKit static site"/"adapter-static"/prerendered SvelteKit, deploying Svelte to GitHub Pages/Netlify/Cloudflare Pages/S3/any CDN, or converting Svelte 4 code (export let, on:click, writable stores) to Svelte 5. Also use when writing or reviewing .svelte files that must ship with no backend, to flag accidental server-only SvelteKit files (+page.server.js, +server.js, form actions) or exposed secrets, and to apply the bundled XSS/CSP/secrets security checklist. Trigger even for a bare pasted .svelte file or a vague "make this a static site" request.

관련 스킬