본문으로 건너뛰기
Advertisement

18.3 반응형 프리미티브

Solid.js의 모든 기능은 반응형 프리미티브(Reactive Primitives) 위에 구축됩니다. createSignal, createEffect, createMemo, createResource 네 가지가 핵심이며, 이들은 서로 연동하여 Solid.js의 세분화된 반응성 시스템을 구성합니다.


Solid.js 반응성의 핵심 원리: Tracking Context

모든 반응형 프리미티브를 이해하기 전에 **Tracking Context(추적 컨텍스트)**를 이해해야 합니다.

Tracking Context란?

createEffect, createMemo, JSX 표현식
↓ (실행 중 Tracking Context 활성화)
내부에서 Signal getter 호출됨

Signal이 "현재 컨텍스트를 구독자 목록에 등록"

Signal 값 변경 시 → 해당 컨텍스트(Effect/Memo)를 재실행

즉, **"어떤 컨텍스트 안에서 Signal을 읽었는가"**가 반응성 추적의 핵심입니다.

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

const [count, setCount] = createSignal(0);

// ✅ Tracking Context 안에서 읽음 → count의 구독자가 됨
createEffect(() => {
console.log(count()); // count 변경 시 재실행됨
});

// ❌ Tracking Context 밖에서 읽음 → 구독 안 됨
console.log(count()); // 한 번만 실행, 변경 추적 없음

createSignal — 기본 상태 관리

createSignal은 Solid.js에서 가장 기본적인 상태 단위입니다. [getter, setter] 튜플을 반환합니다.

기본 사용법

import { createSignal } from 'solid-js';

function Counter() {
// getter: count (함수), setter: setCount
const [count, setCount] = createSignal(0);

return (
<div>
{/* getter를 호출()해야 값을 읽을 수 있음 */}
<p>현재 값: {count()}</p>
<button onClick={() => setCount(count() + 1)}>+1</button>
</div>
);
}

Setter의 두 가지 형태

const [count, setCount] = createSignal(0);

// 1. 직접 값 전달
setCount(5);

// 2. 함수형 업데이트 (이전 값 기반 변경 시 권장)
setCount(prev => prev + 1);
setCount(prev => prev * 2);

함수형 업데이트는 비동기 환경이나 여러 곳에서 동시에 상태를 변경할 때 race condition을 방지하는 안전한 방법입니다.

다양한 타입의 Signal

// 원시값
const [name, setName] = createSignal('홍길동');
const [active, setActive] = createSignal(false);
const [price, setPrice] = createSignal(10000);

// 배열
const [items, setItems] = createSignal(['사과', '바나나']);

// 객체
const [user, setUser] = createSignal({ name: '홍길동', age: 30 });

// 객체 업데이트 — 반드시 새 객체로 교체
setUser(prev => ({ ...prev, age: 31 }));

// null/undefined
const [error, setError] = createSignal<string | null>(null);

중요: createSignal은 객체의 깊은 반응성을 제공하지 않습니다. 객체 프로퍼티를 직접 변경해도 반응성이 트리거되지 않습니다. 깊은 객체 반응성이 필요하면 createStore를 사용하세요.

배치 업데이트 (batch)

여러 Signal을 한 번에 변경하면 기본적으로 각각 DOM 업데이트가 일어납니다. batch를 사용하면 모든 변경을 하나로 묶어 단 한 번의 DOM 업데이트만 발생합니다.

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

function Form() {
const [name, setName] = createSignal('');
const [email, setEmail] = createSignal('');
const [age, setAge] = createSignal(0);

const handleReset = () => {
// batch 없이: 세 번의 DOM 업데이트
// batch 사용: 한 번의 DOM 업데이트
batch(() => {
setName('');
setEmail('');
setAge(0);
});
};

return (
<form>
<input value={name()} onInput={(e) => setName(e.target.value)} />
<input value={email()} onInput={(e) => setEmail(e.target.value)} />
<input type="number" value={age()} onInput={(e) => setAge(Number(e.target.value))} />
<button type="button" onClick={handleReset}>초기화</button>
</form>
);
}

Signal 동등성 비교 설정

기본적으로 Signal은 ===로 이전 값과 비교합니다. 같으면 업데이트하지 않습니다.

// 커스텀 동등성 비교
const [user, setUser] = createSignal(
{ name: '홍길동', age: 30 },
{
// 이름이 같으면 동일한 값으로 취급 (age 변경 무시)
equals: (prev, next) => prev.name === next.name,
}
);

// 항상 업데이트 (동등성 검사 비활성화)
const [forceUpdate, setForceUpdate] = createSignal(0, { equals: false });

createEffect — 의존성 자동 추적

createEffect는 반응형 컨텍스트 안에서 실행되는 함수입니다. 내부에서 읽은 Signal이 변경되면 자동으로 재실행됩니다.

기본 사용법

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

function Logger() {
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal('홍길동');

// count()와 name() 둘 다 구독 — 어느 하나라도 바뀌면 재실행
createEffect(() => {
console.log(`${name()}의 카운트: ${count()}`);
// 브라우저 탭 제목 업데이트 (사이드 이펙트)
document.title = `${name()} - ${count()}`;
});

return (
<div>
<p>{name()}: {count()}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={() => setName('김철수')}>이름 변경</button>
</div>
);
}

Cleanup 함수

Effect가 재실행되기 전(또는 컴포넌트가 언마운트될 때) 이전 Effect의 정리 작업을 위해 cleanup 함수를 반환합니다.

import { createSignal, createEffect, onCleanup } from 'solid-js';

function MouseTracker() {
const [position, setPosition] = createSignal({ x: 0, y: 0 });
const [enabled, setEnabled] = createSignal(false);

createEffect(() => {
if (!enabled()) return; // enabled가 false면 이벤트 리스너 등록 안 함

const handleMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};

window.addEventListener('mousemove', handleMove);

// onCleanup: Effect 재실행 전 또는 컴포넌트 언마운트 시 실행
onCleanup(() => {
window.removeEventListener('mousemove', handleMove);
console.log('이벤트 리스너 제거됨');
});
});

return (
<div>
<label>
<input
type="checkbox"
checked={enabled()}
onChange={(e) => setEnabled(e.target.checked)}
/>
마우스 추적 활성화
</label>
<p>위치: ({position().x}, {position().y})</p>
</div>
);
}

중첩 Effect

Effect 안에 또 다른 Effect를 중첩할 수 있습니다. 내부 Effect는 외부 Effect가 재실행될 때 자동으로 정리(dispose)됩니다.

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

function NestedEffects() {
const [outer, setOuter] = createSignal(0);
const [inner, setInner] = createSignal(0);

createEffect(() => {
console.log('외부 Effect 실행, outer =', outer());

// inner가 바뀔 때만 실행되는 내부 Effect
// outer가 바뀌면 내부 Effect는 자동으로 재생성됨
createEffect(() => {
console.log(' 내부 Effect 실행, inner =', inner());
});
});

return (
<div>
<button onClick={() => setOuter(o => o + 1)}>Outer +1</button>
<button onClick={() => setInner(i => i + 1)}>Inner +1</button>
</div>
);
}

주의: 무한 루프 방지

// ❌ 무한 루프 — Effect 안에서 자신이 의존하는 Signal을 변경
createEffect(() => {
setCount(count() + 1); // count 읽기 → 재실행 → 무한 루프!
});

// ✅ 올바른 방식 — 다른 Signal만 변경
createEffect(() => {
setLog(prev => [...prev, `count: ${count()}`]); // log는 읽지 않으므로 안전
});

createMemo — 계산된 값, 메모이제이션

