SvelteKit 3 정식 출시: v2와 달라진 점과 마이그레이션 가이드 총정리
SvelteKit 3가 정식 출시되었습니다. 기능을 대거 추가하는 릴리스라기보다는 레거시를 걷어내고 설정 구조를 Vite 중심으로 재정비하는 정리 릴리스에 가깝습니다. 이 글에서는 v3의 핵심 변화, v2와의 차이점, 그리고 실제 마이그레이션 절차를 한 번에 정리합니다.
1. SvelteKit 3, 한눈에 보기
SvelteKit 3의 방향성은 크게 세 가지로 요약할 수 있습니다.
- 설정의 Vite 일원화: svelte.config.js가 사라지고 모든 설정이 vite.config.js(ts)의 sveltekit() 플러그인 옵션으로 이동
- 레거시 제거와 Svelte 5 전제: $app/stores, base/assets 등 deprecated API 제거, Svelte 5 필수
- 에러 처리·보안·관측성 강화: 모든 에러가 handleError를 거치고, CSRF 설정과 쿠키 기본값이 정비됨
v2에서 이미 deprecated 경고가 나오던 항목이 대부분이라, v2 최신 버전에서 경고를 먼저 해결해 두었다면 체감 난이도는 높지 않습니다.
2. 최소 요구 버전 (Updated dependencies)
항목 v3 최소 버전
| Node.js | v22.17 |
| TypeScript | v6 |
| Svelte | v5.57.1 |
| Vite | v8.0.12 (stable rolldown v1 번들 첫 Vite 8 릴리스) |
| @sveltejs/vite-plugin-svelte | v7 |
TypeScript 5를 플러그인 호환성 때문에 고정해 두었던 프로젝트는 특히 주의가 필요합니다. 또한 Vite 8 기반이라 빌드 파이프라인에 Rolldown이 사용되며, adapter-node도 Rolldown으로 번들링됩니다.
3. v2 vs v3 핵심 차이점 요약
구분 v2 v3
| 설정 파일 | svelte.config.js (config.kit.*) | vite.config.js의 sveltekit({...}) 옵션 |
| 경로 별칭 | $lib 자동 생성 | #lib (package.json imports로 직접 선언) |
| 환경 모듈 | $app/environment, $env/* | $app/env, $app/env/public, $app/env/private |
| 페이지 스토어 | $app/stores | 제거 → $app/state |
| 얕은 라우팅 | pushState / replaceState | goto(url, { shallow: true, state }) |
| 데이터 갱신 | invalidateAll | refreshAll |
| 경로 헬퍼 | base, assets, resolveRoute | 제거 → resolve, asset |
| tsconfig | ./.svelte-kit/tsconfig.json 상속 | $app/tsconfig 상속 |
| 서비스 워커 | $service-worker | 제거 → $app/manifest, $app/env, $app/paths |
| handleError | 예상된 에러(error())는 호출 안 됨 | 모든 에러가 전달됨 |
| 렌더링 에러 | 별도 실험 플래그 필요 | 항상 처리 (error boundary 연동) |
| CSRF | csrf.checkOrigin | csrf.trustedOrigins (보호는 항상 켜짐) |
| 쿠키 기본 path | 현재 요청 경로 기준 | '/' |
| 파라미터 매처 | src/params/ 디렉터리 내 파일 | src/params.ts 단일 파일 + defineParams |
| 서버 전용 모듈 | .server. 접미 + src/lib/server | 파일명에 server 세그먼트, 프로젝트 내 모든 server 디렉터리 |
4. 주요 변경점 상세
4-1. 설정이 vite.config로 이동
v3에서는 svelte.config.js를 더 이상 지원하지 않습니다. config.kit.* 아래에 있던 옵션이 플러그인의 최상위 옵션이 되고, compilerOptions 같은 Svelte 옵션과 나란히 위치합니다. (참고로 vite.config 방식 자체는 v2.62부터 도입되었기 때문에 v2 최신 버전에서 미리 옮겨둘 수 있습니다.)
// vite.config.js
import adapter from '@sveltejs/adapter-auto';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
sveltekit({
adapter: adapter(),
compilerOptions: {
experimental: { async: true }
},
experimental: {
remoteFunctions: true
}
})
]
});
sveltekit()에 SvelteKit 옵션이 아닌 값을 넘기면 vite-plugin-svelte로 그대로 전달되므로 inspector 같은 옵션도 여기서 설정합니다.
제거된 옵션
- files.lib
- experimental.handleRenderingErrors, experimental.instrumentation (더 이상 불필요)
- experimental.tracing → 최상위 tracing 옵션으로 승격
- vitePlugin (옵션을 플러그인에 직접 전달)
- preloadStrategy (modulepreload를 항상 사용)
- prerender.origin → paths.origin
- csrf.checkOrigin → csrf.trustedOrigins
추가·변경된 옵션
- output.linkHeaderPreload: Link 헤더 프리로드 사용 여부 (헤더가 너무 커지는 문제를 피하려고 기본은 <link> 요소 주입)
- csrf.trustedOrigins: 외부 신뢰 오리진 허용 목록
- paths.origin: 리버스 프록시 뒤에서 공개 오리진을 지정 (CSRF 검사에 사용, adapter-node의 ORIGIN 환경변수를 대체)
- version.pollInterval: 기본값이 1시간으로 변경 (이전에는 폴링 없음)
4-2. $lib → #lib
$lib 별칭은 더 이상 자동 생성되지 않습니다. 대신 Node의 표준 subpath imports를 package.json에 선언해서 사용합니다. Vite와 TypeScript가 네이티브로 해석하기 때문에 별도 별칭 설정이 필요 없습니다.
{
"imports": {
"#lib": "./src/lib/index.js",
"#lib/*": "./src/lib/*"
}
}
// before
import { foo } from '$lib/foo';
// after (확장자 필수)
import { foo } from '#lib/foo.js';
주의할 점은 모듈 확장자(.js, .ts 등)를 명시해야 한다는 것입니다. 프로젝트 전체에서 치환이 필요하므로 자동 마이그레이션 도구를 쓰는 것을 권장합니다.
4-3. $app/* 모듈 정비
- $app/environment → $app/env 로 이름 변경 (서비스 워커에서도 import 가능)
- $env/... 계열은 deprecated → $app/env/private, $app/env/public 사용 권장
- $app/stores 제거 → $app/state 사용 ($page 대신 page)
- 신규 $app/manifest: assets, immutable, prerendered 등 앱 메타데이터 제공
- 신규 $app/service-worker: 서비스 워커 실행 컨텍스트의 타입 안전한 접근
- $service-worker 제거: version은 $app/env, 자산 목록은 $app/manifest, resolved는 $app/paths에서 가져옴
<script>
import { page } from '$app/state';
</script>
<p>current pathname: {page.url.pathname}</p>
또한 page.url은 이제 ReadonlyURL 타입이라 page.url.searchParams.set(...) 같은 변경이 타입 오류가 됩니다. 수정이 필요하면 new URL(page.url.href)로 복사해서 쓰면 됩니다.
4-4. 네비게이션 & 얕은 라우팅(Shallow routing)
pushState / replaceState는 deprecated 되었고, goto의 옵션으로 통합되었습니다.
// before
pushState('/foo', state);
replaceState('/bar', state);
// after
goto('/foo', { shallow: true, state });
goto('/bar', { shallow: true, replace: true, state });
그 외 변경 사항입니다.
- persistState: true 옵션: 새로고침 후에도 page.state를 다시 적용
- 얕은 라우팅도 beforeNavigate / onNavigate / afterNavigate를 트리거 (콜백 인자의 shallow 속성으로 구분 가능)
- invalidateAll → refreshAll (page.state를 초기화하지 않음)
- goto 옵션: invalidateAll → refreshAll, keepFocus: true + noScroll: true → reset: false, replaceState → replace
- 앱 라우트로 해석되지 않는 URL에 goto를 호출하면 reject
- delta는 popstate(뒤로/앞으로) 내비게이션에서만 존재
- preloadData가 실패 시 { type: 'error', status, error }를 반환 (처리 분기 추가 필요)
- 현재 페이지로 향하는 링크를 클릭하면 아무 일도 안 하는 대신 refreshAll() 수행
4-5. $app/paths: base / assets 제거
// before
const pathname = base + resolveRoute('/blog/[slug]', { slug });
const file = assets + '/foo.png';
// after
const pathname = resolve('/blog/[slug]', { slug });
const file = asset('foo.png');
타입 이름도 Pathname → Path, Asset → AssetPath로 바뀌었고, 맨 앞의 /가 빠졌습니다. 즉 asset('/foo.png')가 아니라 asset('foo.png')처럼 쓰고, 선행 /는 이제 라우트 ID에만 사용됩니다.
4-6. 에러 처리 대폭 개선
v3에서 가장 체감이 큰 영역입니다.
- App.Error에 항상 status 가 포함됨
- error(status, message)의 두 번째 인자는 항상 문자열. 추가 속성은 세 번째 인자로 전달
- handleValidationError 제거 → 검증 오류는 handleError에 kind: 'validation'으로 전달
- 모든 에러가 handleError로 전달 (v2에서는 error()로 만든 예상된 에러는 제외됨)
- handleError에서 status를 반환해 HTTP 상태 코드에 영향을 줄 수 있음
- 렌더링 중 에러도 handleError를 거쳐 가장 가까운 error boundary로 전달 (각 +error.svelte에 자동으로 boundary가 생성됨)
- 향상된(enhanced) 폼 액션 응답이 fail(...)에 지정한 상태 코드를 사용 (기존에는 항상 200)
- 소스맵이 기본 생성되고 스택 트레이스에 적용
// before
error(404, { message: 'Not found', code: 'NOT_FOUND' });
// after
error(404, 'Not found', { code: 'NOT_FOUND' });
hooks.client.ts에서 async handleError 를 사용 중이라면 compilerOptions.experimental.async를 켜야 렌더링 중에도 await 할 수 있습니다.
4-7. 보안: CSRF, 쿠키
CSRF
- csrf.checkOrigin: false로 보호를 끄는 방식은 사라졌습니다. CSRF 보호는 항상 켜져 있고, 신뢰할 외부 오리진만 허용 목록에 추가합니다.
- Content-Type 헤더가 없는 크로스 오리진 변경 요청은 CSRF로 간주되어 거부됩니다.
- 개발 환경에서 정적 에셋에 access-control-allow-origin: *를 붙여주던 동작이 사라지고 Vite의 CORS 미들웨어에 위임됩니다.
// vite.config.js
sveltekit({
csrf: {
trustedOrigins: ['https://checkout.stripe.com']
}
})
쿠키
- cookie 패키지가 v2로 업데이트: 쿠키 이름은 ASCII만 허용, 타입명 CookieSerializeOptions → SerializeOptions, CookieParseOptions → ParseOptions
- path를 명시하지 않으면 기본값이 '/' (현재 요청 경로가 아님)
외부 리다이렉트
외부 URL로 redirect하려면 이제 external 옵션을 명시해야 합니다.
redirect(307, 'https://example.com', { external: true });
// 또는 허용 오리진 배열
4-8. 파라미터 매처: src/params.ts 단일 파일
기존에는 src/params/ 디렉터리에 매처 파일을 하나씩 만들었다면, v3에서는 defineParams로 한 파일에 선언합니다. 매처는 함수(파싱된 값 반환, 불일치 시 undefined)이거나 Standard Schema 가 될 수 있어 Valibot 같은 스키마 라이브러리를 그대로 쓸 수 있습니다.
// src/params.ts
import { defineParams } from '@sveltejs/kit/params';
import * as v from 'valibot';
export const params = defineParams({
// 스키마 방식
integer: v.pipe(v.string(), v.toNumber()),
// 함수 방식
fruit: (param) => {
if (param === 'apple' || param === 'orange') return param;
}
});
4-9. 서버 전용 모듈 규칙 변경
- 파일: 파일명에 server 세그먼트가 있으면 서버 전용 (stuff.server.ts, stuff.server.test.ts, server.ts 모두 해당. 기존에는 server.ts가 해당되지 않았음)
- 디렉터리: 기존 src/lib/server뿐 아니라, src/routes와 static을 제외한 프로젝트 내 모든 server 디렉터리가 서버 전용
기존에 server라는 이름의 파일·폴더를 일반 모듈로 쓰고 있었다면 의도치 않게 서버 전용으로 취급되어 빌드가 실패할 수 있으니 확인해 보세요.
4-10. 관측성(Observability)
- src/instrumentation.server.js 파일이 있으면 서버 계측이 자동으로 적용됩니다.
- OpenTelemetry 트레이싱은 tracing.server로 옵트인합니다. 오버헤드가 있으므로 개발/프리뷰 환경에서만 켜는 것도 고려해 볼 만합니다.
4-11. 앱 업데이트 감지 (updated)
updated.current가 true가 되는 시점이 훨씬 다양해졌습니다.
- 서버에서 데이터를 가져오는 모든 내비게이션
- 모든 remote function 호출
- 창이 다시 보이거나 포커스를 받을 때
- 폴링 주기(기본 1시간)
Vercel의 skew protection 같은 기능을 쓰면 내비게이션/remote function 기반의 수동 감지가 거짓 음성이 될 수 있지만, 폴링과 이벤트 기반 체크는 이를 우회하므로 계속 동작합니다.
4-12. 어댑터 변경 사항
모든 first-party 어댑터가 SvelteKit 3를 요구합니다.
adapter-cloudflare
- platform에서 Cloudflare 전용 API가 사라지고 일반 Worker 방식으로 접근합니다.
- env, ctx.waitUntil 등 → cloudflare:workers에서 import
- cf → request.cf
- caches → 전역 변수
- 타입 사용을 위해 wrangler 설치 후 wrangler types 실행, 최소 wrangler는 ^4.67.0
import { env, waitUntil } from 'cloudflare:workers';
const value = await env.KV.get('key');
adapter-node
- Rolldown으로 번들링
- ORIGIN 환경변수 제거 → paths.origin 사용
- 정적 에셋은 빌드 시점에 기록된 목록으로만 서빙 (빌드 후 출력 디렉터리에 추가한 파일은 서빙되지 않음)
- 정적 에셋은 GET/HEAD만 허용 (그 외 메서드는 405)
adapter-netlify
- 안정화된 Netlify Frameworks API 규격 준수, Netlify CLI v17.31.0 이상 필요
- publish 디렉터리는 netlify.toml이 아닌 어댑터 옵션으로 지정
adapter-vercel
- edge 런타임 지원 중단
4-13. 그 밖의 변경 사항
- data-sveltekit-* 속성의 'off' 값 제거 → data-sveltekit-preload-data="false"
- json(...), text(...) 헬퍼 deprecated → Response.json(...), new Response(text) 사용
- +server.js에서 204(또는 빈 2xx) 응답을 반환하면 본문이 없는 응답으로 처리
- handle의 resolve는 항상 Promise<Response>
- 페이지 옵션 config는 universal(+page.js) 쪽이 server(+page.server.js) 쪽보다 우선
- 서비스 워커는 type: 'module'로 번들링·등록
- use:enhance에서 다른 페이지의 action을 지정하면 해당 페이지로 이동 (네이티브 폼 동작과 일치)
- @sveltejs/kit/node의 getRequest, setResponse가 동기 함수로 변경 (호출부의 await 제거)
- @sveltejs/kit/node/polyfills 제거
- 타입 위치 이동: defineParams → @sveltejs/kit/params, 환경 관련 타입(defineEnvVars 포함) → @sveltejs/kit/env, Handle 등 훅 타입 → @sveltejs/kit/hooks, RemoteQuery 등 → $app/server
4-14. Remote functions는 아직 실험적
타입 안전하게 서버 함수를 호출할 수 있는 Remote functions는 v3에서도 여전히 실험 기능입니다. 사용하려면 두 플래그를 모두 켜야 합니다.
sveltekit({
compilerOptions: { experimental: { async: true } },
experimental: { remoteFunctions: true }
})
v3에서 함께 바뀐 점입니다.
- 파일명에 remote 세그먼트가 있으면 remote 모듈로 취급 (stuff.remote.ts 등). 플래그를 켜지 않으면 이런 파일이 있을 때 에러 발생
- query 안에서 event.url, event.params, event.route 접근 시 에러 (필요한 값은 인자로 전달)
- 리소스의 error 타입이 any에서 App.Error | undefined로 변경
- 폼 입력은 반드시 field.as(...)를 사용해야 하며, <input name="message">처럼 이름을 직접 쓰면 제출이 거부됨
<input {...myform.fields.message.as('text')}>
5. 마이그레이션 방법
Step 0. 먼저 v2 최신 버전으로 올리기
공식 문서에서도 3.0으로 올리기 전에 가장 최신 2.x로 업그레이드하길 권장합니다. 2.x에서는 v3에서 깨질 부분에 대해 구체적인 deprecation 경고가 나오기 때문에, 이 경고가 가장 좋은 마이그레이션 안내서 역할을 합니다.
Step 1. 작업 상태 커밋
마이그레이션 CLI는 파일을 직접 수정하므로 먼저 커밋해 두세요.
git add -A && git commit -m "chore: before sveltekit 3 migration"
Step 2. 자동 마이그레이션 실행
npx sv migrate sveltekit-3
- sv CLI 안에 SvelteKit 3용 마이그레이션이 작업(task) 단위로 들어 있습니다.
- 의존성 업그레이드, $lib → #lib 치환 등을 자동으로 처리해 줍니다.
- 세부 옵션(작업 목록 확인, 전체 작업 일괄 실행, 설치 방식 지정 등)은 npx sv migrate --help로 확인하세요. 버전에 따라 옵션이 달라질 수 있습니다.
Step 3. 의존성 버전 확인
package.json이 아래 요구 버전을 만족하는지 확인하고 설치 명령을 실행합니다. 어댑터도 SvelteKit 3 호환 버전이어야 합니다.
Node 22.17+ / TypeScript 6 / Svelte 5.57.1+ / Vite 8.0.12+ / vite-plugin-svelte 7
Step 4. 설정 이전
- svelte.config.js의 내용을 vite.config의 sveltekit({...}) 옵션으로 이동한 뒤 파일 삭제
- 제거된 옵션(preloadStrategy, files.lib, vitePlugin 등) 삭제
- csrf.checkOrigin → csrf.trustedOrigins, prerender.origin → paths.origin으로 교체
- tsconfig.json을 $app/tsconfig 상속으로 변경 (include / exclude 직접 지정 필요)
{
"extends": "$app/tsconfig",
"include": ["src", "test", "*"],
"exclude": ["src/service-worker"]
}
서비스 워커가 있다면 src/service-worker/tsconfig.json을 만들어 $app/tsconfig/service-worker를 상속시키고, 루트 tsconfig에서는 제외합니다.
Step 5. 코드 수정 (자동 변환 후 수동 점검)
점검 항목 할 일
| $lib import | #lib + 확장자 명시 |
| $app/stores | $app/state로 교체, $ 접두사 제거 |
| $app/environment | $app/env로 이름 변경 |
| $env/* | $app/env/private / $app/env/public으로 점진 이전 |
| base, assets, resolveRoute | resolve, asset으로 교체 (선행 / 제거) |
| pushState / replaceState | goto(..., { shallow: true }) |
| invalidateAll | refreshAll |
| goto 옵션 | keepFocus/noScroll → reset: false, replaceState → replace |
| error(404, { message }) | error(404, 'message', { ...extra }) |
| handleValidationError | handleError의 kind: 'validation' 처리로 이전 |
| handleError | 이제 예상된 에러도 들어오므로 로깅/알림 로직 점검 |
| preloadData 결과 | type: 'error' 분기 추가 |
| data-sveltekit-*="off" | "false"로 변경 |
| src/params/* | src/params.ts로 통합 (defineParams) |
| 쿠키 | path 기본값이 '/'인 점 확인, 쿠키 이름에 비ASCII 문자 사용 여부 점검 |
| 외부 redirect | { external: true } 또는 허용 오리진 배열 추가 |
| json() / text() | Response.json() / new Response() |
| server라는 이름의 파일/디렉터리 | 서버 전용 모듈로 처리되는지 확인 |
| 어댑터별 설정 | Cloudflare / Node / Netlify / Vercel 변경 사항 반영 |
Step 6. 검증
npm run check # svelte-check로 타입 오류 확인
npm run build # 빌드 확인
npm run test # 테스트 확인
체크 포인트는 아래와 같습니다.
- 타입 오류 (특히 page.url 불변 타입, App.Error의 status, preloadData 반환 타입)
- 폼 액션 테스트에서 상태 코드 (fail의 코드가 그대로 반영됨)
- 프로덕션 빌드 후 adapter-node의 정적 파일 서빙 및 paths.origin 설정
- 크로스 오리진 폼 제출 (결제, 소셜 로그인 콜백 등) 동작 여부
- 서비스 워커 등록과 오프라인 캐시 동작
6. 마이그레이션 체크리스트 (요약)
- [ ] 최신 2.x로 업그레이드하고 deprecation 경고 해결
- [ ] Node 22.17+, TS 6, Svelte 5.57.1+, Vite 8.0.12+ 확인
- [ ] npx sv migrate sveltekit-3 실행
- [ ] svelte.config.js → vite.config 이전 후 삭제
- [ ] $lib → #lib (package.json의 imports 선언, 확장자 포함)
- [ ] $app/stores → $app/state, $app/environment → $app/env
- [ ] base/assets → resolve/asset
- [ ] 얕은 라우팅 / invalidateAll / goto 옵션 교체
- [ ] 에러 처리(error() 시그니처, handleError) 점검
- [ ] CSRF, 쿠키, 외부 리다이렉트 설정 점검
- [ ] src/params.ts로 매처 통합
- [ ] tsconfig를 $app/tsconfig 기반으로 교체
- [ ] 사용 중인 어댑터의 변경 사항 반영
- [ ] check / build / 테스트 통과
7. 마치며
SvelteKit 3는 화려한 신기능보다는 "앞으로의 확장을 위한 기반 다지기" 에 초점을 맞춘 릴리스입니다. 설정이 Vite로 모이고, 레거시 API가 정리되며, 에러 처리와 보안 기본값이 한층 견고해졌습니다. 수정해야 할 곳은 많아 보이지만 대부분 이름 변경과 시그니처 정리 수준이고, 상당 부분은 sv migrate가 자동으로 처리해 줍니다.
신규 프로젝트라면 바로 v3로 시작하면 되고, 기존 프로젝트라면 v2 최신 버전 → deprecation 경고 해결 → 자동 마이그레이션 → 수동 점검 순서로 진행하는 것이 가장 안전합니다.
참고 자료
'Web > JavaScript' 카테고리의 다른 글
| [Next.js] Layout으로 공통 화면 구성하기 (3) | 2024.02.17 |
|---|---|
| [Next.js] Routing using App Router (0) | 2024.02.04 |
| [Typescript] Module System (0) | 2023.08.09 |
| [Svelte] Template Syntax (1) | 2022.12.10 |
| [Svelte] Getting Started with SvelteKit (0) | 2022.12.01 |
댓글