SolidJS 완전 가이드 | React보다 빠른 반응형 UI 프레임워크
이 글의 핵심
React와 문법은 유사하지만 2배 빠른 SolidJS. Virtual DOM 없이 진짜 반응성(Fine-grained Reactivity)으로 작동하며, 번들 크기는 React의 1/3입니다. JSX를 그대로 사용할 수 있어 마이그레이션이 쉽습니다.
이 글의 핵심
SolidJS는 React보다 2배 빠르고 가벼운 프론트엔드 프레임워크입니다. Virtual DOM 없이 진짜 반응성(Fine-grained Reactivity)으로 작동하며, JSX를 그대로 사용할 수 있어 React 개발자에게 친숙합니다. 번들 크기는 React의 1/3입니다.
목차
SolidJS란?
SolidJS는 2018년 Ryan Carniato가 개발한 반응형 UI 프레임워크입니다.
🚀 핵심 특징
1. Virtual DOM 없음
React:
컴포넌트 렌더링 → Virtual DOM 생성 → Diffing → 실제 DOM 업데이트
SolidJS:
Signal 변경 → 실제 DOM 직접 업데이트
→ 2배 빠름
2. Fine-grained Reactivity
// 상태가 변경되면 정확히 그 부분만 업데이트
const [count, setCount] = createSignal(0);
// count()가 사용된 곳만 재실행
<div>{count()}</div> // 이 부분만 업데이트
3. JSX 그대로 사용
// React와 거의 동일한 문법
function App() {
const [count, setCount] = createSignal(0);
return (
<div>
<p>Count: {count()}</p>
<button onClick={() => setCount(count() + 1)}>
Increment
</button>
</div>
);
}
4. 번들 크기
React + ReactDOM: 45KB (gzip)
SolidJS: 13KB (gzip)
→ 1/3 크기
SolidJS 시작하기
프로젝트 생성
# Vite 템플릿
npm create vite@latest my-solid-app -- --template solid
cd my-solid-app
npm install
npm run dev
# 또는 Degit
npx degit solidjs/templates/js my-app
cd my-app
npm install
npm run dev
# TypeScript 템플릿
npx degit solidjs/templates/ts my-app
프로젝트 구조
my-solid-app/
├── src/
│ ├── App.tsx
│ ├── index.tsx
│ └── index.css
├── index.html
├── package.json
└── vite.config.ts
반응성 (Reactivity)
createSignal (State)
import { createSignal } from 'solid-js';
function Counter() {
// Signal 생성
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal('Alice');
// Getter 함수로 값 읽기
console.log(count()); // 0
console.log(name()); // 'Alice'
// Setter 함수로 값 변경
setCount(10);
setName('Bob');
// 함수형 업데이트
setCount(c => c + 1);
return (
<div>
<p>Count: {count()}</p>
<p>Name: {name()}</p>
<button onClick={() => setCount(count() + 1)}>
Increment
</button>
</div>
);
}
createEffect (Side Effects)
import { createSignal, createEffect } from 'solid-js';
function App() {
const [count, setCount] = createSignal(0);
// count()가 변경될 때마다 실행
createEffect(() => {
console.log('Count changed:', count());
});
// 정리 함수 (cleanup)
createEffect(() => {
const timer = setInterval(() => {
console.log('Tick');
}, 1000);
// 컴포넌트 언마운트 시 실행
return () => clearInterval(timer);
});
return <button onClick={() => setCount(count() + 1)}>Count: {count()}</button>;
}
createMemo (Computed Values)
import { createSignal, createMemo } from 'solid-js';
function App() {
const [count, setCount] = createSignal(0);
// count()가 변경될 때만 재계산 (캐싱)
const doubled = createMemo(() => count() * 2);
const squared = createMemo(() => count() ** 2);
console.log(doubled()); // 0
console.log(squared()); // 0
return (
<div>
<p>Count: {count()}</p>
<p>Doubled: {doubled()}</p>
<p>Squared: {squared()}</p>
<button onClick={() => setCount(count() + 1)}>Increment</button>
</div>
);
}
컴포넌트
함수 컴포넌트
// 컴포넌트는 한 번만 실행됨 (중요!)
function Greeting(props) {
console.log('Component created'); // 한 번만 출력
return <h1>Hello, {props.name}!</h1>;
}
// 사용
<Greeting name="Alice" />
Props
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
function Button(props: ButtonProps) {
// props는 반응형 프록시
return (
<button
onClick={props.onClick}
disabled={props.disabled}
>
{props.label}
</button>
);
}
// 사용
<Button label="Click me" onClick={() => console.log('Clicked')} />
Children
import { JSX } from 'solid-js';
interface CardProps {
title: string;
children: JSX.Element;
}
function Card(props: CardProps) {
return (
<div class="card">
<h2>{props.title}</h2>
<div class="content">
{props.children}
</div>
</div>
);
}
// 사용
<Card title="Welcome">
<p>This is the content</p>
</Card>
조건부 렌더링
Show 컴포넌트
import { Show } from 'solid-js';
function App() {
const [loggedIn, setLoggedIn] = createSignal(false);
return (
<div>
<Show
when={loggedIn()}
fallback={<p>Please log in</p>}
>
<p>Welcome back!</p>
</Show>
<button onClick={() => setLoggedIn(!loggedIn())}>
Toggle
</button>
</div>
);
}
Switch/Match
import { Switch, Match } from 'solid-js';
function App() {
const [status, setStatus] = createSignal<'loading' | 'success' | 'error'>('loading');
return (
<Switch>
<Match when={status() === 'loading'}>
<p>Loading...</p>
</Match>
<Match when={status() === 'success'}>
<p>Success!</p>
</Match>
<Match when={status() === 'error'}>
<p>Error occurred</p>
</Match>
</Switch>
);
}
리스트 렌더링
For 컴포넌트
import { For } from 'solid-js';
function TodoList() {
const [todos, setTodos] = createSignal([
{ id: 1, text: 'Learn SolidJS', completed: false },
{ id: 2, text: 'Build an app', completed: false },
]);
return (
<ul>
<For each={todos()}>
{(todo, index) => (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => {
setTodos(todos => {
const newTodos = [...todos];
newTodos[index()].completed = e.target.checked;
return newTodos;
});
}}
/>
{todo.text}
</li>
)}
</For>
</ul>
);
}
Index 컴포넌트 (Key 기반)
import { Index } from 'solid-js';
// Index는 각 항목이 변경되지 않고 순서만 바뀔 때 최적화됨
function App() {
const [items, setItems] = createSignal(['A', 'B', 'C']);
return (
<ul>
<Index each={items()}>
{(item, index) => (
<li>{index + 1}: {item()}</li>
)}
</Index>
</ul>
);
}
이벤트 처리
function App() {
const [value, setValue] = createSignal('');
const handleClick = (e: MouseEvent) => {
console.log('Clicked!', e);
};
const handleInput = (e: InputEvent) => {
setValue((e.target as HTMLInputElement).value);
};
const handleSubmit = (e: Event) => {
e.preventDefault();
console.log('Submitted:', value());
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value()}
onInput={handleInput}
placeholder="Type something..."
/>
<button type="submit" onClick={handleClick}>
Submit
</button>
</form>
);
}
Stores (복잡한 상태)
createStore
import { createStore } from 'solid-js/store';
function TodoApp() {
const [todos, setTodos] = createStore([
{ id: 1, text: 'Learn SolidJS', completed: false },
{ id: 2, text: 'Build an app', completed: false },
]);
// 특정 항목만 업데이트 (효율적!)
const toggleTodo = (id: number) => {
setTodos(
todo => todo.id === id,
'completed',
completed => !completed
);
};
// 항목 추가
const addTodo = (text: string) => {
setTodos(todos.length, {
id: Date.now(),
text,
completed: false,
});
};
return (
<ul>
<For each={todos}>
{(todo) => (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
)}
</For>
</ul>
);
}
Context API
import { createContext, useContext } from 'solid-js';
// Context 생성
const ThemeContext = createContext();
// Provider
function ThemeProvider(props) {
const [theme, setTheme] = createSignal('light');
const value = {
theme,
toggleTheme: () => setTheme(theme() === 'light' ? 'dark' : 'light'),
};
return (
<ThemeContext.Provider value={value}>
{props.children}
</ThemeContext.Provider>
);
}
// Consumer (Hook)
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// 사용
function Button() {
const { theme, toggleTheme } = useTheme();
return (
<button
class={theme()}
onClick={toggleTheme}
>
Current theme: {theme()}
</button>
);
}
function App() {
return (
<ThemeProvider>
<Button />
</ThemeProvider>
);
}
Solid Router
npm install @solidjs/router
// App.tsx
import { Router, Route, Routes, A } from '@solidjs/router';
import { lazy } from 'solid-js';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const User = lazy(() => import('./pages/User'));
function App() {
return (
<Router>
<nav>
<A href="/">Home</A>
<A href="/about">About</A>
<A href="/user/123">User</A>
</nav>
<Routes>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/user/:id" component={User} />
</Routes>
</Router>
);
}
// pages/User.tsx
import { useParams } from '@solidjs/router';
function User() {
const params = useParams();
return <h1>User ID: {params.id}</h1>;
}
SolidJS vs React
| 기능 | SolidJS | React |
|---|---|---|
| 렌더링 | 실제 DOM | Virtual DOM |
| 반응성 | Fine-grained | Re-render 전체 |
| 성능 | ⚡⚡ 2배 빠름 | ⚡ 빠름 |
| 번들 크기 | 13KB | 45KB |
| 문법 | JSX (거의 동일) | JSX |
| 학습 곡선 | 🟡 중간 | 🟡 중간 |
| 생태계 | 🌱 성장 중 | 🌳 성숙 |
| 서버 컴포넌트 | ✅ SolidStart | ✅ Next.js |
실전 프로젝트: Counter 앱
// App.tsx
import { createSignal, createEffect, Show } from 'solid-js';
function App() {
const [count, setCount] = createSignal(0);
const [history, setHistory] = createSignal<number[]>([]);
// count가 변경될 때마다 history에 추가
createEffect(() => {
setHistory([...history(), count()]);
});
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(0);
return (
<div class="app">
<h1>Counter: {count()}</h1>
<div class="buttons">
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
<button onClick={increment}>+</button>
</div>
<Show when={history().length > 1}>
<div class="history">
<h2>History</h2>
<ul>
<For each={history()}>
{(value, index) => (
<li>Step {index() + 1}: {value}</li>
)}
</For>
</ul>
</div>
</Show>
</div>
);
}
export default App;
핵심 정리
✅ SolidJS의 장점
- 압도적인 성능: React보다 2배 빠름
- 작은 번들 크기: React의 1/3 (13KB)
- 진짜 반응성: Virtual DOM 없이 직접 업데이트
- 익숙한 문법: JSX 그대로 사용
- 쉬운 마이그레이션: React 개발자에게 친숙
🚀 다음 단계
- SolidJS 공식 문서에서 심화 학습
- SolidStart로 풀스택 앱 개발
- SolidJS Discord에서 커뮤니티 참여
시작하기:
npm create vite@latest my-app -- --template solid로 5분 만에 프로젝트를 시작하고, React보다 2배 빠른 성능을 경험하세요! 🚀