Preact Complete Guide | Fast 3KB React Alternative
이 글의 핵심
Preact is a fast 3KB alternative to React with the same modern API. It's perfect for performance-critical applications and works with most React libraries.
Introduction
Preact is a fast 3KB alternative to React with the same modern API. It’s not a reimplementation of React, but a fresh take on the Virtual DOM with a focus on performance and size.
React vs Preact
React (40KB):
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Preact (3KB):
import { h } from 'preact';
import { useState } from 'preact/hooks';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Almost identical!
1. Installation
New Project with Vite
npm create vite@latest my-app -- --template preact-ts
cd my-app
npm install
npm run dev
Add to Existing Project
npm install preact
2. Core Concepts
JSX and h()
import { h } from 'preact';
// JSX (recommended)
const element = <div>Hello</div>;
// h() function (JSX compiles to this)
const element = h('div', null, 'Hello');
Components
import { h } from 'preact';
// Function component
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
// With TypeScript
interface GreetingProps {
name: string;
}
function Greeting({ name }: GreetingProps) {
return <h1>Hello, {name}!</h1>;
}
Props and Children
function Card({ title, children }) {
return (
<div class="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
// Usage
<Card title="My Card">
<p>Card content</p>
</Card>
3. Hooks
useState
import { useState } from 'preact/hooks';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(c => c + 1)}>Increment (updater)</button>
</div>
);
}
useEffect
import { useEffect } from 'preact/hooks';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]); // Re-run when userId changes
return <div>{user?.name}</div>;
}
useMemo
import { useMemo } from 'preact/hooks';
function ExpensiveList({ items, filter }) {
const filtered = useMemo(() => {
console.log('Filtering...');
return items.filter(item => item.includes(filter));
}, [items, filter]);
return (
<ul>
{filtered.map(item => <li key={item}>{item}</li>)}
</ul>
);
}
useCallback
import { useCallback } from 'preact/hooks';
function TodoList({ todos }) {
const [filter, setFilter] = useState('');
const handleFilter = useCallback((e) => {
setFilter(e.target.value);
}, []);
return (
<div>
<input onInput={handleFilter} />
{/* TodoItem won't re-render if handleFilter doesn't change */}
</div>
);
}
useRef
import { useRef } from 'preact/hooks';
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</div>
);
}
4. React Compatibility
Using preact/compat
npm install preact
vite.config.ts:
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
export default defineConfig({
plugins: [preact()],
resolve: {
alias: {
react: 'preact/compat',
'react-dom': 'preact/compat',
},
},
});
Now you can use React libraries!
// Works with Preact via compat
import { useState } from 'react';
import { createPortal } from 'react-dom';
5. Signals (Preact’s Secret Weapon)
npm install @preact/signals
import { signal, computed } from '@preact/signals';
const count = signal(0);
const doubled = computed(() => count.value * 2);
function Counter() {
// No useState needed! Signals auto-update
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
Why Signals?
- No re-renders needed
- Automatically optimized
- Global state without context
6. Routing
npm install preact-router
import { Router, Route } from 'preact-router';
function App() {
return (
<Router>
<Home path="/" />
<Profile path="/profile/:user" />
<NotFound default />
</Router>
);
}
function Profile({ user }) {
return <h1>Profile: {user}</h1>;
}
7. Server-Side Rendering
import { render } from 'preact-render-to-string';
const html = render(<App />);
console.log(html); // <div>...</div>
8. Performance Tips
1. Use Signals for Global State
// Global signal (no context needed)
import { signal } from '@preact/signals';
export const user = signal(null);
// Use anywhere
function Header() {
return <div>Welcome, {user.value?.name}</div>;
}
2. Memoize Components
import { memo } from 'preact/compat';
const ExpensiveComponent = memo(({ data }) => {
// Only re-renders when data changes
return <div>{data}</div>;
});
3. Lazy Load Components
import { lazy, Suspense } from 'preact/compat';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
9. Differences from React
class vs className
// Preact: use "class" (HTML standard)
<div class="container">Hello</div>
// React: use "className"
<div className="container">Hello</div>
// Both work in Preact!
Event Naming
// Preact: lowercase events work
<input onchange={handleChange} />
// React: camelCase only
<input onChange={handleChange} />
// Both work in Preact!
No Synthetic Events
Preact uses native browser events (faster!):
function handleClick(e) {
// e is a native Event, not SyntheticEvent
console.log(e.target);
}
10. Real-World Example
Todo App with Signals
import { signal, computed } from '@preact/signals';
const todos = signal([
{ id: 1, text: 'Learn Preact', done: false },
]);
const filter = signal('all');
const filteredTodos = computed(() => {
if (filter.value === 'all') return todos.value;
return todos.value.filter(t =>
filter.value === 'done' ? t.done : !t.done
);
});
function TodoApp() {
const addTodo = (text) => {
todos.value = [...todos.value, {
id: Date.now(),
text,
done: false,
}];
};
const toggleTodo = (id) => {
todos.value = todos.value.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
};
return (
<div>
<input onKeyUp={(e) => {
if (e.key === 'Enter') {
addTodo(e.target.value);
e.target.value = '';
}
}} />
<div>
<button onClick={() => filter.value = 'all'}>All</button>
<button onClick={() => filter.value = 'active'}>Active</button>
<button onClick={() => filter.value = 'done'}>Done</button>
</div>
<ul>
{filteredTodos.value.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
</div>
);
}
11. Bundle Size Comparison
| Library | Size (gzipped) | Load Time (3G) |
|---|---|---|
| Preact | 3KB | 60ms |
| Vue | 34KB | 680ms |
| React | 40KB | 800ms |
Summary
Preact is the perfect React alternative for performance:
- 3KB vs React’s 40KB (13x smaller)
- Same API - React knowledge transfers
- Signals for ultra-fast state
- Compatible with most React libraries
- Faster rendering and smaller bundles
Key Takeaways:
- Almost identical API to React
- Use
preact/compatfor React libraries - Signals eliminate re-renders
- Use
classinstead ofclassName - Perfect for performance-critical apps
Next Steps:
Resources: