본문으로 건너뛰기
Advertisement

18.4 제어 흐름과 Props

Solid.js는 조건부 렌더링과 리스트 렌더링을 위해 내장 컴포넌트를 제공합니다. JavaScript의 if, map, switch 대신 Show, For, Index, Switch/Match를 사용합니다. 이 컴포넌트들은 세분화된 반응성 시스템과 긴밀하게 통합되어 최적의 DOM 업데이트를 보장합니다.


왜 내장 제어 흐름 컴포넌트를 사용하는가?

JavaScript 표현식의 문제점

// ❌ 삼항 연산자 — 조건이 바뀔 때마다 두 브랜치를 모두 평가
function BadComponent() {
const [show, setShow] = createSignal(true);

// show()가 false여도 <HeavyComponent />의 JSX가 평가됨
return (
<div>
{show() ? <HeavyComponent /> : <EmptyState />}
</div>
);
}

// ✅ Show — 조건이 true일 때만 children 평가 (lazy evaluation)
function GoodComponent() {
const [show, setShow] = createSignal(true);

return (
<div>
<Show when={show()} fallback={<EmptyState />}>
<HeavyComponent />
</Show>
</div>
);
}

Solid.js의 JSX는 컴포넌트 함수가 한 번만 실행되기 때문에, 삼항 연산자를 써도 실제 컴포넌트 재실행은 일어나지 않습니다. 그러나 Show를 쓰면 children 자체의 생성/파괴를 명확히 제어할 수 있습니다.


<Show> — 조건부 렌더링

기본 사용법

import { Show } from 'solid-js';
import { createSignal } from 'solid-js';

function LoginStatus() {
const [isLoggedIn, setIsLoggedIn] = createSignal(false);
const [username, setUsername] = createSignal('홍길동');

return (
<div>
<Show
when={isLoggedIn()}
fallback={<button onClick={() => setIsLoggedIn(true)}>로그인</button>}
>
{/* when이 truthy일 때만 렌더링 */}
<div>
<span>안녕하세요, {username()}!</span>
<button onClick={() => setIsLoggedIn(false)}>로그아웃</button>
</div>
</Show>
</div>
);
}

render prop 패턴으로 타입 좁히기

Show의 children을 함수로 전달하면, when 속성의 타입이 좁혀진 값을 받을 수 있습니다.

interface User {
name: string;
email: string;
role: 'admin' | 'user';
}

function UserCard() {
const [user, setUser] = createSignal<User | null>(null);

return (
<Show
when={user()}
fallback={<p>사용자 정보 없음</p>}
>
{/* (u) => ... 형태: u는 User 타입 (null이 제거됨) */}
{(u) => (
<div class="user-card">
<h2>{u().name}</h2>
<p>{u().email}</p>
<Show when={u().role === 'admin'}>
<span class="badge">관리자</span>
</Show>
</div>
)}
</Show>
);
}

when 속성 상세

// 숫자 (0은 falsy)
<Show when={count()}> ... </Show>

// 문자열 (빈 문자열은 falsy)
<Show when={errorMessage()}> ... </Show>

// 객체/배열 (null은 falsy)
<Show when={data()}> ... </Show>

// 복합 조건
<Show when={isAdmin() && hasPermission('write')}> ... </Show>

<For> — 리스트 렌더링

For는 배열을 렌더링하는 표준 방법입니다. 각 항목은 참조(reference)로 추적됩니다. 즉, 배열에서 항목이 이동하면 컴포넌트를 재생성하지 않고 DOM 노드를 이동시킵니다.

기본 사용법

import { For } from 'solid-js';
import { createSignal } from 'solid-js';

interface Todo {
id: number;
text: string;
done: boolean;
}

function TodoList() {
const [todos, setTodos] = createSignal<Todo[]>([
{ id: 1, text: '운동하기', done: false },
{ id: 2, text: '독서하기', done: true },
{ id: 3, text: '코딩하기', done: false },
]);

const toggleTodo = (id: number) => {
setTodos(prev =>
prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
);
};

return (
<ul>
{/* each: 반복할 배열 Signal */}
{/* fallback: 빈 배열일 때 표시할 컨텐츠 */}
<For
each={todos()}
fallback={<li>할 일이 없습니다.</li>}
>
{/* 첫 번째 인자: 현재 항목 (Signal), 두 번째: 인덱스 (Signal) */}
{(todo, index) => (
<li style={todo.done ? 'text-decoration: line-through' : ''}>
<span style="color: gray; margin-right: 0.5rem;">{index() + 1}.</span>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
)}
</For>
</ul>
);
}