createMemo는 다른 Signal에서 파생되는 계산된 값을 메모이제이션합니다. 의존하는 Signal이 바뀌지 않으면 이전 값을 반환하여 불필요한 재계산을 방지합니다.

기본 사용법

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

function PriceCalculator() {
const [price, setPrice] = createSignal(10000);
const [quantity, setQuantity] = createSignal(3);
const [discountRate, setDiscountRate] = createSignal(0.1);

// price나 quantity 중 하나가 바뀔 때만 재계산
const subtotal = createMemo(() => price() * quantity());

// subtotal이나 discountRate가 바뀔 때만 재계산
const discount = createMemo(() => subtotal() * discountRate());

// 포맷된 문자열 메모이제이션
const finalPrice = createMemo(() =>
new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW',
}).format(subtotal() - discount())
);

return (
<div>
<label>단가: <input type="number" value={price()} onInput={(e) => setPrice(Number(e.target.value))} /></label>
<label>수량: <input type="number" value={quantity()} onInput={(e) => setQuantity(Number(e.target.value))} /></label>
<label>할인율: <input type="range" min="0" max="1" step="0.05" value={discountRate()} onInput={(e) => setDiscountRate(Number(e.target.value))} /></label>
<p>소계: {subtotal().toLocaleString()}</p>
<p>할인액: {discount().toLocaleString()}</p>
<p><strong>최종: {finalPrice()}</strong></p>
</div>
);
}

React useMemo와의 차이점

// React useMemo
const result = useMemo(() => expensiveCalc(a, b), [a, b]);
// result는 값 (number, string 등)
// 의존성 배열을 수동으로 관리해야 함

// Solid createMemo
const result = createMemo(() => expensiveCalc(a(), b()));
// result는 getter 함수
// 의존성 자동 추적 — 배열 불필요
console.log(result()); // 함수 호출()로 값 접근
항목React useMemoSolid createMemo
반환값계산된 값 직접getter 함수
의존성수동 배열 지정자동 추적
실행 시점렌더링 중Signal 변경 시 즉시
중첩불가가능 (Signal처럼 사용)

Memo를 Signal처럼 사용하기

createMemo의 반환값은 Signal getter처럼 사용할 수 있습니다. 즉, 다른 Memo나 Effect가 이를 구독할 수 있습니다.

const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);

const sum = createMemo(() => a() + b()); // a, b 구독
const sumSquared = createMemo(() => sum() ** 2); // sum 구독
const isLarge = createMemo(() => sumSquared() > 100); // sumSquared 구독

// a가 바뀌면: sum → sumSquared → isLarge 순서로 최소한으로 재계산

이전 값 활용

// createMemo에 이전 값을 전달받는 함수 형태 사용
const smoothedValue = createMemo<number>((prev = 0) => {
// 급격한 변동을 완화하는 지수 이동 평균
const alpha = 0.3;
return alpha * rawValue() + (1 - alpha) * prev;
});

createResource — 비동기 데이터 페칭

createResource는 비동기 데이터 로딩을 위한 프리미티브입니다. Suspense와 통합하여 로딩/에러/성공 상태를 선언적으로 처리할 수 있습니다.

기본 사용법

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

// API 함수
async function fetchUser(id: number) {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) throw new Error('사용자를 불러올 수 없습니다.');
return response.json();
}

function UserProfile() {
// [data, { loading, error, refetch }] = createResource(source, fetcher)
const [user, { loading, error, refetch }] = createResource(1, fetchUser);
// 또는 단순 버전: const [data] = createResource(() => fetchUser(1));

return (
<div>
{user.loading && <p>로딩 중...</p>}
{user.error && <p style="color: red">에러: {user.error.message}</p>}
{user() && (
<div>
<h2>{user()?.name}</h2>
<p>{user()?.email}</p>
</div>
)}
<button onClick={refetch}>새로고침</button>
</div>
);
}

Signal 기반 동적 fetching

첫 번째 인자(source)로 Signal을 전달하면, Signal 값이 바뀔 때마다 자동으로 다시 fetch합니다.

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

