콘텐츠로 이동

SolidJS

가상 DOM 없이 뛰어난 성능을 제공하는 세밀한 반응형 JavaScript UI 프레임워크.

명령어설명
npx degit solidjs/templates/ts my-appTypeScript SolidJS 프로젝트 생성
npx degit solidjs/templates/js my-appJavaScript SolidJS 프로젝트 생성
npm create solid@latestSolidStart CLI로 프로젝트 생성
npm install solid-js기존 프로젝트에 SolidJS 설치
npm install solid-startSolidStart 메타 프레임워크 설치
명령어설명
npm run dev핫 리로드로 개발 서버 시작
npm run build프로덕션 빌드
npm run serve프로덕션 빌드 로컬 미리보기
npm run startSolidStart 서버 시작
# 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/routerSolidJS 라우터 설치
<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-componentsSolid용 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@latestSolidStart 프로젝트 생성
"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()
  })
})
  1. 항상 시그널을 함수로 호출count가 아닌 count(). 괄호를 빠뜨리는 것이 SolidJS에서 가장 흔한 실수입니다. 괄호 없이는 값 대신 getter 함수를 전달하게 됩니다.

  2. props를 구조 분해하지 않기 — 구조 분해는 값을 한 번만 읽으므로 반응성이 깨집니다. props.name을 직접 사용하거나 props 조작에는 splitProps()/mergeProps()를 사용하세요.

  3. 객체 배열에는 <For>, 원시값에는 <Index> 사용<For>는 참조로 키를 지정하고 (재정렬 가능한 객체에 적합) <Index>는 위치로 키를 지정합니다 (단순 값 리스트에 적합).

  4. 복잡한 상태에는 스토어 선호 — 시그널은 단순 값에 적합하지만, 중첩 객체와 배열에는 전체 객체를 교체하지 않고 세밀한 반응성을 제공하는 createStore를 사용하세요.

  5. 명시적 추적에 on() 사용 — 이펙트가 내부에서 읽는 모든 시그널이 아닌 특정 시그널만 추적하려면 on(signal, callback)으로 감싸세요.

  6. SuspenseErrorBoundary 활용 — 비동기 컴포넌트를 Suspense로 감싸서 로딩 상태를 처리하고, ErrorBoundary로 우아한 오류 처리를 하세요.

  7. 라우트를 지연 로드 — 라우트 컴포넌트에 lazy(() => import('./Page'))를 사용하여 코드 분할을 활성화하고 초기 번들 크기를 줄이세요.

  8. 여러 업데이트에 batch() 사용 — 여러 시그널을 한 번에 설정할 때 batch()로 감싸서 여러 번이 아닌 단일 리렌더링을 트리거하세요.

  9. 컴포넌트를 작게 유지 — SolidJS 컴포넌트는 본문을 한 번만 실행합니다 (React처럼 매 렌더링마다가 아닌). 큰 컴포넌트를 분할하여 반응형 업데이트의 범위를 제한하세요.

  10. 시그널에 TypeScript 제네릭 사용createSignal<string | null>(null)은 반응형 상태에 대해 더 나은 타입 안전성과 자동 완성을 제공합니다.

  11. 컴파일 모델 이해 — SolidJS는 빌드 시 JSX를 실제 DOM 연산으로 컴파일합니다. 가상 DOM 디핑이 없어 빠르지만 반응성 규칙 (구조 분해 금지, 시그널 호출)이 중요한 이유이기도 합니다.

  12. untrack()을 아껴서 사용untrack() 내부에서 시그널을 읽으면 의존성 추적이 방지됩니다. 로깅이나 일회성 읽기에 유용하지만 과용하면 버그를 유발할 수 있습니다.