For의 최적화 원리

배열 변경 전: [A, B, C, D, E]
배열 변경 후: [A, C, B, D, E] (B와 C 순서 교환)

React (map + key): B, C DOM 노드 파괴 → 재생성
Solid (For): B, C DOM 노드를 이동만 함 → 훨씬 빠름

정렬, 필터링과 함께 사용

function SortableList() {
const [items, setItems] = createSignal([
{ id: 1, name: '바나나', price: 1500 },
{ id: 2, name: '사과', price: 2000 },
{ id: 3, name: '딸기', price: 5000 },
{ id: 4, name: '포도', price: 3000 },
]);
const [sortBy, setSortBy] = createSignal<'name' | 'price'>('name');
const [ascending, setAscending] = createSignal(true);

const sorted = createMemo(() => {
const key = sortBy();
const asc = ascending();
return [...items()].sort((a, b) => {
const diff = a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0;
return asc ? diff : -diff;
});
});

return (
<div>
<div style="margin-bottom: 1rem;">
정렬:
<button onClick={() => setSortBy('name')}>이름</button>
<button onClick={() => setSortBy('price')}>가격</button>
<button onClick={() => setAscending(a => !a)}>
{ascending() ? '오름차순' : '내림차순'}
</button>
</div>

<ul>
<For each={sorted()}>
{(item) => (
<li>{item.name}{item.price.toLocaleString()}</li>
)}
</For>
</ul>
</div>
);
}

<Index> — 인덱스 기반 렌더링

IndexFor와 달리 인덱스(위치)를 안정적으로 유지합니다. 배열 항목이 변경되면 해당 인덱스의 컴포넌트를 업데이트합니다.

For vs Index 차이점

항목<For><Index>
추적 기준항목의 참조(identity)배열의 인덱스(위치)
항목 인자 타입값 (불변)Signal (getter)
인덱스 인자 타입Signal (getter)값 (불변)
적합한 경우객체 배열 (key 있음)원시값 배열 또는 빈번한 위치 변경
항목 변경 시DOM 이동해당 위치 인플레이스 업데이트
import { Index } from 'solid-js';
import { createSignal } from 'solid-js';

function IndexExample() {
const [items, setItems] = createSignal(['사과', '바나나', '체리']);

const updateItem = (idx: number, value: string) => {
setItems(prev => prev.map((item, i) => i === idx ? value : item));
};

return (
<div>
{/* Index: item은 Signal, index는 일반 숫자 */}
<Index each={items()}>
{(item, index) => (
<div>
<span>#{index}: </span>
{/* item()로 호출해야 값을 읽을 수 있음 */}
<input
value={item()}
onInput={(e) => updateItem(index, e.target.value)}
/>
</div>
)}
</Index>
</div>
);
}

For vs Index 선택 기준

// ✅ 객체 배열 → For 사용 (id로 참조 추적)
<For each={users()}>
{(user) => <UserCard name={user.name} email={user.email} />}
</For>

// ✅ 원시값 배열 + 인플레이스 편집 → Index 사용
<Index each={tableRows()}>
{(row, rowIndex) => (
<Index each={row()}>
{(cell, colIndex) => (
<td onInput={(e) => updateCell(rowIndex, colIndex, e.target.value)}>
{cell()}
</td>
)}
</Index>
)}
</Index>

// ✅ 자주 변경되는 슬라이딩 윈도우 → Index 사용
// 예: 채팅 메시지 목록 (위치별로 업데이트)

<Switch> + <Match> — 다중 조건 처리

여러 조건 중 하나를 선택하는 경우 SwitchMatch를 사용합니다.

기본 사용법

import { Switch, Match } from 'solid-js';

type Status = 'loading' | 'error' | 'empty' | 'success';