async function fetchPosts(userId: number) {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`
);
return res.json() as Promise<{ id: number; title: string }[]>;
}

function UserPosts() {
const [userId, setUserId] = createSignal(1);

// userId()가 바뀔 때마다 자동으로 fetchPosts 재실행
const [posts] = createResource(userId, fetchPosts);

return (
<div>
<select
value={userId()}
onChange={(e) => setUserId(Number(e.target.value))}
>
{[1, 2, 3, 4, 5].map(id => (
<option value={id}>사용자 {id}</option>
))}
</select>

<Show when={posts.loading}>
<p>포스트 로딩 중...</p>
</Show>

<Show when={!posts.loading}>
<ul>
<For each={posts()}>
{(post) => <li>{post.title}</li>}
</For>
</ul>
</Show>
</div>
);
}

Suspense와 통합

createResourceSuspense 컴포넌트와 완벽하게 통합됩니다. 데이터가 로딩 중이면 fallback을 보여주고, 완료되면 자동으로 실제 UI를 표시합니다.

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

async function fetchProfile(id: number) {
await new Promise(r => setTimeout(r, 1000)); // 지연 시뮬레이션
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!res.ok) throw new Error('에러 발생');
return res.json();
}

function Profile(props: { id: number }) {
const [user] = createResource(() => props.id, fetchProfile);

return (
<div>
<h2>{user()?.name}</h2>
<p>이메일: {user()?.email}</p>
<p>전화: {user()?.phone}</p>
</div>
);
}

function App() {
const [selectedId, setSelectedId] = createSignal(1);

return (
<div>
<button onClick={() => setSelectedId(i => i + 1)}>다음 사용자</button>

{/* ErrorBoundary: 에러 발생 시 fallback 표시 */}
<ErrorBoundary fallback={(err, reset) => (
<div>
<p>에러: {err.message}</p>
<button onClick={reset}>다시 시도</button>
</div>
)}>
{/* Suspense: 로딩 중 fallback 표시 */}
<Suspense fallback={<p>프로필 로딩 중...</p>}>
<Profile id={selectedId()} />
</Suspense>
</ErrorBoundary>
</div>
);
}

refetch와 mutate

const [data, { refetch, mutate }] = createResource(source, fetcher);

// 강제 재로딩 (캐시 무효화)
refetch();

// 서버 요청 없이 로컬 데이터 즉시 업데이트 (낙관적 업데이트)
mutate(newValue);

React useState/useEffect와의 근본적 차이점

1. 컴포넌트 재실행 여부

// React: 버튼 클릭 시 Counter 전체가 재실행됨
function Counter() {
const [count, setCount] = useState(0);
console.log('React Counter 실행됨'); // 클릭마다 출력

return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// Solid: 버튼 클릭 시 Counter 함수는 재실행되지 않음
function Counter() {
const [count, setCount] = createSignal(0);
console.log('Solid Counter 실행됨'); // 초기 1회만 출력

return <button onClick={() => setCount(c => c + 1)}>{count()}</button>;
}

2. 의존성 배열

// React: 의존성 배열 수동 관리 (버그 유발 가능)
useEffect(() => {
document.title = `${name} - ${count}`;
}, [name, count]); // 빼먹으면 버그!

// Solid: 의존성 자동 추적
createEffect(() => {
document.title = `${name()} - ${count()}`; // 자동으로 name, count 추적
});

3. Stale Closure 문제

// React의 stale closure 문제
function Timer() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count가 0으로 고정됨 (stale closure!)
}, 1000);
return () => clearInterval(id);
}, []); // 의존성 배열에 count를 넣지 않아서 문제 발생
}

// Solid: stale closure 없음 (Signal getter는 항상 최신값)
function Timer() {
const [count, setCount] = createSignal(0);

onMount(() => {
const id = setInterval(() => {
setCount(c => c + 1); // Signal getter는 항상 최신값 보장
}, 1000);
onCleanup(() => clearInterval(id));
});
}

4. 성능 최적화 필요성

// React: 렌더링 최적화를 위해 추가 훅 필요
const memoizedValue = useMemo(() => expensive(a, b), [a, b]);
const memoizedCallback = useCallback(() => doSomething(a), [a]);
const Component = React.memo(MyComponent); // 리렌더링 방지

// Solid: 컴포넌트 재실행이 없으므로 추가 최적화 불필요
const result = createMemo(() => expensive(a(), b())); // createMemo로 충분

비교 요약표

항목ReactSolid.js
상태 선언useState(0)[value, setter]createSignal(0)[getter, setter]
값 읽기value (직접 접근)value() (함수 호출)
Effect 의존성수동 배열 지정자동 추적
컴포넌트 재실행상태 변경 시 재실행초기 1회만 실행
Stale Closure발생 가능발생하지 않음
메모이제이션useMemo, React.memo 필요createMemo만으로 충분
비동기 데이터useEffect + statecreateResource

구조 분해 할당 금지 이유 (반응성 손실)

Solid.js에서 가장 흔한 실수이자 가장 중요한 규칙입니다.

왜 구조 분해를 하면 안 되는가?

const [state, setState] = createSignal({ name: '홍길동', count: 0 });

// ❌ 구조 분해 시 반응성 손실
const { name, count } = state(); // 이 순간의 값 스냅샷 캡처
// name과 count는 일반 변수 — Signal과 연결 끊김
// state()가 바뀌어도 name, count는 영원히 '홍길동', 0

// ✅ 올바른 방법: 항상 Signal getter를 통해 접근
const name = () => state().name; // getter 함수로 래핑
// 또는 JSX에서 직접 접근
<div>{state().name}</div>

Props에서도 동일

// ❌ props 구조 분해 — 반응성 손실!
function BadComponent({ name, age }) {
// name과 age는 초기값으로 고정됨
return <div>{name} ({age})</div>;
}

// ✅ props 객체로 접근
function GoodComponent(props) {
// props.name은 항상 최신값 (Signal처럼 동작)
return <div>{props.name} ({props.age})</div>;
}

예외: createStore의 구조 분해

createStore로 만든 상태는 Proxy 기반이므로 구조 분해 후에도 반응성이 유지됩니다(단, 최상위 레벨만).

import { createStore } from 'solid-js/store';

const [state, setState] = createStore({ name: '홍길동', count: 0 });

// createStore는 구조 분해 가능 (Proxy이므로 추적됨)
// 단, 이는 createSignal과 다른 메커니즘!

실전 예제 1: 자동완성 검색

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

const CITIES = [
'서울', '부산', '인천', '대구', '대전', '광주', '울산', '세종',
'수원', '성남', '고양', '용인', '창원', '청주', '전주', '천안',
];

function AutoComplete() {
const [query, setQuery] = createSignal('');
const [selected, setSelected] = createSignal('');
const [isOpen, setIsOpen] = createSignal(false);
const [focusIndex, setFocusIndex] = createSignal(-1);

const filtered = createMemo(() => {
const q = query().trim().toLowerCase();
if (!q) return [];
return CITIES.filter(c => c.includes(q)).slice(0, 8);
});

// 검색 결과가 없어지면 드롭다운 닫기
createEffect(() => {
if (filtered().length === 0) {
setIsOpen(false);
setFocusIndex(-1);
} else if (query().length > 0) {
setIsOpen(true);
}
});

const handleInput = (e: InputEvent & { target: HTMLInputElement }) => {
setQuery(e.target.value);
setSelected('');
};

const handleSelect = (city: string) => {
setSelected(city);
setQuery(city);
setIsOpen(false);
setFocusIndex(-1);
};

const handleKeyDown = (e: KeyboardEvent) => {
if (!isOpen()) return;

switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setFocusIndex(i => Math.min(i + 1, filtered().length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setFocusIndex(i => Math.max(i - 1, -1));
break;
case 'Enter':
if (focusIndex() >= 0) {
handleSelect(filtered()[focusIndex()]);
}
break;
case 'Escape':
setIsOpen(false);
break;
}
};

return (
<div class="autocomplete" style="position: relative; max-width: 300px;">
<input
type="text"
value={query()}
onInput={handleInput}
onKeyDown={handleKeyDown}
placeholder="도시 검색..."
style="width: 100%; padding: 0.5rem;"
/>

<Show when={isOpen() && filtered().length > 0}>
<ul style="
position: absolute; top: 100%; left: 0; right: 0;
background: white; border: 1px solid #ccc;
list-style: none; margin: 0; padding: 0;
max-height: 200px; overflow-y: auto; z-index: 10;
">
<For each={filtered()}>
{(city, index) => (
<li
onClick={() => handleSelect(city)}
style={`
padding: 0.5rem;
cursor: pointer;
background: ${focusIndex() === index() ? '#e3f2fd' : 'white'};
`}
>
{city}
</li>
)}
</For>
</ul>
</Show>

<Show when={selected()}>
<p style="color: green; margin-top: 0.5rem;">
선택됨: <strong>{selected()}</strong>
</p>
</Show>
</div>
);
}

export default AutoComplete;

실전 예제 2: 디바운스 검색 with createResource

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

// 디바운스 Signal 생성 유틸리티
function createDebouncedSignal<T>(value: T, delay = 300) {
const [signal, setSignal] = createSignal<T>(value);
let timeout: ReturnType<typeof setTimeout>;

const setDebounced = (newValue: T) => {
clearTimeout(timeout);
timeout = setTimeout(() => setSignal(() => newValue), delay);
};

return [signal, setDebounced] as const;
}

interface Post {
id: number;
title: string;
body: string;
userId: number;
}

async function searchPosts(query: string): Promise<Post[]> {
if (!query) return [];
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts?_limit=5`
);
const posts: Post[] = await res.json();
return posts.filter(p =>
p.title.toLowerCase().includes(query.toLowerCase())
);
}

