Solid.js & SolidStart 완벽 가이드 — 진짜 반응성으로 React를 대체하는 프레임워크

Solid.js & SolidStart 완벽 가이드 — 진짜 반응성으로 React를 대체하는 프레임워크

이 글의 핵심

Solid.js는 "JSX는 React처럼, 반응성은 MobX/Vue처럼, 런타임은 바닐라 JS처럼"이라는 독특한 조합의 프레임워크입니다. 가상 DOM 없이 **컴포넌트가 1회만 실행**되고 signal 기반 fine-grained 업데이트로 DOM을 직접 갱신해 최상위 성능을 냅니다. SolidStart는 SSR·Streaming·File-based Routing·Server Functions를 갖춘 메타 프레임워크로 2024년 GA됐습니다.

설치

Solid.js 단독 (Vite)

pnpm create vite@latest my-app --template solid-ts
cd my-app && pnpm install && pnpm dev

SolidStart (메타 프레임워크)

pnpm create solid@latest
# 템플릿 선택: bare / minimal / with-tailwindcss / with-auth / ...

Signal 기초

import { createSignal } from "solid-js"

function Counter() {
  const [count, setCount] = createSignal(0)
  return (
    <button onClick={() => setCount(count() + 1)}>
      count: {count()}
    </button>
  )
}
  • count함수: 호출해야 값이 나옴 (구독 추적을 위해)
  • setCount(next) 또는 setCount((c) => c + 1)
  • 컴포넌트 Counter() 자체는 1회만 실행

createEffect

import { createSignal, createEffect } from "solid-js"

function Example() {
  const [count, setCount] = createSignal(0)

  createEffect(() => {
    document.title = `count: ${count()}`
  })

  return <button onClick={() => setCount(count() + 1)}>+</button>
}
  • 의존성 배열 불필요 (자동 추적)
  • 첫 실행 + 의존 변경 시 실행
  • cleanup은 onCleanup() 사용

createMemo

const [items] = createSignal([1, 2, 3])
const total = createMemo(() => items().reduce((a, b) => a + b, 0))

Show / For / Switch

React의 조건/리스트 렌더링 대안.

import { Show, For, Switch, Match } from "solid-js"

<Show when={user()} fallback={<p>Loading…</p>}>
  {(u) => <p>Hello {u().name}</p>}
</Show>

<For each={items()}>
  {(item, i) => <li>{i() + 1}. {item.name}</li>}
</For>

<Switch fallback={<p>none</p>}>
  <Match when={status() === "ok"}><p>✅</p></Match>
  <Match when={status() === "error"}><p>❌</p></Match>
</Switch>

<For>는 key 기반 fine-grained DOM 재사용. map()·filter()로 JSX를 만들면 전체 리스트가 재생성되니 <For> 사용이 권장.

Store (중첩 상태)

import { createStore } from "solid-js/store"

const [state, setState] = createStore({
  user: { name: "JB", prefs: { theme: "dark" } },
  todos: [{ id: 1, done: false, text: "hi" }],
})

setState("user", "prefs", "theme", "light")
setState("todos", 0, "done", true)
setState("todos", (todos) => [...todos, { id: 2, done: false, text: "new" }])

MobX·Vue reactive와 유사. 깊은 경로도 세밀하게 업데이트.

Resources (비동기 데이터)

import { createResource, Show } from "solid-js"

const fetchUser = (id: number) =>
  fetch(`/api/users/${id}`).then((r) => r.json())

function UserCard(props: { id: number }) {
  const [user] = createResource(() => props.id, fetchUser)

  return (
    <Show when={!user.loading} fallback={<p>Loading…</p>}>
      <Show when={!user.error} fallback={<p>Error</p>}>
        <p>{user()!.name}</p>
      </Show>
    </Show>
  )
}
  • 인수 getter → 변경 시 자동 리페치
  • refetch()·mutate()·loading·error 상태

Context