function DataView() {
const [status, setStatus] = createSignal<Status>('loading');
const [data, setData] = createSignal<string[]>([]);
const [error, setError] = createSignal('');

return (
<div>
{/* Switch: 첫 번째로 when이 truthy인 Match만 렌더링 */}
<Switch fallback={<p>알 수 없는 상태입니다.</p>}>
<Match when={status() === 'loading'}>
<LoadingSpinner />
</Match>
<Match when={status() === 'error'}>
<ErrorMessage message={error()} />
</Match>
<Match when={status() === 'empty'}>
<EmptyState message="데이터가 없습니다." />
</Match>
<Match when={status() === 'success'}>
<DataList items={data()} />
</Match>
</Switch>

{/* 테스트 버튼 */}
<div style="margin-top: 1rem;">
<button onClick={() => setStatus('loading')}>로딩</button>
<button onClick={() => { setStatus('error'); setError('네트워크 오류'); }}>에러</button>
<button onClick={() => setStatus('empty')}>비어있음</button>
<button onClick={() => { setStatus('success'); setData(['항목1', '항목2']); }}>성공</button>
</div>
</div>
);
}

render prop으로 타입 좁히기

type Result =
| { type: 'ok'; value: string }
| { type: 'err'; message: string };

function ResultView() {
const [result, setResult] = createSignal<Result>({ type: 'ok', value: '성공!' });

return (
<Switch>
<Match when={result().type === 'ok' && result()}>
{(r) => <p style="color: green">성공: {(r() as { type: 'ok'; value: string }).value}</p>}
</Match>
<Match when={result().type === 'err' && result()}>
{(r) => <p style="color: red">에러: {(r() as { type: 'err'; message: string }).message}</p>}
</Match>
</Switch>
);
}

<ErrorBoundary> — 에러 처리

자식 컴포넌트에서 발생하는 JavaScript 에러를 잡아 fallback UI를 표시합니다.

기본 사용법

import { ErrorBoundary } from 'solid-js';

function BrokenComponent() {
throw new Error('예상치 못한 에러 발생!');
return <div>이 코드는 실행되지 않음</div>;
}

function App() {
return (
<ErrorBoundary
fallback={(err, reset) => (
<div style="border: 2px solid red; padding: 1rem; border-radius: 8px;">
<h3>오류가 발생했습니다</h3>
<p>{err.message}</p>
<button onClick={reset}>다시 시도</button>
</div>
)}
>
<BrokenComponent />
</ErrorBoundary>
);
}

중첩 ErrorBoundary

function Dashboard() {
return (
{/* 전체 앱 수준 에러 경계 */}
<ErrorBoundary fallback={(err) => <CriticalError error={err} />}>
<Header />

{/* 위젯별 독립적인 에러 경계 */}
<div class="widgets">
<ErrorBoundary fallback={<WidgetError name="날씨" />}>
<WeatherWidget />
</ErrorBoundary>

<ErrorBoundary fallback={<WidgetError name="주식" />}>
<StockWidget />
</ErrorBoundary>

<ErrorBoundary fallback={<WidgetError name="뉴스" />}>
<NewsWidget />
</ErrorBoundary>
</div>
</ErrorBoundary>
);
}

<Suspense> — 비동기 로딩 처리

Suspense는 자식 컴포넌트의 createResource가 로딩 중일 때 fallback을 표시합니다.

기본 사용법

import { Suspense, createResource } from 'solid-js';

async function fetchData() {
await new Promise(r => setTimeout(r, 1500)); // 지연 시뮬레이션
return { name: '홍길동', email: 'hong@example.com' };
}

function UserInfo() {
const [user] = createResource(fetchData);
// Suspense 경계 내에서 데이터 로딩 중이면 자동으로 Suspense fallback 표시
return <div>{user()?.name}</div>;
}

function App() {
return (
<Suspense fallback={<div class="spinner">로딩 중...</div>}>
<UserInfo />
</Suspense>
);
}

SuspenseList로 여러 로딩 조율

import { SuspenseList, Suspense } from 'solid-js';

function Dashboard() {
return (
{/* revealOrder: 'forwards' | 'backwards' | 'together' */}
<SuspenseList revealOrder="forwards" tail="collapsed">
<Suspense fallback={<Skeleton />}>
<UserCard />
</Suspense>
<Suspense fallback={<Skeleton />}>
<RecentOrders />
</Suspense>
<Suspense fallback={<Skeleton />}>
<Statistics />
</Suspense>
</SuspenseList>
);
}