function DebounceSearch() {
const [inputValue, setInputValue] = createSignal('');
const [debouncedQuery, setDebouncedQuery] = createDebouncedSignal('', 400);

const [results] = createResource(debouncedQuery, searchPosts);

const handleInput = (e: InputEvent & { target: HTMLInputElement }) => {
const val = e.target.value;
setInputValue(val);
setDebouncedQuery(val);
};

return (
<div>
<input
type="search"
value={inputValue()}
onInput={handleInput}
placeholder="포스트 제목 검색 (0.4초 디바운스)..."
style="width: 100%; padding: 0.5rem; margin-bottom: 1rem;"
/>

<Suspense fallback={<p>검색 중...</p>}>
<Show
when={results() && results()!.length > 0}
fallback={
<Show when={debouncedQuery()}>
<p>'{debouncedQuery()}' 검색 결과가 없습니다.</p>
</Show>
}
>
<ul>
<For each={results()}>
{(post) => (
<li style="padding: 0.5rem; border-bottom: 1px solid #eee;">
<strong>{post.title}</strong>
<p style="font-size: 0.85rem; color: #666;">{post.body.slice(0, 80)}...</p>
</li>
)}
</For>
</ul>
</Show>
</Suspense>
</div>
);
}

export default DebounceSearch;