import { createContext, useContext } from "solid-js"

const ThemeContext = createContext<"light" | "dark">("light")

<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>

const theme = useContext(ThemeContext)

React와 비슷하지만 provider가 리렌더 안 됨(컨포넌트 1회 실행) → 성능 부담 없음.

SolidStart: File-based Routing

src/routes/
  index.tsx              → /
  about.tsx              → /about
  blog/
    [slug].tsx           → /blog/:slug
    index.tsx            → /blog
  (group)/
    settings.tsx         → /settings  (그룹화, URL 영향 없음)
// src/routes/blog/[slug].tsx
import { RouteSectionProps, createAsync } from "@solidjs/router"
import { Show } from "solid-js"

async function getPost(slug: string) {
  "use server"
  const res = await fetch(`https://api.example.com/posts/${slug}`)
  return res.json()
}

export default function BlogPost(props: RouteSectionProps) {
  const post = createAsync(() => getPost(props.params.slug))
  return (
    <Show when={post()}>
      <article>
        <h1>{post()!.title}</h1>
        <div innerHTML={post()!.html} />
      </article>
    </Show>
  )
}
  • "use server": Server Function (Next.js와 동일 개념)
  • createAsync: Suspense 통합 비동기 데이터

Server Functions

// src/routes/posts.tsx
import { action, useSubmission } from "@solidjs/router"

const createPost = action(async (form: FormData) => {
  "use server"
  const title = form.get("title") as string
  const db = await import("~/db")
  const row = await db.insertPost(title)
  return { id: row.id }
}, "createPost")