Props 반응성 유지 패턴

Solid.js에서 Props는 읽기 전용 반응형 객체입니다. 컴포넌트가 한 번만 실행되기 때문에, props의 최신 값에 접근하려면 반드시 함수처럼 접근해야 합니다.

props는 함수처럼 동작한다

// ✅ props는 항상 최신값을 반환하는 Proxy 객체
function Greeting(props) {
// props.name은 항상 부모에서 전달되는 최신 name을 반환
return <h1>안녕하세요, {props.name}!</h1>;
}

// 부모 컴포넌트
function Parent() {
const [name, setName] = createSignal('홍길동');
return (
<div>
<Greeting name={name()} />
<button onClick={() => setName('김철수')}>이름 변경</button>
</div>
);
}

구조 분해 절대 금지

// ❌ props 구조 분해 — 최초 값으로 고정됨!
function BadGreeting({ name, age }) {
// name = '홍길동', age = 30으로 영구 고정
// 부모가 name을 '김철수'로 바꿔도 업데이트되지 않음
return <div>{name} ({age}세)</div>;
}

// ✅ 항상 props 객체 전체를 받아서 접근
function GoodGreeting(props) {
return <div>{props.name} ({props.age}세)</div>;
}

mergeProps — 기본값 설정

mergeProps는 props에 기본값을 반응형으로 병합합니다.

import { mergeProps } from 'solid-js';

interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
}

function Button(props: ButtonProps) {
// 기본값 설정 — 반응형 유지됨
const merged = mergeProps(
{ variant: 'primary', size: 'md', disabled: false },
props
);

return (
<button
class={`btn btn-${merged.variant} btn-${merged.size}`}
disabled={merged.disabled}
onClick={merged.onClick}
>
{merged.label}
</button>
);
}

// 사용
<Button label="저장" />
<Button label="삭제" variant="danger" size="lg" />

splitProps — props 분리

자식 컴포넌트나 DOM 요소로 일부 props만 전달할 때 splitProps를 사용합니다.

import { splitProps } from 'solid-js';

interface InputProps {
label: string;
error?: string;
// + 모든 HTML input 속성
[key: string]: any;
}

function LabeledInput(props: InputProps) {
// 컴포넌트 전용 props와 input 요소용 props 분리
const [local, inputProps] = splitProps(props, ['label', 'error']);

return (
<div class="form-group">
<label>{local.label}</label>
{/* inputProps에는 label, error를 제외한 모든 props가 들어있음 */}
<input {...inputProps} class={local.error ? 'input-error' : ''} />
{local.error && <span class="error-msg">{local.error}</span>}
</div>
);
}

// 사용
<LabeledInput
label="이메일"
type="email"
value={email()}
onInput={(e) => setEmail(e.target.value)}
error={emailError()}
placeholder="example@email.com"
/>

실전 예제: 대시보드

import {
createSignal, createResource, createMemo,
For, Show, Switch, Match, Suspense, ErrorBoundary
} from 'solid-js';

// 타입 정의
interface Stat {
label: string;
value: number;
change: number;
unit: string;
}

interface Order {
id: number;
customer: string;
amount: number;
status: 'pending' | 'processing' | 'done' | 'cancelled';
date: string;
}

// 가상 API 함수
async function fetchStats(): Promise<Stat[]> {
await new Promise(r => setTimeout(r, 800));
return [
{ label: '오늘 매출', value: 4250000, change: 12.5, unit: '원' },
{ label: '신규 주문', value: 48, change: -3.2, unit: '건' },
{ label: '활성 사용자', value: 1284, change: 8.1, unit: '명' },
{ label: '전환율', value: 3.8, change: 0.5, unit: '%' },
];
}

async function fetchOrders(): Promise<Order[]> {
await new Promise(r => setTimeout(r, 1200));
return [
{ id: 1001, customer: '홍길동', amount: 150000, status: 'done', date: '2026-03-21' },
{ id: 1002, customer: '김철수', amount: 89000, status: 'processing', date: '2026-03-21' },
{ id: 1003, customer: '이영희', amount: 230000, status: 'pending', date: '2026-03-21' },
{ id: 1004, customer: '박민준', amount: 45000, status: 'cancelled', date: '2026-03-20' },
{ id: 1005, customer: '최지원', amount: 178000, status: 'done', date: '2026-03-20' },
];
}