실전 예제 3: 커스텀 훅 패턴

// src/hooks/useLocalStorage.ts
import { createSignal, createEffect } from 'solid-js';

export function useLocalStorage<T>(key: string, initialValue: T) {
// localStorage에서 초기값 읽기
const stored = localStorage.getItem(key);
const initial = stored ? (JSON.parse(stored) as T) : initialValue;

const [value, setValue] = createSignal<T>(initial);

// 값이 바뀔 때마다 localStorage에 저장
createEffect(() => {
localStorage.setItem(key, JSON.stringify(value()));
});

return [value, setValue] as const;
}

// 사용 예시
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('lang', 'ko');

return (
<div>
<select value={theme()} onChange={(e) => setTheme(e.target.value)}>
<option value="light">라이트</option>
<option value="dark">다크</option>
</select>
<select value={language()} onChange={(e) => setLanguage(e.target.value)}>
<option value="ko">한국어</option>
<option value="en">English</option>
</select>
</div>
);
}

고수 팁

팁 1: untrack으로 반응성 선택적 차단

import { createSignal, createEffect, untrack } from 'solid-js';

const [a, setA] = createSignal(0);
const [b, setB] = createSignal(0);

createEffect(() => {
// a가 변할 때만 실행
// b는 읽지만 구독하지 않음
const currentA = a();
const currentB = untrack(b); // 추적하지 않고 읽기만 함
console.log(`a=${currentA}, b(비추적)=${currentB}`);
});

