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

 

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는 개발할 때 정말 편하게 느껴지는데, 그 이유가 이 세 가지 원칙에 있어요:

  1. 컴포넌트는 한 번만 실행돼요: 처음 화면에 그려질 때 딱 한 번만 실행되고, 이후에는 변경된 부분만 업데이트됩니다. React처럼 컴포넌트 전체가 계속 다시 실행되지 않아요.
  2. 자동으로 연결돼요: 어떤 데이터를 사용하면 자동으로 그 데이터를 “구독”하게 되고, 데이터가 바뀔 때만 화면이 업데이트됩니다. 일일이 의존성을 신경 쓸 필요가 없어요.
  3. 읽기와 쓰기가 분리돼 있어요: 데이터를 읽는 부분과 수정하는 부분이 명확히 구분되어서, 코드를 읽을 때 “아, 이건 데이터를 읽는 거구나” “이건 수정하는 거구나” 바로 알 수 있죠.

이런 원칙 덕분에 코드가 직관적이고 버그도 적어요.

 

 

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() vs count
  • 컴포넌트 함수는 한 번만 실행되므로 성능 최적화가 자동으로 됩니다
  • useMemo, useCallback 같은 최적화 훅이 필요 없습니다

 

React(리액트)란? – 컴포넌트 기반 JavaScript 라이브러리, 쉽게 이해하기

 

 

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/about
    • users/[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로 옮기는 방법:

  1. 새로 추가하는 기능은 Solid.js로 만들어보기
  2. 성능이 특히 중요한 페이지부터 바꿔보기
  3. 마이크로 프론트엔드 방식으로 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. 커뮤니티와 학습 리소스

공식 리소스

커뮤니티

  • Discord: Solid 공식 Discord 서버 (가장 활발)
  • Reddit: r/solidjs
  • Stack Overflow: #solidjs 태그

한국어 리소스

현재 한국어 리소스는 제한적이지만, 공식 문서는 번역이 진행 중입니다. 커뮤니티에서 한국 사용자들이 점점 늘어나고 있습니다.

 

 

16. Solid.js 2.0: 미래를 향한 발전

Solid 2.0 개발이 진행 중이며, 여러 핵심 영역의 개선이 예정되어 있습니다. 현재 #next 브랜치에서 개발이 진행되고 있으며, 다음과 같은 목표를 가지고 있습니다:

주요 개선 사항

  1. 향상된 Props 처리: 더 직관적인 props destructuring
  2. 부분 Hydration: Islands Architecture 개선
  3. 서버 컴포넌트: React Server Components와 유사한 기능
  4. 향상된 스트리밍 SSR: 더 빠른 초기 로딩
  5. 개선된 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분이면 기본은 다 배울 수 있어요. 성능 차이를 직접 느껴보시면 깜짝 놀라실 거예요! 😊

 

 

댓글 남기기