// 통계 카드 컴포넌트
function StatCard(props: { stat: Stat }) {
const isPositive = () => props.stat.change >= 0;

return (
<div class="stat-card">
<p class="stat-label">{props.stat.label}</p>
<p class="stat-value">
{props.stat.value.toLocaleString()}{props.stat.unit}
</p>
<p class={isPositive() ? 'change positive' : 'change negative'}>
{isPositive() ? '▲' : '▼'} {Math.abs(props.stat.change)}%
</p>
</div>
);
}

// 주문 상태 배지
function StatusBadge(props: { status: Order['status'] }) {
const config = () => ({
pending: { label: '대기중', color: '#ffa726' },
processing: { label: '처리중', color: '#42a5f5' },
done: { label: '완료', color: '#66bb6a' },
cancelled: { label: '취소', color: '#ef5350' },
}[props.status]);

return (
<span style={`
background: ${config().color}20;
color: ${config().color};
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8rem;
`}>
{config().label}
</span>
);
}

// 메인 대시보드
function Dashboard() {
const [filterStatus, setFilterStatus] = createSignal<Order['status'] | 'all'>('all');
const [stats] = createResource(fetchStats);
const [orders] = createResource(fetchOrders);

const filteredOrders = createMemo(() => {
const f = filterStatus();
if (f === 'all') return orders() ?? [];
return (orders() ?? []).filter(o => o.status === f);
});

const totalAmount = createMemo(() =>
filteredOrders().reduce((sum, o) => sum + o.amount, 0)
);

return (
<div class="dashboard">
<header class="dashboard-header">
<h1>대시보드</h1>
<p>2026년 3월 21일</p>
</header>

{/* 통계 카드 영역 */}
<section class="stats-section">
<ErrorBoundary fallback={(err) => <p>통계 로드 실패: {err.message}</p>}>
<Suspense fallback={
<div class="stats-grid">
{[1,2,3,4].map(() => <div class="stat-card skeleton" />)}
</div>
}>
<div class="stats-grid">
<For each={stats()}>
{(stat) => <StatCard stat={stat} />}
</For>
</div>
</Suspense>
</ErrorBoundary>
</section>

{/* 주문 목록 */}
<section class="orders-section">
<div class="section-header">
<h2>주문 목록</h2>
<div class="filter-buttons">
<For each={(['all', 'pending', 'processing', 'done', 'cancelled'] as const)}>
{(status) => (
<button
class={filterStatus() === status ? 'filter-btn active' : 'filter-btn'}
onClick={() => setFilterStatus(status)}
>
{status === 'all' ? '전체' :
status === 'pending' ? '대기' :
status === 'processing' ? '처리중' :
status === 'done' ? '완료' : '취소'}
</button>
)}
</For>
</div>
</div>

<ErrorBoundary fallback={(err) => <p>주문 로드 실패: {err.message}</p>}>
<Suspense fallback={<p>주문 목록 로딩 중...</p>}>
<Show
when={filteredOrders().length > 0}
fallback={<p class="empty">해당 상태의 주문이 없습니다.</p>}
>
<table class="orders-table">
<thead>
<tr>
<th>주문번호</th>
<th>고객</th>
<th>금액</th>
<th>상태</th>
<th>날짜</th>
</tr>
</thead>
<tbody>
<For each={filteredOrders()}>
{(order) => (
<tr>
<td>#{order.id}</td>
<td>{order.customer}</td>
<td>{order.amount.toLocaleString()}</td>
<td><StatusBadge status={order.status} /></td>
<td>{order.date}</td>
</tr>
)}
</For>
</tbody>
<tfoot>
<tr>
<td colSpan={2}><strong>합계</strong></td>
<td><strong>{totalAmount().toLocaleString()}</strong></td>
<td colSpan={2} />
</tr>
</tfoot>
</table>
</Show>
</Suspense>
</ErrorBoundary>
</section>
</div>
);
}

export default Dashboard;

실전 예제: 동적 목록 — 드래그 앤 드롭

import { createSignal, For } from 'solid-js';