팁 2: on으로 의존성 명시 제한

import { createSignal, createEffect, on } from 'solid-js';

const [a, setA] = createSignal(0);
const [b, setB] = createSignal(0);

// React의 useEffect([a])와 유사하게 동작
// a가 변할 때만 실행, b는 무시
createEffect(on(a, (currentA) => {
console.log('a가 변경됨:', currentA);
// b()를 읽어도 b에 대한 구독은 생기지 않음
}));

// 여러 Signal 명시
createEffect(on([a, b], ([currentA, currentB]) => {
console.log('a 또는 b 변경됨:', currentA, currentB);
}));

// 초기 실행 방지 (defer)
createEffect(on(a, (val) => {
console.log('a 변경됨 (초기 실행 없음):', val);
}, { defer: true }));

팁 3: Signal 구독자 수 모니터링

// 개발 환경에서 과도한 구독 감지
import { createSignal } from 'solid-js';

function createTrackedSignal<T>(initial: T, name: string) {
const [get, set] = createSignal<T>(initial);

let readCount = 0;
const trackedGet = () => {
readCount++;
if (readCount % 100 === 0) {
console.warn(`[Signal:${name}] ${readCount}번 읽힘`);
}
return get();
};

return [trackedGet, set] as const;
}

팁 4: createRoot로 컴포넌트 외부 반응성

import { createRoot, createSignal, createEffect } from 'solid-js';

// 컴포넌트 밖에서 반응성이 필요할 때 (전역 상태 등)
const globalStore = createRoot(() => {
const [user, setUser] = createSignal<string | null>(null);
const [theme, setTheme] = createSignal('light');

createEffect(() => {
document.documentElement.setAttribute('data-theme', theme());
});

return { user, setUser, theme, setTheme };
});

export default globalStore;

팁 5: 비동기 Signal 업데이트와 batch

// 복잡한 비동기 작업 후 여러 상태를 동시에 업데이트
async function loadDashboard() {
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
]);

// 두 업데이트를 하나의 DOM 렌더링으로 처리
batch(() => {
setUsers(users);
setPosts(posts);
setLoading(false);
});
}

정리

프리미티브용도핵심 특징
createSignal기본 상태getter/setter, 배치 업데이트 지원
createEffect사이드 이펙트자동 의존성 추적, onCleanup
createMemo계산된 값메모이제이션, Signal처럼 사용 가능
createResource비동기 데이터Suspense 통합, refetch/mutate
batch일괄 업데이트여러 Signal 변경을 1회 DOM 업데이트로
untrack추적 차단Signal 값 읽되 구독하지 않음
on명시적 의존성특정 Signal만 추적, defer 옵션

다음 장에서는 Solid.js의 제어 흐름 컴포넌트(Show, For, Index, Switch, Suspense)와 Props 반응성 패턴을 살펴봅니다.

Advertisement