18.6 SolidStart 기초
SolidStart는 Solid.js 팀이 공식으로 개발한 풀스택 메타프레임워크입니다. Next.js가 React 위에 있듯이, SolidStart는 Solid.js 위에 파일 기반 라우팅, 서버 함수, SSR/SSG, 스트리밍을 추가합니다. 이 장에서는 SolidStart의 핵심 개념을 처음부터 실전 수준까지 단계별로 배웁니다.
1. SolidStart란?
다른 메타프레임워크와 비교
| 기능 | Next.js (React) | SvelteKit | SolidStart |
|---|---|---|---|
| 기반 프레임워크 | React | Svelte | Solid.js |
| 라우팅 방식 | 파일 기반 | 파일 기반 | 파일 기반 |
| 서버 함수 | Server Actions | Form Actions | "use server" RPC |
| 데이터 로딩 | loader, fetch in RSC | load 함수 | createAsync + cache |
| 스트리밍 | React Suspense | Streaming | Solid Suspense |
| 배포 어댑터 | Vercel/커스텀 | Vercel/CF/Node | Vercel/CF/Node/static |
| 번들러 | Webpack/Turbopack | Vite | Vite (Vinxi 기반) |
SolidStart는 Vinxi(Vite 기반 앱 번들러)를 사용하며, 서버와 클라이언트 코드를 하나의 파일에 함께 작성하는 "같은 파일에서 서버/클라이언트 혼합" 패턴을 지원합니다.
언제 SolidStart를 선택하나?
- SEO가 중요한 공개 사이트 (SSR 필요)
- 블로그, 문서 사이트 (SSG 적합)
- API 라우트와 풀스택 기능이 필요한 앱
- 최고 성능의 서버 렌더링이 필요한 경우
순수 CSR(싱글 페이지 앱)이면 SolidStart 없이 Solid.js + Vite만으로도 충분합니다.
2. 프로젝트 생성
새 프로젝트 시작
npm create solid@latest my-solid-app
cd my-solid-app
npm install
npm run dev
대화형 설정에서 선택 옵션:
- Template:
bare(빈 프로젝트) 또는with-tailwindcss,hackernews등 - Server Side Rendering: Yes (SSR 사용 권장)
- TypeScript: Yes (강력 권장)
프로젝트 구조
my-solid-app/
├── src/
│ ├── routes/ # 파일 기반 라우팅 루트
│ │ ├── index.tsx # / 경로
│ │ ├── about.tsx # /about 경로
│ │ ├── blog/
│ │ │ ├── index.tsx # /blog 경로
│ │ │ └── [slug].tsx # /blog/:slug 동적 라우트
│ │ └── (auth)/ # 레이아웃 그룹 (URL 미포함)
│ │ ├── login.tsx # /login
│ │ └── register.tsx # /register
│ ├── components/ # 재사용 컴포넌트
│ ├── lib/ # 유틸리티, DB 연결 등
│ ├── app.tsx # 앱 루트 컴포넌트
│ └── entry-server.tsx # SSR 진입점
├── public/ # 정적 파일
├── app.config.ts # SolidStart 설정
└── package.json
app.config.ts 기본 설정
// app.config.ts
import { defineConfig } from '@solidjs/start/config';
export default defineConfig({
// Vite 플러그인 추가
vite: {
plugins: [],
},
// 서버 설정
server: {
preset: 'node', // 'vercel' | 'cloudflare' | 'netlify' | 'static'
},
});
3. 파일 기반 라우팅
라우트 파일 명명 규칙
src/routes/
├── index.tsx → /
├── about.tsx → /about
├── contact.tsx → /contact
├── blog/
│ ├── index.tsx → /blog
│ └── [slug].tsx → /blog/:slug
├── shop/
│ ├── [...path].tsx → /shop/* (와일드카드)
│ └── (category)/ → 레이아웃 그룹 (URL 미포함)
│ ├── electronics.tsx → /shop/electronics
│ └── clothing.tsx → /shop/clothing
└── api/
└── users.ts → /api/users (API 라우트)
기본 라우트 컴포넌트
// src/routes/index.tsx
export default function HomePage() {
return (
<main>
<h1>홈페이지</h1>
<p>SolidStart 앱에 오신 것을 환영합니다!</p>
</main>
);
}
동적 라우트
// src/routes/blog/[slug].tsx
import { useParams } from '@solidjs/router';
export default function BlogPost() {
const params = useParams();
return (
<article>
<h1>포스트: {params.slug}</h1>
</article>
);
}
중첩 레이아웃
// src/routes/(app).tsx — 중첩 레이아웃 파일
import { Outlet } from '@solidjs/router';
export default function AppLayout() {
return (
<div class="app-layout">
<header>
<nav>
<a href="/">홈</a>
<a href="/dashboard">대시보드</a>
<a href="/profile">프로필</a>
</nav>
</header>
<main>
<Outlet /> {/* 자식 라우트가 여기 렌더링 */}
</main>
<footer>© 2025 My App</footer>
</div>
);
}
// src/routes/(app)/dashboard.tsx — 레이아웃의 자식
export default function Dashboard() {
return <h1>대시보드</h1>;
}
Link와 Navigate
import { A, useNavigate } from '@solidjs/router';
function NavMenu() {
const navigate = useNavigate();
return (
<nav>
{/* A 컴포넌트 — active 클래스 자동 추가 */}
<A href="/" activeClass="active" end>홈</A>
<A href="/about" activeClass="active">소개</A>
<A href="/blog" activeClass="active">블로그</A>
{/* 프로그래매틱 내비게이션 */}
<button onClick={() => navigate('/dashboard')}>
대시보드로 이동
</button>
</nav>
);
}
4. 서버 함수 — "use server"
기본 개념
서버 함수는 "use server" 지시자를 붙이면 서버에서만 실행되는 함수입니다. 클라이언트에서 호출하면 자동으로 RPC(원격 프로시저 호출) 방식으로 요청을 보냅니다.
// src/routes/index.tsx
import { createSignal } from 'solid-js';
// 이 함수는 절대 클라이언트 번들에 포함되지 않음
async function getServerData() {
'use server';
// DB 직접 접근, 환경 변수 사용, 비밀 키 사용 가능
const db = await connectDB();
return db.query('SELECT * FROM products LIMIT 10');
}
export default function HomePage() {
const [data, setData] = createSignal(null);
onMount(async () => {
const result = await getServerData();
setData(result);
});
return <div>{/* ... */}</div>;
}
파일 최상단 "use server" — 파일 전체를 서버 전용으로
// src/lib/db.ts
'use server';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client);
export async function getUsers() {
return db.select().from(users);
}
export async function createUser(data: NewUser) {
return db.insert(users).values(data).returning();
}
폼 Action
SolidStart에서 폼 제출은 action으로 처리합니다. JavaScript 없이도 동작하는 Progressive Enhancement를 지원합니다.
// src/routes/contact.tsx
import { action, useSubmission } from '@solidjs/router';
// action 정의
const sendMessage = action(async (formData: FormData) => {
'use server';
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const message = formData.get('message') as string;
if (!name || !email || !message) {
throw new Error('모든 필드를 입력해주세요');
}
await sendEmail({ name, email, message });
return { success: true };
}, 'sendMessage');
export default function ContactPage() {
const submission = useSubmission(sendMessage);
return (
<div>
<h1>문의하기</h1>
{/* action="..."으로 연결 */}
<form action={sendMessage} method="post">
<input name="name" placeholder="이름" required />
<input name="email" type="email" placeholder="이메일" required />
<textarea name="message" placeholder="메시지" required />
<button type="submit" disabled={submission.pending}>
{submission.pending ? '전송 중...' : '전송'}
</button>
</form>
<Show when={submission.error}>
<p class="error">{submission.error.message}</p>
</Show>
<Show when={submission.result?.success}>
<p class="success">메시지가 전송되었습니다!</p>
</Show>
</div>
);
}
redirect와 쿠키
import { action, redirect } from '@solidjs/router';
import { setCookie } from 'vinxi/http';
const loginAction = action(async (formData: FormData) => {
'use server';
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const user = await verifyCredentials(email, password);
if (!user) throw new Error('이메일 또는 비밀번호가 올바르지 않습니다');
const token = await generateToken(user.id);
// 쿠키 설정
setCookie('auth_token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7일
});
// 리다이렉트
throw redirect('/dashboard');
}, 'login');
5. 데이터 로딩 — createAsync, cache, query
cache와 createAsync — 기본 패턴
// src/lib/queries.ts
import { cache } from '@solidjs/router';
// cache로 감싸면 서버에서 한 번만 실행, 결과를 캐시
export const getProducts = cache(async () => {
'use server';
const products = await db.select().from(productsTable);
return products;
}, 'products'); // 두 번째 인자는 캐시 키
export const getProduct = cache(async (slug: string) => {
'use server';
const [product] = await db
.select()
.from(productsTable)
.where(eq(productsTable.slug, slug));
return product ?? null;
}, 'product');
// src/routes/shop/index.tsx
import { createAsync } from '@solidjs/router';
import { getProducts } from '~/lib/queries';
export default function ShopPage() {
// createAsync — 서버에서 미리 실행, 클라이언트에서 hydration
const products = createAsync(() => getProducts());
return (
<Suspense fallback={<p>상품 로딩 중...</p>}>
<For each={products()}>
{(product) => <ProductCard product={product} />}
</For>
</Suspense>
);
}
동적 파라미터와 데이터 로딩
// src/routes/blog/[slug].tsx
import { useParams, createAsync } from '@solidjs/router';
import { cache } from '@solidjs/router';
const getPost = cache(async (slug: string) => {
'use server';
const post = await db
.select()
.from(postsTable)
.where(eq(postsTable.slug, slug))
.limit(1);
return post[0] ?? null;
}, 'post');
export default function BlogPostPage() {
const params = useParams();
const post = createAsync(() => getPost(params.slug));
return (
<Suspense fallback={<ArticleSkeleton />}>
<Show when={post()} fallback={<p>포스트를 찾을 수 없습니다</p>}>
{(p) => (
<article>
<h1>{p().title}</h1>
<time>{p().publishedAt}</time>
<div innerHTML={p().content} />
</article>
)}
</Show>
</Suspense>
);
}
preload — 데이터 사전 로딩
// src/routes/blog/[slug].tsx
import { cache, createAsync, useParams } from '@solidjs/router';
const getPost = cache(async (slug: string) => {
'use server';
return fetchPost(slug);
}, 'post');
// preload: 라우트 진입 전에 데이터를 미리 가져옴
export const route = {
preload: ({ params }) => getPost(params.slug),
};
export default function BlogPostPage() {
const params = useParams();
const post = createAsync(() => getPost(params.slug));
// preload 덕분에 Suspense가 즉시 해소됨
return <article>{/* ... */}</article>;
}
6. SSR / SSG 모드 설정
SSR (서버 사이드 렌더링) — 기본값
// app.config.ts
import { defineConfig } from '@solidjs/start/config';
export default defineConfig({
server: {
preset: 'node', // Node.js 서버에서 SSR
},
});
모든 라우트는 기본적으로 서버에서 HTML을 렌더링한 뒤 클라이언트에서 hydration합니다.
SSG (정적 사이트 생성)
// app.config.ts
import { defineConfig } from '@solidjs/start/config';
export default defineConfig({
server: {
preset: 'static', // 빌드 시 정적 HTML 파일 생성
prerender: {
crawlLinks: true, // 링크를 따라가며 자동 사전 렌더링
routes: ['/'], // 루트부터 크롤 시작
ignore: ['/admin'], // 제외할 경로
},
},
});
동적 라우트의 경우 prerender 배열에 명시합니다.
prerender: {
routes: [
'/',
'/about',
'/blog/post-1',
'/blog/post-2',
// 실제로는 DB에서 슬러그를 가져와 동적으로 생성
],
},
라우트별 설정
// src/routes/dashboard.tsx — 이 라우트는 CSR만 사용
export const route = {
// 서버 사이드 preload 없음
};
// SPA 모드로 내보내기
export default function Dashboard() {
return <div>대시보드 (클라이언트 전용)</div>;
}
7. 배포 어댑터
Vercel
npm install @solidjs/start
// app.config.ts
export default defineConfig({
server: { preset: 'vercel' },
});
// vercel.json (선택 사항)
{
"buildCommand": "npm run build",
"outputDirectory": ".vercel/output"
}
Cloudflare Pages
// app.config.ts
export default defineConfig({
server: { preset: 'cloudflare-pages' },
});
# 배포
npx wrangler pages deploy .output/public
Cloudflare Workers에서는 process.env 대신 import.meta.env를 사용합니다.
Node.js (자체 서버)
// app.config.ts
export default defineConfig({
server: { preset: 'node' },
});
npm run build
node .output/server/index.mjs
프록시(Nginx) 설정:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Static (완전 정적)
// app.config.ts
export default defineConfig({
server: {
preset: 'static',
prerender: { crawlLinks: true, routes: ['/'] },
},
});
빌드 결과물인 .output/public 폴더를 어떤 정적 호스팅(GitHub Pages, S3, Netlify 등)에도 배포 가능합니다.
8. API 라우트
// src/routes/api/users.ts
import type { APIEvent } from '@solidjs/start/server';
export async function GET(event: APIEvent) {
const users = await db.select().from(usersTable);
return new Response(JSON.stringify(users), {
headers: { 'Content-Type': 'application/json' },
});
}
export async function POST(event: APIEvent) {
const body = await event.request.json();
const [newUser] = await db
.insert(usersTable)
.values(body)
.returning();
return new Response(JSON.stringify(newUser), {
status: 201,
headers: { 'Content-Type': 'application/json' },
});
}
// src/routes/api/users/[id].ts
export async function GET(event: APIEvent) {
const id = event.params.id;
const user = await db
.select()
.from(usersTable)
.where(eq(usersTable.id, parseInt(id)));
if (!user.length) {
return new Response('Not Found', { status: 404 });
}
return Response.json(user[0]);
}
export async function DELETE(event: APIEvent) {
const id = event.params.id;
await db.delete(usersTable).where(eq(usersTable.id, parseInt(id)));
return new Response(null, { status: 204 });
}
9. 실전 예제
예제 1: 블로그 — 목록 + 상세 + API
// src/lib/blog.ts
'use server';
import { cache } from '@solidjs/router';
export interface Post {
id: number;
slug: string;
title: string;
excerpt: string;
content: string;
publishedAt: string;
author: { name: string; avatar: string };
tags: string[];
}
// 목록 조회 (캐시 5분)
export const getPosts = cache(async (tag?: string) => {
const query = db.select().from(postsTable).orderBy(desc(postsTable.publishedAt));
if (tag) query.where(sql`${tag} = ANY(tags)`);
return query;
}, 'posts');
// 단건 조회
export const getPost = cache(async (slug: string) => {
const [post] = await db
.select()
.from(postsTable)
.where(eq(postsTable.slug, slug));
return post ?? null;
}, 'post');
// src/routes/blog/index.tsx
import { createAsync, A } from '@solidjs/router';
import { getPosts } from '~/lib/blog';
import { For, Suspense } from 'solid-js';
export const route = {
preload: () => getPosts(),
};
export default function BlogListPage() {
const posts = createAsync(() => getPosts());
return (
<main class="max-w-3xl mx-auto py-12 px-4">
<h1 class="text-4xl font-bold mb-8">블로그</h1>
<Suspense fallback={<PostsSkeleton />}>
<ul class="space-y-8">
<For each={posts()}>
{(post) => (
<li class="border-b pb-8">
<A href={`/blog/${post.slug}`}>
<h2 class="text-2xl font-semibold hover:underline">{post.title}</h2>
</A>
<time class="text-gray-500 text-sm">{post.publishedAt}</time>
<p class="mt-2 text-gray-700">{post.excerpt}</p>
<div class="mt-3 flex gap-2">
<For each={post.tags}>
{(tag) => (
<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded text-xs">
{tag}
</span>
)}
</For>
</div>
</li>
)}
</For>
</ul>
</Suspense>
</main>
);
}
// src/routes/blog/[slug].tsx
import { createAsync, useParams, A } from '@solidjs/router';
import { getPost } from '~/lib/blog';
import { Show, Suspense } from 'solid-js';
export const route = {
preload: ({ params }) => getPost(params.slug),
};
export default function BlogDetailPage() {
const params = useParams();
const post = createAsync(() => getPost(params.slug));
return (
<Suspense fallback={<ArticleSkeleton />}>
<Show
when={post()}
fallback={
<div class="text-center py-24">
<h1 class="text-2xl">포스트를 찾을 수 없습니다</h1>
<A href="/blog" class="text-blue-600 mt-4 block">블로그 목록으로</A>
</div>
}
>
{(p) => (
<article class="max-w-3xl mx-auto py-12 px-4">
<header class="mb-8">
<h1 class="text-4xl font-bold">{p().title}</h1>
<div class="flex items-center gap-4 mt-4">
<img
src={p().author.avatar}
alt={p().author.name}
class="w-10 h-10 rounded-full"
/>
<div>
<p class="font-medium">{p().author.name}</p>
<time class="text-sm text-gray-500">{p().publishedAt}</time>
</div>
</div>
</header>
<div class="prose" innerHTML={p().content} />
</article>
)}
</Show>
</Suspense>
);
}
예제 2: 인증 흐름 — 로그인/로그아웃
// src/lib/auth.ts
'use server';
import { cache, action, redirect } from '@solidjs/router';
import { getCookie, setCookie, deleteCookie } from 'vinxi/http';
import { db } from '~/lib/db';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
// 현재 세션 유저 조회
export const getSessionUser = cache(async () => {
const token = getCookie('auth_token');
if (!token) return null;
try {
const payload = jwt.verify(token, JWT_SECRET) as { userId: number };
const [user] = await db
.select({ id: users.id, email: users.email, name: users.name, role: users.role })
.from(users)
.where(eq(users.id, payload.userId));
return user ?? null;
} catch {
return null;
}
}, 'session-user');
// 로그인 action
export const loginAction = action(async (formData: FormData) => {
'use server';
const email = String(formData.get('email'));
const password = String(formData.get('password'));
const [user] = await db
.select()
.from(users)
.where(eq(users.email, email));
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return { error: '이메일 또는 비밀번호가 올바르지 않습니다' };
}
const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
setCookie('auth_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
path: '/',
});
throw redirect('/dashboard');
}, 'login');
// 로그아웃 action
export const logoutAction = action(async () => {
'use server';
deleteCookie('auth_token');
throw redirect('/login');
}, 'logout');
// src/routes/login.tsx
import { createAsync, useSubmission } from '@solidjs/router';
import { loginAction, getSessionUser } from '~/lib/auth';
import { Show } from 'solid-js';
import { redirect } from '@solidjs/router';
export const route = {
preload: async () => {
const user = await getSessionUser();
if (user) throw redirect('/dashboard');
},
};
export default function LoginPage() {
const submission = useSubmission(loginAction);
return (
<div class="min-h-screen flex items-center justify-center">
<div class="w-full max-w-md bg-white rounded-xl shadow p-8">
<h1 class="text-2xl font-bold mb-6">로그인</h1>
<form action={loginAction} method="post" class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">이메일</label>
<input
name="email"
type="email"
required
class="w-full border rounded-lg px-3 py-2"
placeholder="you@example.com"
/>
</div>
<div>
<label class="block text-sm font-medium mb-1">비밀번호</label>
<input
name="password"
type="password"
required
class="w-full border rounded-lg px-3 py-2"
/>
</div>
<Show when={submission.result?.error}>
<p class="text-red-500 text-sm">{submission.result?.error}</p>
</Show>
<button
type="submit"
disabled={submission.pending}
class="w-full bg-blue-600 text-white rounded-lg py-2 font-medium disabled:opacity-50"
>
{submission.pending ? '로그인 중...' : '로그인'}
</button>
</form>
</div>
</div>
);
}
// src/routes/(app)/dashboard.tsx — 보호된 라우트
import { createAsync } from '@solidjs/router';
import { getSessionUser, logoutAction } from '~/lib/auth';
import { Show } from 'solid-js';
import { redirect } from '@solidjs/router';
export const route = {
preload: async () => {
const user = await getSessionUser();
if (!user) throw redirect('/login');
},
};
export default function DashboardPage() {
const user = createAsync(() => getSessionUser());
return (
<div class="p-8">
<Show when={user()}>
{(u) => (
<div>
<h1 class="text-3xl font-bold">대시보드</h1>
<p class="mt-2 text-gray-600">안녕하세요, {u().name}님!</p>
<form action={logoutAction} method="post" class="mt-6">
<button type="submit" class="bg-red-500 text-white px-4 py-2 rounded">
로그아웃
</button>
</form>
</div>
)}
</Show>
</div>
);
}
10. 고수 팁
팁 1: 에러 바운더리와 ErrorBoundary
// src/app.tsx
import { ErrorBoundary } from 'solid-js';
export default function App() {
return (
<ErrorBoundary
fallback={(err, reset) => (
<div class="error-page">
<h1>오류가 발생했습니다</h1>
<pre>{err.message}</pre>
<button onClick={reset}>다시 시도</button>
</div>
)}
>
<Router />
</ErrorBoundary>
);
}
팁 2: Middleware로 인증 처리
// src/middleware.ts
import { createMiddleware } from '@solidjs/start/middleware';
export default createMiddleware({
onRequest: [
async (event) => {
const url = new URL(event.request.url);
const token = getCookie(event, 'auth_token');
const protectedPaths = ['/dashboard', '/profile', '/settings'];
if (protectedPaths.some(p => url.pathname.startsWith(p))) {
if (!token || !(await verifyToken(token))) {
return new Response(null, {
status: 302,
headers: { Location: '/login' },
});
}
}
},
],
});
// app.config.ts
export default defineConfig({
middleware: './src/middleware.ts',
});
팁 3: createAsync의 초기 데이터 패턴
// 서버에서 이미 받은 데이터를 클라이언트에서 재사용
const data = createAsync(() => getData(), {
initialValue: props.serverData, // SSR에서 받은 데이터로 초기화
deferStream: true, // 스트리밍 지연
});
팁 4: revalidate로 캐시 무효화
import { revalidate } from '@solidjs/router';
import { getProducts } from '~/lib/queries';
const deleteProduct = action(async (id: number) => {
'use server';
await db.delete(productsTable).where(eq(productsTable.id, id));
// 캐시 무효화 → 다음 렌더링 시 자동 재조회
revalidate(getProducts.key);
}, 'deleteProduct');
팁 5: 스트리밍 SSR 활용
// 무거운 컴포넌트는 Suspense로 감싸서 나머지 페이지를 먼저 전송
export default function ProductPage() {
const product = createAsync(() => getProduct(params.slug));
const reviews = createAsync(() => getReviews(params.slug));
return (
<div>
{/* 빠른 데이터 — 먼저 스트리밍 */}
<Suspense fallback={<ProductSkeleton />}>
<ProductDetail product={product()} />
</Suspense>
{/* 느린 데이터 — 나중에 스트리밍 */}
<Suspense fallback={<ReviewsSkeleton />}>
<ReviewList reviews={reviews()} />
</Suspense>
</div>
);
}
정리
| 기능 | SolidStart API | 설명 |
|---|---|---|
| 라우팅 | src/routes/ 파일 구조 | 파일 이름 = URL 경로 |
| 동적 라우트 | [param].tsx | useParams()로 접근 |
| 레이아웃 | (group).tsx + <Outlet /> | URL 영향 없는 레이아웃 그룹 |
| 서버 함수 | "use server" | 서버 전용 실행, RPC 자동화 |
| 폼 처리 | action() + useSubmission() | Progressive Enhancement |
| 데이터 로딩 | cache() + createAsync() | SSR + 캐싱 |
| 사전 로딩 | route.preload | 라우트 진입 전 데이터 fetch |
| 캐시 무효화 | revalidate() | mutation 후 자동 갱신 |
| 배포 | preset 설정 | Vercel/CF/Node/Static |
다음 장에서는...
18.7 실전 고수 팁에서는 Solid.js를 실무에서 능숙하게 사용하기 위한 고급 패턴들을 다룹니다. 구조 분해 할당의 위험성, batch/untrack을 활용한 성능 최적화, TypeScript 통합, 테스팅, 그리고 React에서 Solid.js로 마이그레이션하는 방법까지 배웁니다.