interface Item {
id: number;
text: string;
color: string;
}

function DragDropList() {
const [items, setItems] = createSignal<Item[]>([
{ id: 1, text: '첫 번째 항목', color: '#ffcdd2' },
{ id: 2, text: '두 번째 항목', color: '#c8e6c9' },
{ id: 3, text: '세 번째 항목', color: '#bbdefb' },
{ id: 4, text: '네 번째 항목', color: '#fff9c4' },
{ id: 5, text: '다섯 번째 항목', color: '#f3e5f5' },
]);

let dragIndex = -1;

const handleDragStart = (index: number) => {
dragIndex = index;
};

const handleDragOver = (e: DragEvent, index: number) => {
e.preventDefault();
if (dragIndex === index) return;

setItems(prev => {
const next = [...prev];
const [dragged] = next.splice(dragIndex, 1);
next.splice(index, 0, dragged);
dragIndex = index;
return next;
});
};

const handleDragEnd = () => {
dragIndex = -1;
};

return (
<div>
<h3>드래그하여 순서 변경</h3>
{/* For는 참조 기반 추적으로 DOM 이동 최적화 */}
<ul style="list-style: none; padding: 0; max-width: 300px;">
<For each={items()}>
{(item, index) => (
<li
draggable={true}
onDragStart={() => handleDragStart(index())}
onDragOver={(e) => handleDragOver(e, index())}
onDragEnd={handleDragEnd}
style={`
background: ${item.color};
padding: 1rem;
margin-bottom: 0.5rem;
border-radius: 8px;
cursor: grab;
user-select: none;
border: 2px solid transparent;
transition: border-color 0.2s;
`}
>
{item.text}
</li>
)}
</For>
</ul>
</div>
);
}

export default DragDropList;

실전 예제: 에러 처리 패턴

import { createSignal, createResource, ErrorBoundary, Suspense } from 'solid-js';

