18.7 실전 고수 팁
Solid.js의 기본을 익혔다면, 이제 실무에서 자주 만나는 함정을 피하고 성능과 유지보수성을 극대화하는 고급 패턴을 배울 차례입니다. 이 장은 Solid.js로 프로덕션 수준의 앱을 만드는 개발자라면 반드시 숙지해야 할 내용으로 가득 차 있습니다.
1. 구조 분해 할당 절대 금지
왜 반응성이 손실되는가?
Solid.js의 반응성은 함수 호출 시점에 의존성을 추적합니다. 시그널은 getter 함수이므로, 해당 함수를 리액티브 컨텍스트(effect, memo, JSX) 안에서 실제로 호출해야 추적이 이루어집니다.
구조 분해 할당은 함수를 즉시 호출해 값을 꺼내기 때문에, 이후 시그널이 변해도 꺼낸 값은 변하지 않습니다.
import { createSignal, createEffect } from 'solid-js';
const [count, setCount] = createSignal(0);
// ❌ 잘못된 패턴 — 반응성 손실
const { value } = { value: count() }; // count()를 즉시 호출해 0을 복사
createEffect(() => {
console.log(value); // 항상 0, count가 바뀌어도 재실행 안 됨
});
// ✅ 올바른 패턴 — 함수 참조 유지
createEffect(() => {
console.log(count()); // count() 호출 시점에 추적 → 변경 시 재실행
});
Props 구조 분해 — 가장 흔한 실수
// ❌ 절대 금지 — props 구조 분해
function UserCard({ name, age, email }) { // name, age, email은 정적 복사본
return (
<div>
<h2>{name}</h2> {/* 부모가 name을 바꿔도 리렌더링 안 됨 */}
<p>{age}세</p>
<p>{email}</p>
</div>
);
}
// ✅ 올바른 패턴 — props 객체 그대로 사용
function UserCard(props) {
return (
<div>
<h2>{props.name}</h2> {/* 부모가 바꾸면 즉시 반영 */}
<p>{props.age}세</p>
<p>{props.email}</p>
</div>
);
}
splitProps — 안전한 props 분리
외부로 전달할 props와 내부에서 쓸 props를 분리해야 할 때:
import { splitProps, mergeProps } from 'solid-js';
function Button(props) {
// 내부용과 전달용을 안전하게 분리
const [local, rest] = splitProps(props, ['children', 'class', 'loading']);
return (
<button
{...rest} // type, onClick 등은 그대로 전달
class={`btn ${local.class ?? ''}`}
disabled={local.loading || rest.disabled}
>
{local.loading ? <Spinner /> : local.children}
</button>
);
}
// mergeProps — 기본값 설정 (defaultProps 대체)
function Avatar(props) {
const merged = mergeProps({ size: 40, shape: 'circle' }, props);
return (
<img
src={merged.src}
width={merged.size}
height={merged.size}
style={{ 'border-radius': merged.shape === 'circle' ? '50%' : '4px' }}
/>
);
}
스토어 구조 분해도 금지
import { createStore } from 'solid-js/store';
const [state, setState] = createStore({ user: { name: '홍길동', age: 30 } });
// ❌ 잘못된 패턴
const { name, age } = state.user; // 정적 복사
// ✅ 올바른 패턴
function Profile() {
return <p>{state.user.name} ({state.user.age}세)</p>; // 경로를 통한 접근
}
2. batch — 성능 최적화를 위한 일괄 업데이트
batch의 원리
Solid.js는 기본적으로 시그널 업데이트마다 즉시 effect와 컴포넌트를 재실행합니다. 여러 시그널을 연달아 업데이트하면 중간 상태가 노출되고 불필요한 렌더링이 발생합니다.
batch는 콜백 내의 모든 업데이트를 모았다가 한 번에 처리합니다.
import { createSignal, batch, createEffect } from 'solid-js';
const [firstName, setFirstName] = createSignal('홍');
const [lastName, setLastName] = createSignal('길동');
const [age, setAge] = createSignal(30);
// batch 없이 — effect가 3번 실행
setFirstName('김'); // effect 1회 실행 (중간 상태: 김길동 30세)
setLastName('철수'); // effect 2회 실행 (중간 상태: 김철수 30세)
setAge(25); // effect 3회 실행 (최종 상태: 김철수 25세)
// batch 사용 — effect가 1번만 실행
batch(() => {
setFirstName('김'); // 아직 적용 안 됨
setLastName('철수'); // 아직 적용 안 됨
setAge(25); // 아직 적용 안 됨
});
// 여기서 한꺼번에 적용 → effect 1회만 실행
실전 활용 예시
import { createStore, produce } from 'solid-js/store';
import { batch } from 'solid-js';
function FormManager() {
const [form, setForm] = createStore({
values: { name: '', email: '', phone: '' },
errors: {},
isSubmitting: false,
isSuccess: false,
});
const submitForm = async () => {
// 제출 시작 상태를 한 번에 업데이트
batch(() => {
setForm('isSubmitting', true);
setForm('errors', {});
setForm('isSuccess', false);
});
try {
await api.submitForm(form.values);
// 성공 상태를 한 번에 업데이트
batch(() => {
setForm('isSubmitting', false);
setForm('isSuccess', true);
setForm('values', { name: '', email: '', phone: '' });
});
} catch (error) {
// 실패 상태를 한 번에 업데이트
batch(() => {
setForm('isSubmitting', false);
setForm('errors', error.fieldErrors ?? { general: error.message });
});
}
};
return (/* ... */);
}
이벤트 핸들러는 자동 batch
Solid.js v1.4+에서는 DOM 이벤트 핸들러 내부가 자동으로 batch 처리됩니다. batch를 명시적으로 써야 하는 경우는 비동기 코드, setTimeout/setInterval, 커스텀 이벤트 등입니다.
// DOM 이벤트 — 자동 batch (명시적 batch 불필요)
<button onClick={() => {
setA(1); // 자동 배치
setB(2); // 자동 배치
}}>클릭</button>
// 비동기 — 명시적 batch 필요
setTimeout(() => {
batch(() => {
setA(1);
setB(2);
});
}, 1000);
3. untrack — 추적 없이 Signal 읽기
무한 루프 방지
createEffect 안에서 시그널을 읽으면 의존성으로 등록됩니다. 그런데 어떤 시그널의 변화로 effect가 실행될 때, 내부에서 다른 시그널을 읽되 추적하고 싶지 않다면 untrack을 사용합니다.
import { createSignal, createEffect, untrack } from 'solid-js';
const [trigger, setTrigger] = createSignal(0);
const [value, setValue] = createSignal('초기값');
const [log, setLog] = createSignal([]);
// trigger가 바뀔 때만 실행, 이때 value를 읽지만 추적하지 않음
createEffect(() => {
const t = trigger(); // trigger 추적 O
const v = untrack(() => value()); // value 추적 X
console.log(`trigger: ${t}, value: ${v}`);
setLog(prev => [...prev, `${t}: ${v}`]);
// log를 setLog로 업데이트해도 effect 재실행 없음
});
초기화 로직에서 untrack
function DataFetcher(props) {
// props.url이 바뀔 때 fetch, 하지만 props.options는 최초값만 사용
createEffect(() => {
const url = props.url; // 추적 O
const options = untrack(() => props.options); // 추적 X
fetch(url, options).then(/* ... */);
});
}
4. on — 명시적 의존성 선언
on은 의존성을 명시적으로 선언해, 해당 시그널이 변할 때만 effect를 실행하고 내부의 다른 값은 추적하지 않습니다.
import { createSignal, createEffect, on } from 'solid-js';
const [source, setSource] = createSignal(0);
const [other, setOther] = createSignal('hello');
// on 없이 — source와 other 모두 추적
createEffect(() => {
console.log(source(), other());
});
// on 사용 — source만 추적, other는 추적 안 함
createEffect(on(source, (value, prevValue) => {
console.log(`변경: ${prevValue} → ${value}`);
console.log(other()); // 읽지만 추적 안 됨
}));
// 여러 시그널에 반응
createEffect(on([source, other], ([s, o]) => {
console.log(`source: ${s}, other: ${o}`);
}));
defer 옵션 — 초기 실행 방지
// 기본값: defer: false → 최초 실행 시에도 콜백 호출
// defer: true → 시그널이 실제로 변경될 때부터 실행
createEffect(on(source, (value) => {
console.log('변경됨:', value);
}, { defer: true })); // 첫 렌더링 시 실행 안 됨
5. 성능 최적화 패턴
컴포넌트 분리 — Solid.js의 핵심 철학
React는 컴포넌트 함수가 리렌더링 단위입니다. Solid.js는 컴포넌트 함수가 단 한 번만 실행되고, 변경은 DOM 직접 업데이트로 처리됩니다. 따라서 React처럼 "작은 컴포넌트 = 빠른 리렌더링"이라는 공식이 Solid.js에는 해당하지 않습니다.
하지만 컴포넌트를 분리하는 다른 이유가 있습니다.
// ❌ 비효율 — 하나의 큰 컴포넌트
function Dashboard() {
const [stats, setStats] = createSignal(null);
const [users, setUsers] = createSignal([]);
const [chart, setChart] = createSignal(null);
// 시그널 업데이트 시 JSX 전체가 재평가됨
return (
<div>
<StatsSection stats={stats()} />
<UsersTable users={users()} />
<ChartSection chart={chart()} />
</div>
);
}
// ✅ 효율적 — 독립 컴포넌트로 분리
// 각 컴포넌트의 시그널이 해당 컴포넌트의 DOM만 업데이트
function StatsWidget() {
const [stats, setStats] = createSignal(null);
return <StatsSection stats={stats()} />;
}
function UsersWidget() {
const [users, setUsers] = createSignal([]);
return <UsersTable users={users()} />;
}
lazy 로딩 — 코드 스플리팅
import { lazy, Suspense } from 'solid-js';
// 라우트 단위 코드 스플리팅
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
function App() {
return (
<Router>
<Route path="/" component={Home} />
<Route
path="/dashboard"
component={() => (
<Suspense fallback={<PageSkeleton />}>
<Dashboard />
</Suspense>
)}
/>
<Route
path="/settings"
component={() => (
<Suspense fallback={<PageSkeleton />}>
<Settings />
</Suspense>
)}
/>
</Router>
);
}
<Dynamic> 컴포넌트 — 동적 컴포넌트 렌더링
import { Dynamic } from 'solid-js/web';
import { createSignal } from 'solid-js';
const componentMap = {
text: TextWidget,
chart: ChartWidget,
table: TableWidget,
image: ImageWidget,
};
function WidgetRenderer(props) {
// if/switch 없이 동적으로 컴포넌트 교체
return (
<Dynamic
component={componentMap[props.type] ?? DefaultWidget}
data={props.data}
config={props.config}
/>
);
}
// 헤딩 레벨 동적 변경
function Heading(props) {
return (
<Dynamic component={`h${props.level}`} class={props.class}>
{props.children}
</Dynamic>
);
}
// <Heading level={2}>제목</Heading> → <h2>제목</h2>
createMemo 최적화
import { createSignal, createMemo } from 'solid-js';
function ProductFilter() {
const [products, setProducts] = createSignal([...largeArray]);
const [query, setQuery] = createSignal('');
const [sortBy, setSortBy] = createSignal('price');
const [category, setCategory] = createSignal('all');
// 파이프라인 메모화 — 각 단계가 필요할 때만 재실행
const filtered = createMemo(() =>
products().filter(p =>
category() === 'all' || p.category === category()
)
);
const searched = createMemo(() =>
filtered().filter(p =>
p.name.toLowerCase().includes(query().toLowerCase())
)
);
const sorted = createMemo(() =>
[...searched()].sort((a, b) => {
if (sortBy() === 'price') return a.price - b.price;
if (sortBy() === 'name') return a.name.localeCompare(b.name);
return 0;
})
);
return (
<div>
<input value={query()} onInput={e => setQuery(e.target.value)} />
<select onChange={e => setSortBy(e.target.value)}>
<option value="price">가격순</option>
<option value="name">이름순</option>
</select>
<p>결과: {sorted().length}개</p>
<For each={sorted()}>
{(product) => <ProductCard product={product} />}
</For>
</div>
);
}
<Portal> — DOM 외부에 렌더링
import { Portal } from 'solid-js/web';
import { createSignal, Show } from 'solid-js';
function Modal(props) {
return (
<Show when={props.isOpen}>
<Portal mount={document.body}>
<div class="modal-overlay" onClick={props.onClose}>
<div class="modal-content" onClick={e => e.stopPropagation()}>
{props.children}
</div>
</div>
</Portal>
</Show>
);
}
function ToastContainer() {
return (
<Portal mount={document.getElementById('toast-root')}>
<div class="toast-stack">
{/* 토스트 목록 */}
</div>
</Portal>
);
}
6. TypeScript와 Solid.js
컴포넌트 타입 정의
import { Component, JSX, ParentComponent, FlowComponent } from 'solid-js';
// 기본 컴포넌트
const Button: Component<{
onClick?: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary' | 'danger';
children: JSX.Element;
}> = (props) => {
return (
<button
onClick={props.onClick}
disabled={props.disabled}
class={`btn btn-${props.variant ?? 'primary'}`}
>
{props.children}
</button>
);
};
// children을 포함하는 컴포넌트 (ParentComponent)
const Card: ParentComponent<{ title: string; class?: string }> = (props) => {
return (
<div class={`card ${props.class ?? ''}`}>
<h2>{props.title}</h2>
{props.children}
</div>
);
};
// when/fallback이 있는 FlowComponent (Show 같은 패턴)
const Authenticated: FlowComponent<{ fallback?: JSX.Element }, JSX.Element> = (props) => {
const { isAuthenticated } = useAuth();
return (
<Show when={isAuthenticated()} fallback={props.fallback}>
{props.children}
</Show>
);
};
JSX 타입과 이벤트 핸들러
import { JSX } from 'solid-js';
// 이벤트 핸들러 타입
function InputField(props: {
onInput: JSX.EventHandler<HTMLInputElement, InputEvent>;
onKeyDown?: JSX.EventHandlerUnion<HTMLInputElement, KeyboardEvent>;
}) {
return (
<input
onInput={props.onInput}
onKeyDown={props.onKeyDown}
/>
);
}
// 사용
<InputField
onInput={(e) => {
// e.target은 자동으로 HTMLInputElement 타입
console.log(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') submitForm();
}}
/>
시그널과 스토어 타입
import { createSignal, createStore } from 'solid-js';
import { createStore as createSolidStore } from 'solid-js/store';
// 시그널 타입 명시 (유니온 타입, null 가능)
const [user, setUser] = createSignal<User | null>(null);
const [count, setCount] = createSignal<number>(0);
// 스토어 인터페이스 정의
interface AppState {
products: Product[];
cart: {
items: CartItem[];
coupon: string | null;
};
ui: {
sidebarOpen: boolean;
theme: 'light' | 'dark';
};
}
const [state, setState] = createSolidStore<AppState>({
products: [],
cart: { items: [], coupon: null },
ui: { sidebarOpen: false, theme: 'light' },
});
createContext 타입 안전성
import { createContext, useContext, Accessor, Setter } from 'solid-js';
interface ThemeContextValue {
theme: Accessor<'light' | 'dark'>;
setTheme: Setter<'light' | 'dark'>;
toggleTheme: () => void;
}
// undefined를 기본값으로 사용하고 훅에서 검증
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (ctx === undefined) {
throw new Error('useTheme은 ThemeProvider 안에서만 사용할 수 있습니다');
}
return ctx;
}
7. 테스팅 — solid-testing-library + vitest
설치 및 설정
npm install -D vitest @solidjs/testing-library @testing-library/jest-dom jsdom
// vite.config.ts
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
export default defineConfig({
plugins: [solidPlugin()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test-setup.ts'],
transformMode: { web: [/\.[jt]sx?$/] },
},
});
// src/test-setup.ts
import '@testing-library/jest-dom';
컴포넌트 테스트
// src/components/Counter.test.tsx
import { render, fireEvent, screen } from '@solidjs/testing-library';
import { describe, it, expect } from 'vitest';
import Counter from './Counter';
describe('Counter 컴포넌트', () => {
it('초기값 0으로 렌더링된다', () => {
render(() => <Counter />);
expect(screen.getByText('카운트: 0')).toBeInTheDocument();
});
it('증가 버튼 클릭 시 카운트가 1 증가한다', async () => {
render(() => <Counter />);
const button = screen.getByRole('button', { name: '증가' });
fireEvent.click(button);
expect(screen.getByText('카운트: 1')).toBeInTheDocument();
});
it('초기값 prop을 받아 렌더링된다', () => {
render(() => <Counter initialValue={5} />);
expect(screen.getByText('카운트: 5')).toBeInTheDocument();
});
it('최대값에 도달하면 버튼이 비활성화된다', () => {
render(() => <Counter initialValue={10} max={10} />);
const button = screen.getByRole('button', { name: '증가' });
expect(button).toBeDisabled();
});
});
Context가 있는 컴포넌트 테스트
// src/components/CartButton.test.tsx
import { render, fireEvent, screen } from '@solidjs/testing-library';
import { CartProvider } from '~/providers/CartProvider';
import CartButton from './CartButton';
function renderWithCart(ui: () => JSX.Element) {
return render(() => (
<CartProvider>
{ui()}
</CartProvider>
));
}
describe('CartButton', () => {
it('장바구니에 상품을 추가한다', () => {
renderWithCart(() => (
<CartButton product={{ id: 1, name: '노트북', price: 1000000 }} />
));
fireEvent.click(screen.getByRole('button', { name: '장바구니 추가' }));
expect(screen.getByText('1')).toBeInTheDocument(); // 배지
});
});
비동기 컴포넌트 테스트
import { render, screen, waitFor } from '@solidjs/testing-library';
import { vi } from 'vitest';
// API 모킹
vi.mock('~/lib/api', () => ({
fetchUsers: vi.fn().mockResolvedValue([
{ id: 1, name: '홍길동' },
{ id: 2, name: '김철수' },
]),
}));
describe('UserList', () => {
it('사용자 목록을 로드해서 표시한다', async () => {
render(() => <UserList />);
// 로딩 상태 확인
expect(screen.getByText('로딩 중...')).toBeInTheDocument();
// 데이터 로드 완료 대기
await waitFor(() => {
expect(screen.getByText('홍길동')).toBeInTheDocument();
expect(screen.getByText('김철수')).toBeInTheDocument();
});
});
});
8. React에서 Solid.js로 마이그레이션
핵심 차이점 대조표
| React | Solid.js | 주의 사항 |
|---|---|---|
useState | createSignal | signal은 getter 함수 |
useReducer | createStore + actions | setStore 경로 기반 |
useEffect | createEffect | 자동 의존성 추적 |
useMemo | createMemo | 마찬가지로 함수 반환 |
useCallback | 불필요 (함수 그대로 사용) | 재렌더링이 없으므로 불필요 |
useRef | createSignal 또는 let ref | DOM ref는 let 변수 |
useContext | useContext | 동일 |
React.memo | 불필요 | 컴포넌트가 한 번만 실행 |
key prop | key prop | 동일 (For 내부에서 사용) |
children | props.children | 구조 분해 금지 |
조건부 렌더링 && | <Show when={...}> | short-circuit 주의 |
리스트 렌더링 .map() | <For each={...}> | 인덱스는 함수: i() |
useLayoutEffect | createRenderEffect | DOM 업데이트 후 동기 실행 |
Suspense | <Suspense> | 동일 |
lazy | lazy | 동일 |
마이그레이션 코드 예시
// React 코드
import { useState, useEffect, useMemo } from 'react';
function ProductSearch({ categoryId }) {
const [query, setQuery] = useState('');
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!categoryId) return;
setLoading(true);
fetch(`/api/products?category=${categoryId}`)
.then(r => r.json())
.then(data => {
setProducts(data);
setLoading(false);
});
}, [categoryId]);
const filtered = useMemo(() =>
products.filter(p => p.name.toLowerCase().includes(query.toLowerCase())),
[products, query]
);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{loading ? <p>로딩 중...</p> : (
<ul>
{filtered.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)}
</div>
);
}
// Solid.js로 변환
import { createSignal, createEffect, createMemo, Show, For } from 'solid-js';
function ProductSearch(props) {
const [query, setQuery] = createSignal('');
const [products, setProducts] = createSignal([]);
const [loading, setLoading] = createSignal(false);
createEffect(() => {
if (!props.categoryId) return; // props. 접두사 유지 (구조 분해 금지)
setLoading(true);
fetch(`/api/products?category=${props.categoryId}`)
.then(r => r.json())
.then(data => {
setProducts(data);
setLoading(false);
});
});
const filtered = createMemo(() =>
products().filter(p => p.name.toLowerCase().includes(query().toLowerCase()))
);
return (
<div>
<input value={query()} onInput={e => setQuery(e.target.value)} />
<Show when={!loading()} fallback={<p>로딩 중...</p>}>
<ul>
<For each={filtered()}>
{(p) => <li>{p.name}</li>}
</For>
</ul>
</Show>
</div>
);
}
흔한 마이그레이션 실수
// ❌ React 스타일 — Solid에서 잘못된 패턴
function Wrong() {
const [items, setItems] = createSignal([1, 2, 3]);
return (
<ul>
{/* 짧은 회로 평가 — 숫자 0이 렌더링될 수 있음 */}
{items().length && <li>아이템 있음</li>}
{/* onChange 대신 onInput 사용해야 함 */}
<input onChange={e => console.log(e.target.value)} />
{/* JSX 내 구조 분해 */}
{items().map(({ id, name }) => <li key={id}>{name}</li>)}
</ul>
);
}
// ✅ Solid 스타일 — 올바른 패턴
function Correct() {
const [items, setItems] = createSignal([1, 2, 3]);
return (
<ul>
{/* Show 컴포넌트 사용 */}
<Show when={items().length > 0}>
<li>아이템 있음</li>
</Show>
{/* onInput 사용 */}
<input onInput={e => console.log(e.target.value)} />
{/* For 컴포넌트 + 구조 분해 금지 */}
<For each={items()}>
{(item) => <li>{item.name}</li>}
</For>
</ul>
);
}
9. Solid.js 에코시스템
@solidjs/router — 공식 라우터
npm install @solidjs/router
import { Router, Route, A, useNavigate, useParams, useLocation } from '@solidjs/router';
function App() {
return (
<Router>
<Route path="/" component={Home} />
<Route path="/users/:id" component={UserProfile} />
<Route path="*404" component={NotFound} />
</Router>
);
}
@solidjs/meta — Head 메타 태그 관리
npm install @solidjs/meta
import { Title, Meta, Link } from '@solidjs/meta';
function BlogPost(props) {
return (
<>
<Title>{props.post.title} | My Blog</Title>
<Meta name="description" content={props.post.excerpt} />
<Meta property="og:title" content={props.post.title} />
<Meta property="og:image" content={props.post.thumbnail} />
<Link rel="canonical" href={`https://myblog.com/blog/${props.post.slug}`} />
<article>{/* 본문 */}</article>
</>
);
}
solid-primitives — 유틸리티 훅 모음
npm install @solid-primitives/utils @solid-primitives/storage @solid-primitives/timer
import { createLocalStorage } from '@solid-primitives/storage';
import { createInterval } from '@solid-primitives/timer';
import { createEventListener } from '@solid-primitives/event-listener';
// localStorage와 자동 동기화되는 시그널
const [theme, setTheme] = createLocalStorage('theme', 'light');
// 컴포넌트 — 1초마다 시간 업데이트
function Clock() {
const [time, setTime] = createSignal(new Date());
createInterval(() => setTime(new Date()), 1000);
return <p>{time().toLocaleTimeString()}</p>;
}
// 이벤트 리스너 자동 정리
function KeyboardShortcuts() {
createEventListener(window, 'keydown', (e) => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveDocument();
}
});
return null;
}
10. 최고급 패턴 — 고수 팁
팁 1: 시그널을 props로 전달 — 세밀한 제어
// 시그널 자체(getter 함수)를 props로 전달하면
// 부모의 특정 시그널이 변할 때만 자식의 해당 부분이 업데이트됨
function Parent() {
const [name, setName] = createSignal('홍길동');
const [score, setScore] = createSignal(100);
// name 시그널 자체를 전달 (호출하지 않음)
return <Child name={name} score={score} />;
}
function Child(props) {
// props.name은 Accessor<string> — 반응형 유지
return (
<div>
<p>{props.name()}</p>
<p>{props.score()}</p>
</div>
);
}
팁 2: createResource — 비동기 데이터의 Suspense 통합
import { createResource, Suspense, ErrorBoundary } from 'solid-js';
function UserProfile(props) {
const [user] = createResource(
() => props.userId, // source — userId가 바뀌면 재실행
async (userId) => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('사용자를 찾을 수 없습니다');
return res.json();
}
);
return (
// ErrorBoundary + Suspense 조합으로 선언적 처리
<ErrorBoundary fallback={(err) => <p>오류: {err.message}</p>}>
<Suspense fallback={<Skeleton />}>
<div>
<h1>{user().name}</h1>
<p>{user().email}</p>
</div>
</Suspense>
</ErrorBoundary>
);
}
팁 3: 커스텀 디렉티브
// 커스텀 디렉티브 정의 — 재사용 가능한 DOM 조작
function clickOutside(el, accessor) {
const handler = (e) => {
if (!el.contains(e.target)) accessor()?.();
};
document.addEventListener('click', handler);
onCleanup(() => document.removeEventListener('click', handler));
}
// TypeScript에서는 선언 필요
declare module 'solid-js' {
namespace JSX {
interface Directives {
clickOutside: () => void;
}
}
}
// 사용
function Dropdown() {
const [open, setOpen] = createSignal(false);
return (
<div use:clickOutside={() => setOpen(false)}>
<button onClick={() => setOpen(v => !v)}>메뉴</button>
<Show when={open()}>
<ul>{/* 메뉴 아이템 */}</ul>
</Show>
</div>
);
}
팁 4: 소유자 기반 생명주기 — runWithOwner
import { createRoot, getOwner, runWithOwner } from 'solid-js';
// 컴포넌트 외부에서 반응형 컨텍스트 실행 (고급 패턴)
function setupExternalReactivity() {
let dispose;
createRoot((d) => {
dispose = d;
const [count, setCount] = createSignal(0);
createEffect(() => console.log('외부 effect:', count()));
setInterval(() => setCount(c => c + 1), 1000);
});
// 나중에 수동 정리
return () => dispose();
}
// 이벤트 콜백에서 현재 소유자 캡처 후 사용
function withOwnerExample() {
const owner = getOwner();
someExternalEventEmitter.on('event', (data) => {
runWithOwner(owner, () => {
// 여기서 createSignal, createEffect 등 사용 가능
const [result] = createSignal(data);
});
});
}
팁 5: 세밀한 For 최적화 — key 없이도 OK
// Solid.js의 <For>는 객체 참조 기반으로 비교
// 배열 아이템이 새 객체이면 리렌더링, 같은 참조이면 재사용
// reconcile과 함께 사용하면 서버 응답도 최소 업데이트
import { reconcile } from 'solid-js/store';
const updateItems = (newItems) => {
setStore('items', reconcile(newItems));
// 변경된 필드만 업데이트, 기존 DOM 재사용
};
팁 6: 에러 처리와 onError
import { onError } from 'solid-js';
function RobustComponent() {
// 이 컴포넌트와 자식에서 발생한 에러를 캐치
onError((err) => {
console.error('컴포넌트 에러:', err);
reportToSentry(err);
});
return <RiskyChild />;
}
팁 7: 선택적 체이닝과 nullish 처리
// createResource의 loading 상태를 활용한 안전한 접근
function UserDetails() {
const [user] = createResource(fetchCurrentUser);
// user.loading, user.error, user() 모두 활용
return (
<div>
<Switch>
<Match when={user.loading}>
<Skeleton />
</Match>
<Match when={user.error}>
<ErrorMessage error={user.error} />
</Match>
<Match when={user()}>
{(u) => (
<div>
<h1>{u().name}</h1>
<p>{u().email}</p>
</div>
)}
</Match>
</Switch>
</div>
);
}
종합 정리
Solid.js 핵심 원칙 요약
| 원칙 | 설명 |
|---|---|
| 구조 분해 금지 | props와 store는 항상 경로로 접근, 반응성 유지 |
| 함수 호출 위치 | 시그널은 반응형 컨텍스트 안에서만 호출 |
| 컴포넌트 = 1회 실행 | 초기화 코드만, 리렌더링 없음 |
| batch | 비동기 다중 업데이트 시 명시적 일괄 처리 |
| untrack/on | 불필요한 의존성 추적 제거 |
| 세밀한 반응성 | 필요한 부분만 구독, createMemo로 파생 최적화 |
| 선언적 흐름 | Show/For/Switch/Suspense 활용 |
에코시스템 핵심 패키지
| 패키지 | 역할 |
|---|---|
@solidjs/router | SPA 라우팅 |
@solidjs/meta | <head> 메타 태그 |
@solidjs/start | SolidStart 풀스택 프레임워크 |
solid-primitives | 공식 유틸리티 훅 모음 |
@tanstack/solid-query | 서버 상태 관리 |
solid-transition-group | 애니메이션 트랜지션 |
@solidjs/testing-library | 컴포넌트 테스팅 |
React 개발자를 위한 최종 체크리스트
-
useState→createSignal(값은count(), setter는setCount) -
useEffect→createEffect(의존성 배열 불필요, 자동 추적) -
useMemo→createMemo(반환값도 함수:memo()) -
useCallback→ 일반 함수 (필요 없음) - props 구조 분해 →
props.name직접 접근 -
&&조건 렌더링 →<Show> -
.map()리스트 →<For> -
onChange→onInput(input 실시간) -
React.memo→ 불필요 -
keyprop →<For>자동 처리
Ch18 마무리
이 챕터에서는 Solid.js의 핵심부터 실전까지를 다루었습니다.
- 18.1 Solid.js 소개 — 세밀한 반응성, React와의 차이
- 18.2 개발 환경 설정 — Vite 프로젝트 구성
- 18.3 반응성 시스템 — createSignal, createEffect, createMemo
- 18.4 제어 흐름 — Show, For, Switch, Suspense
- 18.5 스토어와 상태 공유 — createStore, Context, 전역 상태
- 18.6 SolidStart 기초 — 풀스택 개발, SSR/SSG, 서버 함수
- 18.7 실전 고수 팁 — 안티패턴 회피, 성능, TypeScript, 테스팅, 마이그레이션
Solid.js는 "반응성을 진지하게 이해하면 이해할수록 강력해지는" 프레임워크입니다. React의 친숙한 JSX 문법을 유지하면서도 훨씬 예측 가능하고 성능 좋은 코드를 작성할 수 있습니다. 여기서 배운 패턴들을 실제 프로젝트에 적용하며 깊이 익혀 나가시길 바랍니다.