export default function Posts() {
  const submission = useSubmission(createPost)
  return (
    <form method="post" action={createPost}>
      <input name="title" required />
      <button disabled={submission.pending}>Submit</button>
      {submission.result && <p>Created #{submission.result.id}</p>}
    </form>
  )
}

서버·클라이언트 경계가 자연스럽게 타입 안전 연결되고 progressive enhancement 지원.

Data Loaders (load)

// src/routes/dashboard.tsx
import { createAsync, query } from "@solidjs/router"

const getDashboardData = query(async () => {
  "use server"
  return { stats: await loadStats(), recent: await loadRecent() }
}, "dashboard")

export default function Dashboard() {
  const data = createAsync(() => getDashboardData())
  return (
    <Show when={data()}>
      <Stats data={data()!.stats} />
      <RecentList items={data()!.recent} />
    </Show>
  )
}

query는 캐시·deduplication·재검증까지 지원하는 SolidStart 공식 데이터 패턴.

Streaming SSR

SolidStart는 기본적으로 streaming SSR: 각 Suspense 경계가 준비되는 대로 HTML이 흘러나옵니다.

import { Suspense } from "solid-js"

export default function Page() {
  return (
    <>
      <Hero />
      <Suspense fallback={<SkeletonFeed />}>
        <Feed />
      </Suspense>
      <Suspense fallback={<SkeletonSidebar />}>
        <Sidebar />
      </Suspense>
    </>
  )
}

Time-to-first-byte·LCP 개선에 직접 기여.

Islands Architecture

import { clientOnly } from "@solidjs/start"

const HeavyWidget = clientOnly(() => import("~/components/HeavyWidget"))

<HeavyWidget fallback={<p>Loading…</p>} />

정적 HTML + 부분 하이드레이션 패턴도 지원.

성능 특성

  • 가상 DOM 없음 → 업데이트 오버헤드 최소
  • 번들 크기: core ~5KB gzip
  • 벤치마크(JS Framework Benchmark)에서 상위 3위 안
  • React 대비 메모리 사용 20-40% 감소

TanStack 통합

pnpm add @tanstack/solid-query
import { createQuery } from "@tanstack/solid-query"

function Posts() {
  const query = createQuery(() => ({
    queryKey: ["posts"],
    queryFn: () => fetch("/api/posts").then(r => r.json()),
  }))

  return (
    <Show when={!query.isLoading}>
      <For each={query.data}>{(p) => <li>{p.title}</li>}</For>
    </Show>
  )
}

TanStack Query·Table·Virtual·Router 모두 공식 Solid 포트가 있어 프레임워크 간 지식 이전이 쉽습니다.

UI 라이브러리

  • Kobalte (@kobalte/core): Radix 스타일 접근성 프리미티브 Solid 구현
  • SolidAria: react-aria 포트
  • solid-ui: shadcn/ui 솔리드판
  • Motion One for Solid: 애니메이션

배포

Vercel / Netlify / Cloudflare

// vite.config.ts
import { defineConfig } from "@solidjs/start/config"

export default defineConfig({
  server: {
    preset: "cloudflare_pages",   // 또는 "vercel", "netlify", "node"
  },
})

Nitro 기반 어댑터로 거의 모든 플랫폼 타겟 가능.

Docker

FROM node:20-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/.output .output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

디버깅

  • Solid Devtools (solid-devtools): signal·effect 트리 시각화
  • console.log는 setup 1회에만 실행되므로 반응 로그는 createEffect(() => console.log(...)) 안에 작성

트러블슈팅

값이 업데이트 안 됨

  • Signal은 함수 호출 필수: count() (O), count (X)
  • store 깊은 변경은 setState의 경로 스타일 사용

<For> 아이템이 리렌더 안 됨

  • 아이템 객체를 교체 시 signal로 감싸거나 store 사용
  • 프리미티브 배열은 값 변경이 감지되도록 setter 사용

Server Function이 클라이언트로 번들됨

  • 함수 최상단에 "use server" 지시어
  • 서버 전용 모듈은 import "./server-only" 대신 .server.ts 네이밍

React 라이브러리 포팅

  • 단순 컴포넌트는 수동 포팅 가능
  • Context 위주·상태 거의 없는 컴포넌트는 10-30분 작업
  • 순수 계산 로직은 그대로 복사

채택 기준

쓰기 좋은 경우

  • 성능·번들 크기 최우선(위젯, 임베디드, 대시보드)
  • React 문법은 유지하되 훅 재실행 이슈에서 벗어나고 싶음
  • SSR/Streaming·Server Functions가 필요
  • TanStack 생태계 이미 활용

피해야 할 경우

  • React Native 필요 (Solid에도 대체가 있으나 성숙도 낮음)
  • 방대한 팀·사내 React 컴포넌트 자산
  • 사용자 다수가 React-only 경험

체크리스트

  • Signal·Resource·Store 3대 개념 이해
  • <For>·<Show> 렌더링 패턴 정착
  • SolidStart의 Server Functions로 데이터 페칭 통합
  • Streaming SSR + Suspense 설계
  • TanStack Query·Table·Router 조합
  • 배포 어댑터(Nitro preset) 선택
  • Kobalte 등 접근성 프리미티브 활용

마무리

Solid.js는 “React의 문법으로 더 빠른 반응성을 쓸 수 없을까”라는 질문에 가장 완성도 높은 답을 내놓은 프레임워크입니다. 가상 DOM 없이 fine-grained 업데이트를 하면서도 JSX·Context·Suspense 같은 React 개념을 대부분 공유해 학습이 빠릅니다. SolidStart가 2024 GA로 메타 프레임워크 공백을 메워 Next.js 수준의 DX로 프로덕션 앱을 작성할 수 있게 되었고, TC39 Signals 프로포절의 기반이 될 만큼 설계가 검증되어 있습니다. 당장 React 팀 전체를 옮기는 것은 아니더라도, 성능 민감한 제품·위젯·임베디드 UI에서 Solid로 작은 영역을 실험해보는 것만으로도 현대 반응성의 방향을 체감할 수 있습니다.

관련 글

  • React 완벽 가이드
  • Svelte 5 Runes 완벽 가이드
  • Next.js 완벽 가이드
  • TanStack Query v5 완벽 가이드