가상 DOM 없이 뛰어난 성능을 제공하는 세밀한 반응형 JavaScript UI 프레임워크.
| 명령어 | 설명 |
|---|
npx degit solidjs/templates/ts my-app | TypeScript SolidJS 프로젝트 생성 |
npx degit solidjs/templates/js my-app | JavaScript SolidJS 프로젝트 생성 |
npm create solid@latest | SolidStart CLI로 프로젝트 생성 |
npm install solid-js | 기존 프로젝트에 SolidJS 설치 |
npm install solid-start | SolidStart 메타 프레임워크 설치 |
| 명령어 | 설명 |
|---|
npm run dev | 핫 리로드로 개발 서버 시작 |
npm run build | 프로덕션 빌드 |
npm run serve | 프로덕션 빌드 로컬 미리보기 |
npm run start | SolidStart 서버 시작 |
# Bare SolidJS (Vite)
npx degit solidjs/templates/ts my-app
cd my-app && npm install
# SolidStart (full-stack meta-framework)
npm create solid@latest my-app
# Choose template: bare, with-auth, with-mdx, with-prisma, with-tailwindcss
# Community templates
npx degit solidjs/templates/ts-windicss my-app # WindiCSS
npx degit solidjs/templates/ts-unocss my-app # UnoCSS
npx degit solidjs/templates/ts-bootstrap my-app # Bootstrap
npx degit solidjs/templates/ts-sass my-app # Sass
| 명령어 | 설명 |
|---|
function App() { return <div>Hello</div> } | 기본 컴포넌트 정의 |
<MyComponent name="value" /> | 컴포넌트에 props 전달 |
props.children | 자식 콘텐츠 접근 |
<div class={styles.container}> | CSS 클래스 적용 (className이 아닌 class 사용) |
<div classList={{ active: isActive() }}> | 조건부 CSS 클래스 |
<div style={{ color: 'red', 'font-size': '14px' }}> | 인라인 스타일 적용 |
<div innerHTML={htmlString} /> | 원시 HTML 콘텐츠 설정 |
<div ref={myRef}> | DOM 참조 연결 |
| 명령어 | 설명 |
|---|
<div onClick={handler}> | 위임된 이벤트 연결 |
<div on:click={handler}> | 네이티브 DOM 이벤트 연결 (위임 우회) |
<div onInput={(e) => setValue(e.target.value)}> | 입력 이벤트 처리 |
<div on:keydown={(e) => handleKey(e)}> | 네이티브 키보드 이벤트 처리 |
<div onClick={[handler, data]}> | 이벤트 핸들러에 데이터 전달 |
import { type Component, type ParentComponent } from 'solid-js'
// Typed component with props
interface UserProps {
name: string
age: number
role?: string
}
const UserCard: Component<UserProps> = (props) => {
return (
<div class="card">
<h2>{props.name}</h2>
<p>Age: {props.age}</p>
<p>Role: {props.role ?? 'Member'}</p>
</div>
)
}
// Parent component (accepts children)
const Layout: ParentComponent = (props) => {
return (
<div class="layout">
<header>My App</header>
<main>{props.children}</main>
<footer>Footer</footer>
</div>
)
}
// Splitting props for forwarding
import { splitProps } from 'solid-js'
const Button: Component<ButtonProps> = (props) => {
const [local, rest] = splitProps(props, ['variant', 'size'])
return (
<button
class={`btn btn-${local.variant} btn-${local.size}`}
{...rest}
/>
)
}
// Merging default props
import { mergeProps } from 'solid-js'
const Alert: Component<AlertProps> = (rawProps) => {
const props = mergeProps({ type: 'info', dismissible: false }, rawProps)
return <div class={`alert alert-${props.type}`}>{props.children}</div>
}
| 명령어 | 설명 |
|---|
const [count, setCount] = createSignal(0) | 반응형 시그널 생성 |
count() | 시그널 값 읽기 (반드시 함수로 호출) |
setCount(5) | 시그널을 특정 값으로 설정 |
setCount(prev => prev + 1) | 이전 값으로 시그널 업데이트 |
const [name, setName] = createSignal<string>() | 타입이 지정된 시그널 생성 |
| 명령어 | 설명 |
|---|
createEffect(() => console.log(count())) | 시그널 변경 시 사이드 이펙트 실행 |
createEffect(on(count, (v) => log(v))) | 특정 시그널을 명시적으로 추적 |
createEffect(on([a, b], ([a, b]) => ...)) | 여러 시그널을 명시적으로 추적 |
const double = createMemo(() => count() * 2) | 파생/계산 값 생성 |
createRenderEffect(() => ...) | DOM 페인트 전에 실행되는 이펙트 |
createComputed(() => ...) | 렌더링 중 동기 이펙트 |
| 명령어 | 설명 |
|---|
onMount(() => { ... }) | 컴포넌트 마운트 시 한 번 실행 |
onCleanup(() => { ... }) | 컴포넌트 언마운트 시 정리 실행 |
batch(() => { setA(1); setB(2) }) | 여러 시그널 업데이트 배치 처리 |
untrack(() => value()) | 의존성 추적 없이 시그널 읽기 |
import { createSignal, createEffect, createMemo, on, batch, untrack } from 'solid-js'
function Counter() {
const [count, setCount] = createSignal(0)
const [step, setStep] = createSignal(1)
// Derived value (memoized, recalculates only when count changes)
const doubled = createMemo(() => count() * 2)
const isEven = createMemo(() => count() % 2 === 0)
// Effect: runs when any accessed signal changes
createEffect(() => {
console.log(`Count is now: ${count()}`)
// Automatically tracks count() as a dependency
})
// Explicit tracking with on()
createEffect(on(count, (value, prev) => {
console.log(`Changed from ${prev} to ${value}`)
}, { defer: true })) // defer: skip initial run
// Batch updates (single re-render)
const reset = () => batch(() => {
setCount(0)
setStep(1)
})
// Read without creating dependency
const logWithoutTracking = () => {
const current = untrack(() => count())
console.log('Snapshot:', current)
}
return (
<div>
<p>Count: {count()} (doubled: {doubled()}, even: {String(isEven())})</p>
<button onClick={() => setCount(c => c + step())}>
Add {step()}
</button>
<button onClick={reset}>Reset</button>
</div>
)
}
| 명령어 | 설명 |
|---|
<Show when={loggedIn()} fallback={<Login/>}> | 조건부 렌더링 |
<For each={items()}>{(item) => <li>{item}</li>}</For> | 리스트 렌더링 (참조로 키 지정) |
<Index each={items()}>{(item, i) => <li>{item()}</li>}</Index> | 리스트 렌더링 (인덱스로 키 지정) |
<Switch><Match when={a()}>A</Match></Switch> | Switch/Case 렌더링 |
<ErrorBoundary fallback={err => <p>{err}</p>}> | 렌더링 오류 포착 |
<Suspense fallback={<Loading/>}> | 비동기 로딩 중 폴백 표시 |
<Dynamic component={MyComp} /> | 동적 컴포넌트 렌더링 |
<Portal mount={document.body}> | 다른 DOM 노드에 렌더링 |
import { Show, For, Index, Switch, Match, Suspense, ErrorBoundary } from 'solid-js'
function Dashboard() {
const [user, setUser] = createSignal(null)
const [items, setItems] = createSignal([
{ id: 1, name: 'Alpha', status: 'active' },
{ id: 2, name: 'Beta', status: 'inactive' },
])
const [view, setView] = createSignal('list')
return (
<div>
{/* Show: conditional rendering with fallback */}
<Show when={user()} fallback={<p>Please log in</p>}>
{(u) => <p>Welcome, {u().name}!</p>}
</Show>
{/* For: keyed by reference (best for objects) */}
<For each={items()}>
{(item, index) => (
<div>
<span>{index() + 1}. {item.name}</span>
<span> ({item.status})</span>
</div>
)}
</For>
{/* Switch/Match: multiple conditions */}
<Switch fallback={<p>Unknown view</p>}>
<Match when={view() === 'list'}>
<ListView />
</Match>
<Match when={view() === 'grid'}>
<GridView />
</Match>
<Match when={view() === 'table'}>
<TableView />
</Match>
</Switch>
{/* ErrorBoundary: catch errors in child tree */}
<ErrorBoundary fallback={(err, reset) => (
<div>
<p>Error: {err.message}</p>
<button onClick={reset}>Try Again</button>
</div>
)}>
<RiskyComponent />
</ErrorBoundary>
</div>
)
}
| 명령어 | 설명 |
|---|
const [store, setStore] = createStore({}) | 반응형 스토어 생성 |
setStore('name', 'Alice') | 경로로 스토어 속성 업데이트 |
setStore('users', 0, 'name', 'Bob') | 중첩된 스토어 속성 업데이트 |
setStore('list', l => [...l, item]) | 스토어 배열에 항목 추가 |
setStore('list', i => i.id === 1, 'done', true) | 일치하는 배열 항목 업데이트 |
produce(s => { s.count++ }) | 뮤터블 스타일 스토어 업데이트 |
reconcile(newData) | 스토어 데이터 효율적 교체 |
unwrap(store) | 프록시되지 않은 원시 데이터 가져오기 |
import { createStore, produce, reconcile, unwrap } from 'solid-js/store'
interface Todo {
id: number
text: string
completed: boolean
}
interface AppState {
todos: Todo[]
filter: 'all' | 'active' | 'completed'
user: { name: string; settings: { theme: string } }
}
function TodoApp() {
const [state, setState] = createStore<AppState>({
todos: [],
filter: 'all',
user: { name: 'Alice', settings: { theme: 'dark' } },
})
// Add item
const addTodo = (text: string) => {
setState('todos', todos => [
...todos,
{ id: Date.now(), text, completed: false }
])
}
// Toggle specific item by matching
const toggleTodo = (id: number) => {
setState('todos', todo => todo.id === id, 'completed', c => !c)
}
// Delete item (filter produces new array)
const deleteTodo = (id: number) => {
setState('todos', todos => todos.filter(t => t.id !== id))
}
// Mutable-style updates with produce
const clearCompleted = () => {
setState(produce((s) => {
s.todos = s.todos.filter(t => !t.completed)
}))
}
// Deep nested update
const setTheme = (theme: string) => {
setState('user', 'settings', 'theme', theme)
}
// Replace entire store data (reconcile diffs efficiently)
const loadFromServer = async () => {
const data = await fetch('/api/todos').then(r => r.json())
setState('todos', reconcile(data))
}
return (/* ... */)
}
| 명령어 | 설명 |
|---|
const [data] = createResource(fetchFn) | 비동기 리소스 생성 |
const [data] = createResource(id, fetchFn) | 반응형 소스 시그널이 있는 리소스 |
data() | 리소스 데이터 접근 |
data.loading | 리소스 로딩 중인지 확인 |
data.error | 리소스 오류 접근 |
data.latest | 마지막 해결된 값 가져오기 (리페치에도 유지) |
data.state | 리소스 상태 가져오기 (‘unresolved’, ‘pending’, ‘ready’, ‘errored’) |
const { refetch } = data | 수동으로 리페치 트리거 |
createResource(source, fetcher, { initialValue }) | 초기값이 있는 리소스 |
import { createResource, createSignal, Suspense, ErrorBoundary } from 'solid-js'
interface User {
id: number
name: string
email: string
}
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error(`User ${id} not found`)
return res.json()
}
function UserProfile() {
const [userId, setUserId] = createSignal(1)
// Resource refetches automatically when userId() changes
const [user, { refetch, mutate }] = createResource(userId, fetchUser)
// Optimistic update
const updateName = async (newName: string) => {
const prev = user()
mutate({ ...prev!, name: newName }) // optimistic
try {
await fetch(`/api/users/${userId()}`, {
method: 'PATCH',
body: JSON.stringify({ name: newName }),
})
} catch {
mutate(prev) // rollback on error
}
}
return (
<ErrorBoundary fallback={<p>Failed to load user</p>}>
<Suspense fallback={<p>Loading user...</p>}>
<div>
<h2>{user()?.name}</h2>
<p>{user()?.email}</p>
<p>State: {user.state}</p>
<button onClick={refetch} disabled={user.loading}>
Refresh
</button>
<button onClick={() => setUserId(id => id + 1)}>
Next User
</button>
</div>
</Suspense>
</ErrorBoundary>
)
}
| 명령어 | 설명 |
|---|
npm install @solidjs/router | SolidJS 라우터 설치 |
<Router><Route path="/" component={Home}/></Router> | 기본 라우트 정의 |
<Route path="/users/:id" component={User}/> | 매개변수가 있는 라우트 |
<Route path="/*all" component={NotFound}/> | 캐치올 라우트 |
const params = useParams() | 라우트 매개변수 접근 |
const [searchParams, setSearchParams] = useSearchParams() | 쿼리 매개변수 접근 |
const navigate = useNavigate() | 프로그래밍 방식 네비게이션 |
navigate('/dashboard') | 경로로 이동 |
<A href="/about">About</A> | 네비게이션 링크 컴포넌트 |
<A href="/about" activeClass="active"> | 활성 스타일이 있는 링크 |
import { Router, Route, A, useParams, useNavigate, useSearchParams } from '@solidjs/router'
import { lazy } from 'solid-js'
// Lazy-loaded routes
const Dashboard = lazy(() => import('./pages/Dashboard'))
const UserProfile = lazy(() => import('./pages/UserProfile'))
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<Router>
<nav>
<A href="/" activeClass="active" end>Home</A>
<A href="/dashboard" activeClass="active">Dashboard</A>
<A href="/settings" activeClass="active">Settings</A>
</nav>
<Route path="/" component={Home} />
<Route path="/dashboard" component={Dashboard} />
<Route path="/users/:id" component={UserProfile} />
<Route path="/settings" component={Settings} />
<Route path="/*all" component={NotFound} />
</Router>
)
}
// Nested routes
function App() {
return (
<Router>
<Route path="/admin" component={AdminLayout}>
<Route path="/" component={AdminDashboard} />
<Route path="/users" component={AdminUsers} />
<Route path="/users/:id" component={AdminUserDetail} />
</Route>
</Router>
)
}
import { createContext, useContext, type ParentComponent } from 'solid-js'
import { createStore } from 'solid-js/store'
interface AuthState {
user: { name: string; role: string } | null
token: string | null
}
interface AuthContextValue {
state: AuthState
login: (token: string, user: AuthState['user']) => void
logout: () => void
isAuthenticated: () => boolean
}
const AuthContext = createContext<AuthContextValue>()
const AuthProvider: ParentComponent = (props) => {
const [state, setState] = createStore<AuthState>({
user: null,
token: null,
})
const value: AuthContextValue = {
state,
login: (token, user) => {
setState({ token, user })
},
logout: () => {
setState({ token: null, user: null })
},
isAuthenticated: () => state.token !== null,
}
return (
<AuthContext.Provider value={value}>
{props.children}
</AuthContext.Provider>
)
}
function useAuth() {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth must be used within AuthProvider')
return context
}
// Usage
function UserMenu() {
const auth = useAuth()
return (
<Show when={auth.isAuthenticated()} fallback={<LoginButton />}>
<p>Hello, {auth.state.user?.name}</p>
<button onClick={auth.logout}>Logout</button>
</Show>
)
}
| 명령어 | 설명 |
|---|
import styles from './App.module.css' | CSS 모듈 임포트 |
<div class={styles.container}> | CSS 모듈 클래스 적용 |
npm install solid-styled-components | Solid용 styled-components 설치 |
const Btn = styled('button')\color: red“ | 스타일드 컴포넌트 생성 |
<div class="static-class"> | 정적 CSS 클래스 적용 |
# Install Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
export default {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: { extend: {} },
plugins: [],
}
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
| 명령어 | 설명 |
|---|
npm create solid@latest | SolidStart 프로젝트 생성 |
"use server" 디렉티브 | 함수를 서버 전용으로 표시 |
createServerAction$() | 서버 액션 생성 |
createRouteData() | 서버에서 라우트 데이터 로드 |
<Title>Page Title</Title> | 페이지 제목 설정 (@solidjs/meta에서) |
<Meta name="description" content="..." /> | 메타 태그 설정 |
// src/routes/todos.tsx
import { createAsync, query, action, redirect } from '@solidjs/router'
// Server query (data loading)
const getTodos = query(async () => {
'use server'
const db = await getDatabase()
return db.todos.findMany()
}, 'todos')
// Server action (mutations)
const addTodo = action(async (formData: FormData) => {
'use server'
const text = formData.get('text') as string
const db = await getDatabase()
await db.todos.create({ data: { text, completed: false } })
throw redirect('/todos') // revalidates
})
export default function TodosPage() {
const todos = createAsync(() => getTodos())
return (
<div>
<form action={addTodo} method="post">
<input name="text" placeholder="New todo" required />
<button type="submit">Add</button>
</form>
<Suspense fallback={<p>Loading...</p>}>
<For each={todos()}>
{(todo) => <p>{todo.text}</p>}
</For>
</Suspense>
</div>
)
}
npm install -D vitest @solidjs/testing-library @testing-library/jest-dom jsdom
npm install -D vite-plugin-solid
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solid()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test-setup.ts',
transformMode: { web: [/\.[jt]sx?$/] },
},
})
import { render, screen, fireEvent } from '@solidjs/testing-library'
import { describe, it, expect } from 'vitest'
import Counter from './Counter'
describe('Counter', () => {
it('renders initial count', () => {
render(() => <Counter initialCount={5} />)
expect(screen.getByText('Count: 5')).toBeInTheDocument()
})
it('increments on click', async () => {
render(() => <Counter initialCount={0} />)
const button = screen.getByRole('button', { name: /increment/i })
fireEvent.click(button)
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
})
-
항상 시그널을 함수로 호출 — count가 아닌 count(). 괄호를 빠뜨리는 것이 SolidJS에서 가장 흔한 실수입니다. 괄호 없이는 값 대신 getter 함수를 전달하게 됩니다.
-
props를 구조 분해하지 않기 — 구조 분해는 값을 한 번만 읽으므로 반응성이 깨집니다. props.name을 직접 사용하거나 props 조작에는 splitProps()/mergeProps()를 사용하세요.
-
객체 배열에는 <For>, 원시값에는 <Index> 사용 — <For>는 참조로 키를 지정하고 (재정렬 가능한 객체에 적합) <Index>는 위치로 키를 지정합니다 (단순 값 리스트에 적합).
-
복잡한 상태에는 스토어 선호 — 시그널은 단순 값에 적합하지만, 중첩 객체와 배열에는 전체 객체를 교체하지 않고 세밀한 반응성을 제공하는 createStore를 사용하세요.
-
명시적 추적에 on() 사용 — 이펙트가 내부에서 읽는 모든 시그널이 아닌 특정 시그널만 추적하려면 on(signal, callback)으로 감싸세요.
-
Suspense와 ErrorBoundary 활용 — 비동기 컴포넌트를 Suspense로 감싸서 로딩 상태를 처리하고, ErrorBoundary로 우아한 오류 처리를 하세요.
-
라우트를 지연 로드 — 라우트 컴포넌트에 lazy(() => import('./Page'))를 사용하여 코드 분할을 활성화하고 초기 번들 크기를 줄이세요.
-
여러 업데이트에 batch() 사용 — 여러 시그널을 한 번에 설정할 때 batch()로 감싸서 여러 번이 아닌 단일 리렌더링을 트리거하세요.
-
컴포넌트를 작게 유지 — SolidJS 컴포넌트는 본문을 한 번만 실행합니다 (React처럼 매 렌더링마다가 아닌). 큰 컴포넌트를 분할하여 반응형 업데이트의 범위를 제한하세요.
-
시그널에 TypeScript 제네릭 사용 — createSignal<string | null>(null)은 반응형 상태에 대해 더 나은 타입 안전성과 자동 완성을 제공합니다.
-
컴파일 모델 이해 — SolidJS는 빌드 시 JSX를 실제 DOM 연산으로 컴파일합니다. 가상 DOM 디핑이 없어 빠르지만 반응성 규칙 (구조 분해 금지, 시그널 호출)이 중요한 이유이기도 합니다.
-
untrack()을 아껴서 사용 — untrack() 내부에서 시그널을 읽으면 의존성 추적이 방지됩니다. 로깅이나 일회성 읽기에 유용하지만 과용하면 버그를 유발할 수 있습니다.