// 에러를 발생시킬 수 있는 API
async function fetchRiskyData(id: number) {
const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}: 요청 실패`);
const data = await res.json();
if (data.id > 10) throw new Error('ID 10 이하만 조회 가능합니다.');
return data;
}

function RiskyDataView() {
const [id, setId] = createSignal(1);
const [data, { refetch }] = createResource(id, fetchRiskyData);

return (
<div>
<div>
<input
type="number"
value={id()}
onInput={(e) => setId(Number(e.target.value))}
min="1"
max="20"
/>
<button onClick={refetch}>새로고침</button>
</div>

<ErrorBoundary
fallback={(err, reset) => (
<div style="background: #fff3f3; border: 1px solid #f44336; padding: 1rem; border-radius: 8px; margin-top: 1rem;">
<h4 style="color: #f44336; margin: 0 0 0.5rem;">오류 발생</h4>
<p style="margin: 0 0 1rem;">{err.message}</p>
<div style="display: flex; gap: 0.5rem;">
<button onClick={reset}>다시 시도</button>
<button onClick={() => { setId(1); reset(); }}>기본값으로</button>
</div>
</div>
)}
>
<Suspense fallback={<p>데이터 로딩 중...</p>}>
<Show when={data()}>
<div style="margin-top: 1rem; padding: 1rem; border: 1px solid #ccc; border-radius: 8px;">
<p><strong>ID:</strong> {data()?.id}</p>
<p><strong>제목:</strong> {data()?.title}</p>
<p><strong>완료:</strong> {data()?.completed ? '✅' : '❌'}</p>
</div>
</Show>
</Suspense>
</ErrorBoundary>
</div>
);
}

export default RiskyDataView;

고수 팁

팁 1: children 헬퍼로 children 반응성 처리

import { children, type JSX } from 'solid-js';

interface AnimatedListProps {
children: JSX.Element | JSX.Element[];
}

function AnimatedList(props: AnimatedListProps) {
// children()을 memo처럼 처리 — 반응형 children을 안전하게 배열로 변환
const resolved = children(() => props.children);

// resolved()는 항상 최신 자식 요소 배열
return (
<ul>
<For each={resolved.toArray()}>
{(child) => (
<li style="animation: fadeIn 0.3s ease;">
{child}
</li>
)}
</For>
</ul>
);
}

팁 2: 중첩 Show의 성능 최적화

// ❌ 중첩이 많으면 가독성 저하
<Show when={isLoggedIn()}>
<Show when={isAdmin()}>
<Show when={hasPermission()}>
<AdminPanel />
</Show>
</Show>
</Show>

// ✅ createMemo로 복합 조건 만들기
const canShowAdmin = createMemo(
() => isLoggedIn() && isAdmin() && hasPermission()
);

<Show when={canShowAdmin()}>
<AdminPanel />
</Show>

팁 3: For 내부 컴포넌트 최적화

// For 내부의 각 항목은 독립적인 reactive scope를 가짐
// 한 항목이 변경되어도 다른 항목은 영향받지 않음
<For each={items()}>
{(item) => (
// ✅ item은 불변값 — createSignal 불필요
// item 객체가 교체되면 이 전체 블록이 재실행됨
<ExpensiveItemComponent
id={item.id}
data={item.data}
/>
)}
</For>

팁 4: 빈 배열 처리 패턴

// For의 fallback 대신 Show + For 조합
const hasItems = createMemo(() => items().length > 0);

<Show when={hasItems()} fallback={<EmptyState />}>
<For each={items()}>
{(item) => <Item data={item} />}
</For>
</Show>

// 또는 For의 fallback 직접 사용 (더 간결)
<For each={items()} fallback={<EmptyState />}>
{(item) => <Item data={item} />}
</For>

팁 5: Suspense 경계 최적화

// ❌ 전체를 하나의 Suspense로 감싸면 모두 로딩될 때까지 기다림
<Suspense fallback={<Loading />}>
<SlowComponent1 /> {/* 3초 */}
<SlowComponent2 /> {/* 1초 */}
<SlowComponent3 /> {/* 2초 */}
{/* 3초 후에 전체가 한 번에 나타남 */}
</Suspense>

// ✅ 개별 Suspense로 감싸면 각자 완료되는 대로 표시
<SlowComponent1 /> {/* Suspense 없이 즉시 표시 (if pre-loaded) */}

<Suspense fallback={<Skeleton />}><SlowComponent2 /></Suspense> {/* 1초 후 */}
<Suspense fallback={<Skeleton />}><SlowComponent1 /></Suspense> {/* 3초 후 */}
<Suspense fallback={<Skeleton />}><SlowComponent3 /></Suspense> {/* 2초 후 */}

팁 6: splitProps를 활용한 고차 컴포넌트 패턴

import { splitProps, mergeProps } from 'solid-js';
import type { JSX } from 'solid-js';

// HTML 속성을 그대로 전달하는 래퍼 컴포넌트
interface CardProps extends JSX.HTMLAttributes<HTMLDivElement> {
title: string;
hoverable?: boolean;
}

function Card(props: CardProps) {
const [local, rest] = splitProps(
mergeProps({ hoverable: false }, props),
['title', 'hoverable', 'children', 'class']
);

return (
<div
{...rest}
class={`card ${local.hoverable ? 'card-hoverable' : ''} ${local.class ?? ''}`}
>
<div class="card-header">
<h3>{local.title}</h3>
</div>
<div class="card-body">
{local.children}
</div>
</div>
);
}

// 사용 — onClick, style 등 모든 HTML 속성 전달 가능
<Card
title="사용자 정보"
hoverable
onClick={() => console.log('클릭')}
style="max-width: 400px;"
>
<p>카드 내용</p>
</Card>

정리

컴포넌트/패턴용도핵심 특징
<Show>조건부 렌더링when, fallback, render prop으로 타입 좁히기
<For>배열 렌더링참조 기반 추적, DOM 이동 최적화
<Index>인덱스 기반 렌더링위치 기반 추적, item이 Signal
<Switch>/<Match>다중 조건 분기첫 번째 truthy Match만 렌더링
<ErrorBoundary>에러 처리fallback(err, reset) 시그니처
<Suspense>비동기 로딩createResource와 자동 통합
mergeProps기본값 설정반응형 props 병합
splitPropsprops 분리컴포넌트용 / HTML요소용 분리
구조 분해 금지반응성 유지props, signal 모두 직접 접근

다음 장에서는 Solid.js의 전역 상태 관리(createStore)와 컴포넌트 간 데이터 공유 패턴을 살펴봅니다.

Advertisement