요즘 프론트엔드 개발자들 사이에서 Solid.js라는 프레임워크가 화제입니다. React와 비슷한 문법을 사용하면서도 vanilla JavaScript보다 단 5% 느린 수준의 놀라운 성능을 보여주거든요. 이 글에서는 Solid.js가 무엇인지, 어떻게 사용하는지, 그리고 왜 많은 개발자들이 주목하는지 차근차근 알아볼게요.

1. Solid.js란 무엇인가요?
Solid.js는 웹 페이지를 만들기 위한 JavaScript 라이브러리예요. Ryan Carniato가 만들었고, 2020년대 초반부터 시작해서 지금까지 꾸준히 발전하고 있습니다.
가장 큰 특징은 React나 Vue와 다르게 Virtual DOM을 사용하지 않는다는 점이에요. 대신 코드를 실제 DOM으로 직접 컴파일하고, 변경이 필요한 부분만 정확히 찾아서 업데이트합니다. 마치 수술할 때 필요한 부위만 정밀하게 치료하는 것처럼요. 전체를 다시 그리지 않으니 당연히 훨씬 빠르겠죠?
현재 최신 버전은 1.9.9이고, npm으로 간단히 설치할 수 있어요. 2.0 버전도 개발 중이라 앞으로가 더 기대되는 프레임워크입니다.
Solid.js가 추구하는 세 가지 원칙
Solid.js는 개발할 때 정말 편하게 느껴지는데, 그 이유가 이 세 가지 원칙에 있어요:
- 컴포넌트는 한 번만 실행돼요: 처음 화면에 그려질 때 딱 한 번만 실행되고, 이후에는 변경된 부분만 업데이트됩니다. React처럼 컴포넌트 전체가 계속 다시 실행되지 않아요.
- 자동으로 연결돼요: 어떤 데이터를 사용하면 자동으로 그 데이터를 “구독”하게 되고, 데이터가 바뀔 때만 화면이 업데이트됩니다. 일일이 의존성을 신경 쓸 필요가 없어요.
- 읽기와 쓰기가 분리돼 있어요: 데이터를 읽는 부분과 수정하는 부분이 명확히 구분되어서, 코드를 읽을 때 “아, 이건 데이터를 읽는 거구나” “이건 수정하는 거구나” 바로 알 수 있죠.
이런 원칙 덕분에 코드가 직관적이고 버그도 적어요.
2. Virtual DOM 없이도 빠른 이유: 세밀한 반응성의 비밀
“Virtual DOM이 빠른 거 아니었어요?” 많이들 궁금해하시죠. 맞아요, 예전에는 그랬어요. DOM이 대규모 업데이트를 처리하는 데 최적화되지 않았을 때는 Virtual DOM이 더 빠른 방법이었습니다.
하지만 Solid.js는 다른 방식을 선택했어요. 상태(state)를 만들고 앱에서 사용하면, 그 상태가 바뀔 때 딱 그것만 사용하는 코드만 다시 실행되는 거예요. 이걸 “Fine-Grained Reactivity”라고 부르는데, 우리말로는 “세밀한 반응성” 정도로 이해하면 될 것 같아요.
쉽게 이해해볼까요?
쇼핑몰 사이트에서 장바구니에 상품을 추가하는 상황을 생각해볼게요:
React는 이렇게 동작해요:
- 장바구니 컴포넌트 전체를 다시 그려요
- 장바구니에 상품 10개가 있는데 1개를 추가하면, 11개 모두를 다시 렌더링합니다
- Virtual DOM에서 뭐가 바뀌었는지 비교한 다음, 실제 DOM에 적용해요
Solid.js는 이렇게 동작해요:
- 새로 추가된 상품 1개만 DOM에 바로 추가해요
- 나머지 10개는 전혀 건드리지 않습니다
- 비교 과정이 없어서 오버헤드가 없어요
이 차이가 성능에 큰 영향을 미쳐요. 벤치마크 테스트를 보면 Solid.js가 React보다 2~3배 빠르게 동작합니다.
컴파일러의 마법
Solid.js는 빌드 시점에 템플릿을 분석하고 최적화된 JavaScript 코드로 변환합니다. 다음 코드를 보면 이해가 쉬울 겁니다:
// 우리가 작성하는 코드
function Counter() {
const [count, setCount] = createSignal(0);
return <button onClick={() => setCount(c => c + 1)}>{count()}</button>;
}
// 컴파일러가 변환한 코드 (간소화 버전)
function Counter() {
const [count, setCount] = createSignal(0);
const button = document.createElement('button');
button.onclick = () => setCount(c => c + 1);
// count가 변경될 때만 이 부분만 실행됨
createEffect(() => {
button.textContent = count();
});
return button;
}
실제 DOM 노드를 직접 생성하고, 변경이 필요한 부분만 반응형으로 연결합니다. 이것이 Solid.js가 빠른 핵심 이유입니다.
3. React 개발자라면 쉽게 배울 수 있습니다
Solid.js의 또 다른 매력은 친숙함입니다. React와 비슷한 철학을 따르며, 단방향 데이터 흐름(unidirectional data flow), 읽기/쓰기 분리, 불변 인터페이스를 지원합니다.
하지만 중요한 차이가 하나 있습니다. Solid에서는 컴포넌트가 처음 렌더링될 때 단 한 번만 실행되며, 훅(Hooks)과 바인딩은 의존성이 업데이트될 때만 실행됩니다. React처럼 재렌더링 걱정을 할 필요가 없다는 뜻이죠.
React vs Solid.js 코드 비교
React 방식:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const doubleCount = count * 2;
console.log('렌더링할 때마다 실행됩니다!');
return (
<button onClick={() => setCount(count + 1)}>
클릭 횟수: {doubleCount}
</button>
);
}
Solid.js 방식:
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const doubleCount = () => count() * 2;
console.log("이 함수는 딱 한 번만 실행됩니다!");
return (
<button onClick={() => setCount(c => c + 1)}>
클릭 횟수: {doubleCount()}
</button>
);
}
핵심 차이점:
- Signal은 함수로 호출해야 합니다:
count()vscount - 컴포넌트 함수는 한 번만 실행되므로 성능 최적화가 자동으로 됩니다
useMemo,useCallback같은 최적화 훅이 필요 없습니다
4. 성능이 정말 빠를까? 실제 수치로 확인해보기
말보다는 숫자로 보여드릴게요. JS Framework Benchmark라는 공식 벤치마크 테스트를 보면, Solid.js는 vanilla JavaScript(순수 자바스크립트)보다 약 5% 느린 정도예요. 프레임워크를 쓰면서 이 정도 성능이면 정말 대단한 거죠. 반면 React는 아무리 최적화해도 vanilla JS보다 거의 100% 느립니다.
더 자세한 수치를 볼까요?
렌더링 속도 테스트 (1,000개 행 생성):
- Solid.js: 평균 43.5ms
- React Hooks: 평균 45.6ms
- React-Redux: 평균 49.1ms
밀리초 단위라 작아 보이지만, 이게 쌓이면 사용자가 확실히 느껴요.
파일 크기 비교 (프로덕션 빌드, gzip 압축 후):
- Solid.js: 11.1kb (프레임워크 6.2kb + 앱 코드 4.9kb)
- React 19: 47.3kb (React 런타임 32.1kb + 앱 코드 15.2kb)
Solid.js가 4배 이상 작네요!
메모리 사용량:
- Solid.js는 순수 JavaScript보다 약 26% 더 사용해요
- React는 순수 JavaScript보다 80~120% 더 사용합니다
이게 실제로는 어떤 의미일까요?
모바일 3G 네트워크로 접속하는 사용자를 생각해볼게요:
- Solid.js 앱: 약 0.5초 안에 로딩됩니다
- React 앱: 약 2초 이상 걸려요
이 1.5초 차이가 작아 보이지만, 사용자 입장에서는 엄청난 차이예요. “빠른 사이트”와 “느린 사이트”를 가르는 기준점이거든요.
5. Solid.js 시작하기: 설치부터 첫 프로젝트까지
이제 직접 Solid.js를 시작해볼까요? Node.js(v18 이상 권장)가 설치되어 있다면 바로 시작할 수 있습니다.
개발 환경 준비
시작하기 전에 필요한 것들:
- Node.js 18 이상 (https://nodejs.org/)
- 코드 에디터 (VS Code 권장)
- VS Code 확장 프로그램: “TypeScript and JavaScript Language Features”
프로젝트 생성하기
터미널(Terminal)을 열고 다음 명령어를 입력하세요:
# npm 사용
npm create solid@latest my-solid-app
# 또는 pnpm 사용 (더 빠름)
pnpm create solid my-solid-app
명령어를 실행하면 대화형 질문이 나옵니다:
? Which template would you like to use?
> bare (기본 템플릿)
with-tailwindcss (Tailwind CSS 포함)
with-typescript (TypeScript 포함)
? Use Server-Side Rendering?
> Yes (SEO가 중요한 경우)
No (단순 SPA인 경우)
? Use TypeScript?
> Yes (권장 - 타입 안정성)
No
프로젝트 구조 완벽 이해하기
생성된 프로젝트의 구조는 다음과 같습니다:
my-solid-app/
├── public/ # 정적 파일 디렉토리
│ ├── favicon.ico
│ └── assets/ # 이미지, 폰트 등
├── src/
│ ├── routes/ # 페이지 라우트 (파일 기반 라우팅)
│ │ └── index.tsx # 홈페이지 (/)
│ ├── components/ # 재사용 가능한 컴포넌트
│ ├── entry-client.tsx # 클라이언트 진입점
│ ├── entry-server.tsx # 서버 진입점
│ └── app.tsx # 앱의 HTML 루트
├── package.json
├── tsconfig.json # TypeScript 설정
└── vite.config.ts # Vite 빌드 설정
주요 파일 설명:
- src/routes/: 이 폴더에 파일을 추가하면 자동으로 라우트가 생성됩니다
index.tsx→/about.tsx→/aboutusers/[id].tsx→/users/:id(동적 라우트)
- src/entry-client.tsx: 브라우저에서 앱이 시작되는 지점
- src/entry-server.tsx: 서버에서 렌더링할 때 사용
- app.tsx: HTML의
<html>,<head>,<body>태그를 정의
개발 서버 실행하기
프로젝트 폴더로 이동한 후:
cd my-solid-app
npm install # 의존성 설치
npm run dev # 개발 서버 시작
브라우저에서 http://localhost:3000을 열면 Solid.js 앱이 실행됩니다. 파일을 수정하면 즉시 반영되는 Hot Module Replacement(HMR)가 작동합니다.
6. 핵심 개념 완벽 정복하기
createSignal: 가장 기본적인 반응형 상태
Signal은 Solid.js의 가장 기본적인 반응형 프리미티브입니다.
import { createSignal } from "solid-js";
function UserProfile() {
// [getter, setter] 형태로 반환
const [name, setName] = createSignal("홍길동");
const [age, setAge] = createSignal(25);
return (
<div>
<p>이름: {name()}</p>
<p>나이: {age()}</p>
<button onClick={() => setAge(age() + 1)}>
생일 축하!
</button>
<input
type="text"
value={name()}
onInput={(e) => setName(e.target.value)}
placeholder="이름을 입력하세요"
/>
</div>
);
}
주의사항:
- Signal을 읽을 때는 반드시 함수로 호출:
name()✅,name❌ - JSX 내에서도 함수 형태로:
{name()}✅,{name}❌
createEffect: 부수 효과 관리
Signal 값이 변경될 때 자동으로 실행되는 코드를 작성할 수 있습니다:
import { createSignal, createEffect } from "solid-js";
function SearchComponent() {
const [searchTerm, setSearchTerm] = createSignal("");
const [results, setResults] = createSignal([]);
// searchTerm이 변경될 때마다 자동 실행
createEffect(() => {
const term = searchTerm();
if (term.length > 2) {
// API 호출
fetch(`https://api.example.com/search?q=${term}`)
.then(res => res.json())
.then(data => setResults(data));
} else {
setResults([]);
}
});
return (
<div>
<input
type="text"
value={searchTerm()}
onInput={(e) => setSearchTerm(e.target.value)}
placeholder="검색어 입력 (3자 이상)"
/>
<ul>
<For each={results()}>
{(item) => <li>{item.title}</li>}
</For>
</ul>
</div>
);
}
createMemo: 파생 상태 만들기
계산 비용이 큰 값을 캐싱할 때 사용합니다:
import { createSignal, createMemo } from "solid-js";
function ShoppingCart() {
const [items, setItems] = createSignal([
{ id: 1, name: "노트북", price: 1000000, quantity: 1 },
{ id: 2, name: "마우스", price: 50000, quantity: 2 },
]);
// items가 변경될 때만 재계산
const totalPrice = createMemo(() => {
console.log("총액 계산 중...");
return items().reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
});
const formattedTotal = createMemo(() =>
totalPrice().toLocaleString('ko-KR') + "원"
);
return (
<div>
<h2>장바구니</h2>
<For each={items()}>
{(item) => (
<div>
<span>{item.name}</span>
<span>{item.price}원 × {item.quantity}</span>
</div>
)}
</For>
<div class="total">
<strong>총 금액: {formattedTotal()}</strong>
</div>
</div>
);
}
createStore: 복잡한 객체 상태 관리
중첩된 객체나 배열을 다룰 때는 Store를 사용하는 게 편합니다:
import { createStore } from "solid-js/store";
function TodoApp() {
const [todos, setTodos] = createStore([
{ id: 1, text: "Solid.js 배우기", completed: false },
{ id: 2, text: "앱 만들기", completed: false }
]);
// 특정 항목만 업데이트 (세밀한 반응성)
const toggleTodo = (id) => {
setTodos(
(todo) => todo.id === id, // 조건
"completed", // 속성
(completed) => !completed // 업데이트 함수
);
};
const addTodo = (text) => {
setTodos([...todos, {
id: Date.now(),
text,
completed: false
}]);
};
return (
<div>
<For each={todos}>
{(todo) => (
<div
onClick={() => toggleTodo(todo.id)}
style={{
"text-decoration": todo.completed ? "line-through" : "none"
}}
>
{todo.text}
</div>
)}
</For>
</div>
);
}
라이프사이클 관리
import { onMount, onCleanup, createSignal } from "solid-js";
function Timer() {
const [seconds, setSeconds] = createSignal(0);
// 컴포넌트가 DOM에 마운트된 후 실행
onMount(() => {
console.log("Timer 컴포넌트가 마운트되었습니다");
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// 컴포넌트가 제거될 때 정리 작업
onCleanup(() => {
clearInterval(interval);
console.log("Timer 컴포넌트가 제거되었습니다");
});
});
return <div>경과 시간: {seconds()}초</div>;
}
7. 여러 가지 예제로 배워보기
이제 실제로 사용할 수 있는 패턴들을 예제로 살펴볼게요. 복사해서 바로 써도 되는 코드들이니 편하게 참고하세요.
예제 1: API 호출과 로딩 상태 관리
import { createSignal, createResource, Show, For } from "solid-js";
// createResource로 비동기 데이터 가져오기
function UserList() {
const fetchUsers = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
return response.json();
};
// [데이터, { refetch, mutate }] 형태로 반환
const [users, { refetch }] = createResource(fetchUsers);
return (
<div>
<h2>사용자 목록</h2>
<button onClick={refetch}>새로고침</button>
{/* 로딩 상태 처리 */}
<Show
when={!users.loading}
fallback={<div>로딩 중...</div>}
>
<Show
when={!users.error}
fallback={<div>에러 발생: {users.error.message}</div>}
>
<ul>
<For each={users()}>
{(user) => (
<li>
<strong>{user.name}</strong> - {user.email}
</li>
)}
</For>
</ul>
</Show>
</Show>
</div>
);
}
예제 2: 폼 유효성 검증
import { createSignal, createMemo } from "solid-js";
function SignupForm() {
const [email, setEmail] = createSignal("");
const [password, setPassword] = createSignal("");
const [confirmPassword, setConfirmPassword] = createSignal("");
// 유효성 검증 로직
const emailError = createMemo(() => {
const value = email();
if (!value) return "";
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
? ""
: "올바른 이메일 형식이 아닙니다";
});
const passwordError = createMemo(() => {
const value = password();
if (!value) return "";
return value.length >= 8
? ""
: "비밀번호는 8자 이상이어야 합니다";
});
const confirmPasswordError = createMemo(() => {
if (!confirmPassword()) return "";
return password() === confirmPassword()
? ""
: "비밀번호가 일치하지 않습니다";
});
const isValid = createMemo(() =>
email() &&
password() &&
!emailError() &&
!passwordError() &&
!confirmPasswordError()
);
const handleSubmit = (e) => {
e.preventDefault();
if (isValid()) {
console.log("회원가입 처리:", { email: email(), password: password() });
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>이메일</label>
<input
type="email"
value={email()}
onInput={(e) => setEmail(e.target.value)}
placeholder="example@email.com"
/>
{emailError() && <span class="error">{emailError()}</span>}
</div>
<div>
<label>비밀번호</label>
<input
type="password"
value={password()}
onInput={(e) => setPassword(e.target.value)}
placeholder="8자 이상"
/>
{passwordError() && <span class="error">{passwordError()}</span>}
</div>
<div>
<label>비밀번호 확인</label>
<input
type="password"
value={confirmPassword()}
onInput={(e) => setConfirmPassword(e.target.value)}
/>
{confirmPasswordError() &&
<span class="error">{confirmPasswordError()}</span>
}
</div>
<button type="submit" disabled={!isValid()}>
회원가입
</button>
</form>
);
}
예제 3: 페이지네이션 구현
import { createSignal, createMemo, For } from "solid-js";
function PaginatedList() {
// 전체 데이터 (실제로는 API에서 가져옴)
const allItems = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
title: `아이템 ${i + 1}`
}));
const [currentPage, setCurrentPage] = createSignal(1);
const itemsPerPage = 10;
// 현재 페이지의 아이템들
const currentItems = createMemo(() => {
const start = (currentPage() - 1) * itemsPerPage;
const end = start + itemsPerPage;
return allItems.slice(start, end);
});
// 총 페이지 수
const totalPages = createMemo(() =>
Math.ceil(allItems.length / itemsPerPage)
);
// 페이지 번호 배열
const pageNumbers = createMemo(() =>
Array.from({ length: totalPages() }, (_, i) => i + 1)
);
return (
<div>
<h2>페이지네이션 예제</h2>
<ul>
<For each={currentItems()}>
{(item) => <li>{item.title}</li>}
</For>
</ul>
<div class="pagination">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage() === 1}
>
이전
</button>
<For each={pageNumbers()}>
{(pageNum) => (
<button
onClick={() => setCurrentPage(pageNum)}
class={currentPage() === pageNum ? "active" : ""}
>
{pageNum}
</button>
)}
</For>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages(), p + 1))}
disabled={currentPage() === totalPages()}
>
다음
</button>
</div>
<p>
전체 {allItems.length}개 중 {currentPage()} / {totalPages()} 페이지
</p>
</div>
);
}
예제 4: Context API로 전역 상태 관리
import { createContext, useContext, createSignal } from "solid-js";
// 1. Context 생성
const ThemeContext = createContext();
// 2. Provider 컴포넌트 생성
function ThemeProvider(props) {
const [theme, setTheme] = createSignal("light");
const toggleTheme = () => {
setTheme(t => t === "light" ? "dark" : "light");
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{props.children}
</ThemeContext.Provider>
);
}
// 3. Custom Hook 생성
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}
// 4. 사용 예시
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header class={theme()}>
<h1>내 웹사이트</h1>
<button onClick={toggleTheme}>
{theme() === "light" ? "🌙 다크모드" : "☀️ 라이트모드"}
</button>
</header>
);
}
function App() {
return (
<ThemeProvider>
<Header />
{/* 다른 컴포넌트들 */}
</ThemeProvider>
);
}
8. SolidStart: Solid.js의 풀스택 프레임워크
React에 Next.js가 있다면, Solid.js에는 SolidStart라는 메타 프레임워크가 있습니다. 2024년 11월에 베타를 벗어나 정식 출시되었으며, 프로덕션 환경에서 안전하게 사용할 수 있습니다.
SolidStart의 주요 기능
| 기능 | 설명 | 예시 |
|---|---|---|
| 파일 기반 라우팅 | 폴더 구조가 곧 URL 구조 | routes/about.tsx → /about |
| API 라우트 | 서버리스 함수 자동 생성 | routes/api/users.ts → /api/users |
| SSR/SSG | 서버/정적 렌더링 선택 가능 | SEO 최적화 |
| Single-Flight Mutations | 중복 요청 자동 방지 | 네트워크 효율성 |
| Islands Architecture | 부분적 hydration 지원 | 성능 최적화 |
| 다중 플랫폼 배포 | Netlify, Vercel, Cloudflare 등 | 간편한 배포 |
SolidStart 프로젝트 생성
# SolidStart 프로젝트 생성
npm create solid@latest my-app
# 선택 사항
# - Template: SolidStart 선택
# - SSR: Yes 선택 (서버 렌더링)
# - TypeScript: Yes 선택 (권장)
cd my-app
npm install
npm run dev
파일 기반 라우팅 활용하기
src/routes/
├── index.tsx → /
├── about.tsx → /about
├── blog/
│ ├── index.tsx → /blog
│ ├── [slug].tsx → /blog/:slug
│ └── new.tsx → /blog/new
└── users/
└── [id].tsx → /users/:id
동적 라우트 예시:
// src/routes/blog/[slug].tsx
import { useParams } from "@solidjs/router";
import { createResource, Show } from "solid-js";
export default function BlogPost() {
const params = useParams();
// slug에 해당하는 블로그 글 가져오기
const [post] = createResource(() => params.slug, async (slug) => {
const res = await fetch(`https://api.example.com/posts/${slug}`);
return res.json();
});
return (
<Show when={!post.loading} fallback={<p>로딩 중...</p>}>
<article>
<h1>{post().title}</h1>
<div innerHTML={post().content} />
</article>
</Show>
);
}
API 라우트 만들기
// src/routes/api/users.ts
import { json } from "@solidjs/start";
// GET /api/users
export async function GET() {
const users = [
{ id: 1, name: "홍길동" },
{ id: 2, name: "김철수" }
];
return json(users);
}
// POST /api/users
export async function POST(event) {
const body = await event.request.json();
// 데이터베이스에 저장하는 로직
const newUser = {
id: Date.now(),
name: body.name
};
return json(newUser, { status: 201 });
}
서버 액션(Server Actions) 사용하기
import { action, useAction } from "@solidjs/router";
import { createSignal } from "solid-js";
// 서버에서만 실행되는 함수
const addTodo = action(async (formData: FormData) => {
"use server"; // 이 지시어로 서버 전용임을 명시
const text = formData.get("text") as string;
// 데이터베이스에 저장
await db.todos.create({ text, completed: false });
return { success: true };
});
function TodoForm() {
const submit = useAction(addTodo);
return (
<form action={submit} method="post">
<input type="text" name="text" required />
<button type="submit">추가</button>
</form>
);
}
9. React와 비교해볼까요?
두 프레임워크를 한눈에 비교해볼게요:
| 항목 | React | Solid.js | 어떤 게 나을까요? |
|---|---|---|---|
| 렌더링 성능 | 우수 | 탁월 (2-3배 빠름) | Solid.js |
| 파일 크기 | 47.3kb | 11.1kb | Solid.js |
| 메모리 사용 | 80-120% 증가 | 26% 증가 | Solid.js |
| 배우기 | 중간 정도 | 쉬움 (React 경험 있으면) | Solid.js |
| 라이브러리 | 엄청 많음 | 점점 늘어나는 중 | React |
| 개발자 구하기 | 쉬움 | 아직 어려움 | React |
| 커뮤니티 | 거대함 | 활발히 성장 중 | React |
| 타입스크립트 | 훌륭함 | 훌륭함 | 비슷해요 |
| SSR | Next.js 필요 | SolidStart 내장 | Solid.js |
| 상태 관리 | 라이브러리 추가 필요 | 내장됨 | Solid.js |
| 최적화 | 직접 해야 함 | 자동 | Solid.js |
프로젝트 특성별로 선택하기
Solid.js가 잘 맞는 경우:
- ✅ 성능이 정말 중요한 서비스 (대시보드, 차트, 데이터 시각화)
- ✅ 모바일 사용자가 많아서 로딩 속도가 중요할 때
- ✅ 실시간으로 업데이트가 많은 앱 (채팅, 실시간 알림, 주식 정보)
- ✅ SEO가 중요한 블로그나 홈페이지
- ✅ 새로 시작하는 프로젝트
- ✅ 소규모 팀 (5명 이하)
React가 더 나은 경우:
- ✅ 큰 회사의 큰 프로젝트 (10명 이상)
- ✅ Material-UI 같은 특정 라이브러리를 꼭 써야 할 때
- ✅ 개발자를 뽑는 게 중요할 때 (React 개발자가 훨씬 많으니까요)
- ✅ 이미 React로 만든 코드가 많을 때
- ✅ React Native로 모바일 앱도 만들어야 할 때
- ✅ 오래된 브라우저도 지원해야 할 때
실제 회사들은 어떻게 쓰고 있을까요?
Solid.js를 쓰는 곳:
- 금융 대시보드 (실시간 데이터가 계속 업데이트되니까)
- 온라인 쇼핑몰 (상품 목록이 많고 빨라야 하니까)
- 기술 블로그 플랫폼 (SEO가 중요하니까)
- 데이터 시각화 도구 (차트가 많고 복잡하니까)
React에서 Solid.js로 옮기는 방법:
- 새로 추가하는 기능은 Solid.js로 만들어보기
- 성능이 특히 중요한 페이지부터 바꿔보기
- 마이크로 프론트엔드 방식으로 React와 같이 쓰기
한 번에 다 바꾸지 않아도 돼요. 점진적으로 시도해볼 수 있답니다.
10. 만든 앱 배포하기
Vercel에 배포하기
# 1. Vercel CLI 설치
npm i -g vercel
# 2. 프로젝트 루트에서 배포
vercel
# 3. 프로덕션 배포
vercel --prod
app.config.ts 설정:
import { defineConfig } from "@solidjs/start/config";
import vercel from "solid-start-vercel";
export default defineConfig({
server: {
preset: "vercel"
}
});
Cloudflare Pages에 배포하기
// app.config.ts
import { defineConfig } from "@solidjs/start/config";
import cloudflare from "solid-start-cloudflare-pages";
export default defineConfig({
server: {
preset: "cloudflare-pages"
}
});
GitHub 저장소와 연결하면 자동 배포됩니다.
Netlify에 배포하기
// app.config.ts
import { defineConfig } from "@solidjs/start/config";
import netlify from "solid-start-netlify";
export default defineConfig({
server: {
preset: "netlify",
// Edge Functions 사용 시
edge: true
}
});
정적 사이트 생성 (SSG)
// app.config.ts
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
server: {
preset: "static"
}
});
빌드 후 dist/public 폴더를 어디든 호스팅할 수 있습니다.
11. 더 빠르게 만드는 방법들
1. 불필요한 Effect 피하기
// ❌ 나쁜 예: 불필요한 Effect
function BadExample() {
const [count, setCount] = createSignal(0);
const [doubled, setDoubled] = createSignal(0);
createEffect(() => {
setDoubled(count() * 2); // 불필요한 Signal
});
return <div>{doubled()}</div>;
}
// ✅ 좋은 예: Memo 사용
function GoodExample() {
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);
return <div>{doubled()}</div>;
}
2. 조건부 렌더링 최적화
// ❌ 나쁜 예: 불필요한 재생성
function BadConditional() {
const [show, setShow] = createSignal(false);
return (
<div>
{show() && <ExpensiveComponent />}
</div>
);
}
// ✅ 좋은 예: Show 컴포넌트 사용
function GoodConditional() {
const [show, setShow] = createSignal(false);
return (
<Show when={show()}>
<ExpensiveComponent />
</Show>
);
}
3. 리스트 렌더링 최적화
// ❌ 나쁜 예: map 사용
function BadList() {
const [items, setItems] = createSignal([1, 2, 3]);
return (
<ul>
{items().map(item => <li>{item}</li>)}
</ul>
);
}
// ✅ 좋은 예: For 컴포넌트 사용
function GoodList() {
const [items, setItems] = createSignal([1, 2, 3]);
return (
<ul>
<For each={items()}>
{(item) => <li>{item}</li>}
</For>
</ul>
);
}
4. 대용량 리스트 처리: Virtualization
import { createVirtualizer } from "@tanstack/solid-virtual";
import { createSignal, For } from "solid-js";
function VirtualizedList() {
const [items] = createSignal(
Array.from({ length: 10000 }, (_, i) => `아이템 ${i}`)
);
let parentRef;
const virtualizer = createVirtualizer({
get count() {
return items().length;
},
getScrollElement: () => parentRef,
estimateSize: () => 35,
});
return (
<div ref={parentRef} style={{ height: "400px", overflow: "auto" }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: "relative"
}}
>
<For each={virtualizer.getVirtualItems()}>
{(virtualRow) => (
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`
}}
>
{items()[virtualRow.index]}
</div>
)}
</For>
</div>
</div>
);
}
5. 이미지 최적화
function OptimizedImage(props) {
const [loaded, setLoaded] = createSignal(false);
return (
<div class="image-container">
<Show when={!loaded()}>
<div class="skeleton" />
</Show>
<img
src={props.src}
alt={props.alt}
loading="lazy" // 브라우저 네이티브 Lazy Loading
onLoad={() => setLoaded(true)}
style={{ display: loaded() ? "block" : "none" }}
/>
</div>
);
}
12. 디버깅과 개발 도구
Dev Mode 활성화
# 개발 모드로 빌드
npm run dev
디버깅 팁
import { createSignal, createEffect } from "solid-js";
function DebugComponent() {
const [count, setCount] = createSignal(0);
// Signal 변화 추적
createEffect(() => {
console.log("count changed:", count());
});
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
{count()}
</button>
</div>
);
}
브라우저 확장 프로그램
현재 Solid.js 전용 DevTools는 제한적이지만, 다음 도구들을 활용할 수 있습니다:
- React DevTools (구조 확인용으로 부분 지원)
- Chrome Performance 탭 (성능 프로파일링)
- Solid DevTools (개발 중)
13. 초보자가 자주 하는 실수!
실수 1: Signal을 함수로 안 부르기
이거 정말 많이 하는 실수예요. React useState와 비슷하다고 생각해서 괄호 없이 쓰다가 당황하게 되죠.
// ❌ 틀림
const [count, setCount] = createSignal(0);
console.log(count); // [Function]이 출력돼요
return <div>{count}</div>; // 함수 객체가 표시됨 😱
// ✅ 맞음
console.log(count()); // 0이 출력돼요
return <div>{count()}</div>; // 값이 표시됨 ✨
왜 이렇게 했을까? Signal이 함수인 이유는 자동 의존성 추적 때문이에요. count()를 호출하면 Solid가 “아, 이 부분이 count를 사용하는구나”하고 알아챕니다.
실수 2: Effect 안에서 async/await 바로 쓰기
// ❌ 틀림 - 이렇게 하면 반응성이 제대로 동작 안 해요
createEffect(async () => {
const data = await fetch('/api/users');
console.log(data);
});
// ✅ 방법 1: 내부 함수로 감싸기
createEffect(() => {
(async () => {
const data = await fetch('/api/users');
const json = await data.json();
console.log(json);
})();
});
// ✅✅ 방법 2: createResource 쓰기 (더 좋아요!)
const [users] = createResource(async () => {
const data = await fetch('/api/users');
return data.json();
});
// 로딩 상태도 자동으로 처리돼요
return (
<Show when={!users.loading} fallback={<div>불러오는 중...</div>}>
<div>{users().length}명의 사용자</div>
</Show>
);
실수 3: Store를 직접 수정하기
import { createStore } from "solid-js/store";
const [todos, setTodos] = createStore([
{ id: 1, text: "Solid.js 배우기", done: false }
]);
// ❌ 틀림 - 반응성이 깨져요
todos[0].done = true; // 화면이 안 바뀌어요!
todos.push({ id: 2, text: "앱 만들기" }); // 이것도 안 돼요
// ✅ 맞음 - setState 함수 사용하기
setTodos(0, "done", true); // 첫 번째 할 일 완료 처리
setTodos([...todos, { id: 2, text: "앱 만들기" }]); // 새 할 일 추가
// ✅✅ 더 편한 방법 - 조건으로 찾아서 수정
setTodos(
(todo) => todo.id === 1, // 조건
"done", // 속성
true // 값
);
실수 4: For를 안 쓰고 map 쓰기
// ❌ 틀림 - 비효율적이에요
function TodoList() {
const [todos, setTodos] = createSignal([...]);
return (
<ul>
{todos().map(todo => (
<li>{todo.text}</li>
))}
</ul>
);
}
// ✅ 맞음 - For 컴포넌트 쓰기
import { For } from "solid-js";
function TodoList() {
const [todos, setTodos] = createSignal([...]);
return (
<ul>
<For each={todos()}>
{(todo) => <li>{todo.text}</li>}
</For>
</ul>
);
}
왜 For가 더 좋아요? map은 배열이 바뀔 때마다 전체를 다시 렌더링하는데, For는 변경된 항목만 업데이트해요. 특히 항목이 많을 때 엄청난 차이가 나요!
실수 5: 불필요한 createEffect 남발
// ❌ 틀림 - Effect가 필요 없어요
const [firstName, setFirstName] = createSignal("홍");
const [lastName, setLastName] = createSignal("길동");
const [fullName, setFullName] = createSignal("");
createEffect(() => {
setFullName(firstName() + lastName()); // 불필요한 Signal!
});
// ✅ 맞음 - createMemo나 파생 함수 쓰기
const [firstName, setFirstName] = createSignal("홍");
const [lastName, setLastName] = createSignal("길동");
// 방법 1: 그냥 함수
const fullName = () => firstName() + lastName();
// 방법 2: createMemo (계산 비용이 클 때)
const fullName = createMemo(() => firstName() + lastName());
언제 Effect를 써야 하나요?
- DOM 직접 조작할 때
- 콘솔에 로그 찍을 때
- 외부 라이브러리 연동할 때
- 로컬 스토리지 저장할 때
Signal 값을 다른 Signal에 복사하는 용도로는 쓰지 마세요!
14. 같이 쓰면 좋은 라이브러리들
Solid.js 생태계는 아직 React만큼 크지는 않지만, 필요한 것들은 거의 다 있어요. 제가 써본 것 중에 괜찮은 것들을 소개해드릴게요.
UI 컴포넌트 라이브러리
Hope UI (https://hope-ui.com/)
- React의 Chakra UI와 비슷해요
- 다크 모드 지원이 정말 편해요
- 접근성도 잘 되어 있어요
Solid UI (https://www.solid-ui.com/)
- Shadcn/ui 스타일의 컴포넌트
- 복사해서 붙여넣기 방식이라 커스터마이징이 쉬워요
Kobalte (https://kobalte.dev/)
- 접근성(Accessibility)이 중요하면 이걸 추천해요
- 스크린 리더 지원이 완벽해요
폼 만들 때
Modular Forms (https://modularforms.dev/)
- 폼 유효성 검증이 필요할 때 써보세요
- React Hook Form과 비슷하게 쓸 수 있어요
- 타입 안정성도 좋아요
npm install @modular-forms/solid
import { createForm } from "@modular-forms/solid";
function LoginForm() {
const [loginForm, { Form, Field }] = createForm();
return (
<Form>
<Field name="email">
{(field, props) => (
<input {...props} type="email" placeholder="이메일" />
)}
</Field>
<button type="submit">로그인</button>
</Form>
);
}
스타일링
Tailwind CSS – 가장 추천해요!
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
UnoCSS – 더 빠른 Tailwind 대안
- 빌드 속도가 엄청 빨라요
- Tailwind와 문법이 거의 똑같아요
상태를 localStorage에 저장하기
npm install @solid-primitives/storage
import { makePersisted } from "@solid-primitives/storage";
// 자동으로 localStorage에 저장돼요
const [theme, setTheme] = makePersisted(
createSignal("light"),
{ name: "theme" }
);
// 페이지를 새로고침해도 유지돼요!
애니메이션
Solid Transition Group
npm install solid-transition-group
import { TransitionGroup } from "solid-transition-group";
<TransitionGroup name="slide-fade">
<For each={items()}>
{(item) => <div>{item}</div>}
</For>
</TransitionGroup>
테스팅
npm install --save-dev @solidjs/testing-library @testing-library/jest-dom vitest
import { render, screen } from "@solidjs/testing-library";
import { expect, test } from "vitest";
test("버튼 클릭하면 숫자 증가", () => {
const { container } = render(() => <Counter />);
const button = screen.getByRole("button");
expect(button).toHaveTextContent("0");
button.click();
expect(button).toHaveTextContent("1");
});
아이콘
Solid Icons (https://solid-icons.vercel.app/)
npm install solid-icons
import { FiHeart } from 'solid-icons/fi'; // Feather Icons
import { AiFillHeart } from 'solid-icons/ai'; // Ant Design
<FiHeart size={24} color="#FF0000" />
차트 그리기
Solid ChartJS
npm install chart.js solid-chartjs
Solid ApexCharts – 더 예쁜 차트
npm install apexcharts solid-apexcharts
import { render, screen } from "@solidjs/testing-library";
import { expect, test } from "vitest";
import Counter from "./Counter";
test("카운터 증가", async () => {
const { unmount } = render(() => <Counter />);
const button = screen.getByRole("button");
expect(button).toHaveTextContent("0");
button.click();
expect(button).toHaveTextContent("1");
unmount();
});
15. 커뮤니티와 학습 리소스
공식 리소스
- 공식 웹사이트: https://www.solidjs.com/
- 공식 문서: https://docs.solidjs.com/
- 인터랙티브 튜토리얼: https://www.solidjs.com/tutorial
- 플레이그라운드: https://playground.solidjs.com/
- GitHub: https://github.com/solidjs/solid
커뮤니티
- Discord: Solid 공식 Discord 서버 (가장 활발)
- Reddit: r/solidjs
- Stack Overflow: #solidjs 태그
한국어 리소스
현재 한국어 리소스는 제한적이지만, 공식 문서는 번역이 진행 중입니다. 커뮤니티에서 한국 사용자들이 점점 늘어나고 있습니다.
16. Solid.js 2.0: 미래를 향한 발전
Solid 2.0 개발이 진행 중이며, 여러 핵심 영역의 개선이 예정되어 있습니다. 현재 #next 브랜치에서 개발이 진행되고 있으며, 다음과 같은 목표를 가지고 있습니다:
주요 개선 사항
- 향상된 Props 처리: 더 직관적인 props destructuring
- 부분 Hydration: Islands Architecture 개선
- 서버 컴포넌트: React Server Components와 유사한 기능
- 향상된 스트리밍 SSR: 더 빠른 초기 로딩
- 개선된 TypeScript 지원: 더 나은 타입 추론
개발 진행 상황은 GitHub Discussion에서 확인할 수 있습니다.
17. 2025년, Solid.js를 주목해야 하는 이유
2025년 현재 웹 개발 트렌드를 보면 몇 가지 중요한 변화가 있습니다:
1. 모바일 퍼스트 시대 전 세계 웹 트래픽의 60%가 모바일에서 발생합니다. 가벼운 번들 크기와 빠른 로딩이 필수이며, Solid.js의 11kb 번들은 이상적입니다.
2. 성능이 곧 경쟁력 Google은 Core Web Vitals를 검색 순위 요소로 사용합니다. 페이지 로딩 속도가 1초 느려지면 전환율이 7% 감소한다는 연구 결과도 있습니다.
3. 개발자 경험 중시 배우기 쉽고, 생산성이 높은 도구가 선호됩니다. Solid.js는 React 경험자라면 하루 만에 배울 수 있습니다.
4. 실시간 애플리케이션 증가 채팅, 협업 도구, 대시보드 등 실시간 업데이트가 많은 앱이 증가하고 있으며, Fine-Grained Reactivity가 이에 최적화되어 있습니다.
마무리하며
Solid.js는 단순히 “빠른 프레임워크”를 넘어서, 웹 개발을 좀 더 즐겁게 만들어주는 도구예요. Virtual DOM 없이도 더 효율적일 수 있다는 걸 증명했고, React를 써본 분들이라면 정말 쉽게 배울 수 있어요.
물론 React를 당장 버리라는 건 아니에요. React는 여전히 훌륭한 프레임워크고, 수많은 라이브러리와 리소스가 있으니까요. 다만 새로운 프로젝트를 시작하거나 성능이 중요한 서비스를 만든다면, Solid.js를 한 번쯤 고려해보면 좋을 것 같아요.
도움이 될 만한 팁
빠르게 시작하기:
# 새 프로젝트 만들기
npm create solid@latest my-app
# 개발 서버 시작
npm run dev
# 빌드하기
npm run build
막힐 때는:
- 공식 Discord에 물어보세요 (친절해요!)
- 공식 문서의 예제 코드를 참고하세요
- Playground에서 실험해보세요
기억하면 좋은 것:
- Signal은 꼭 함수로 호출하기:
count()⭕ /count❌ - Effect는 꼭 필요할 때만 쓰기
- For 컴포넌트로 리스트 렌더링하기
다음 프로젝트에서 Solid.js를 한 번 써보시는 건 어떨까요? 공식 튜토리얼 30분이면 기본은 다 배울 수 있어요. 성능 차이를 직접 느껴보시면 깜짝 놀라실 거예요! 😊