💻 프로그래밍

Solid.js

솔리드

Fine-grained Reactivity 기반의 선언적 UI 라이브러리. Virtual DOM 없이 컴파일 타임 최적화로 React와 유사한 문법에서 뛰어난 성능을 제공합니다.

📖 상세 설명

Solid.js는 현대적인 선언적 UI를 구축하기 위한 반응형(Reactive) JavaScript 라이브러리입니다. React와 유사한 JSX 문법과 컴포넌트 기반 아키텍처를 제공하면서도, 근본적으로 다른 반응성 모델을 사용합니다. 컴포넌트가 한 번만 실행되고 이후에는 Signal(시그널)이라는 세밀한 반응성 단위가 필요한 부분만 정확히 업데이트하는 방식으로 동작합니다. 이로 인해 불필요한 리렌더링 없이 매우 효율적인 UI 업데이트가 가능합니다.

Solid.js는 Ryan Carniato가 2018년부터 개발을 시작하여 2021년에 정식 릴리스되었습니다. Carniato는 MarkoJS 코어 팀 멤버이자 현재 Netlify의 엔지니어로, 10년 이상 반응성 시스템을 연구해왔습니다. 그의 철학은 "React의 개발자 경험과 Knockout.js의 성능을 결합"하는 것이었고, Solid.js는 이 목표를 달성한 결과물입니다. 벤치마크에서 Solid.js는 지속적으로 Vanilla JavaScript에 근접하는 성능을 보여주며, 다른 프레임워크들을 압도합니다.

Fine-grained Reactivity는 Solid.js의 핵심 개념입니다. React의 Virtual DOM은 상태 변경 시 컴포넌트 트리 전체를 비교하여 변경사항을 찾지만, Solid.js는 이런 과정이 필요 없습니다. Signal이 변경되면 해당 Signal을 구독하는 DOM 노드만 직접 업데이트됩니다. 이것은 컴파일 타임에 의존성 그래프가 분석되어 가능한 것으로, 런타임 오버헤드가 거의 없습니다. createSignal, createEffect, createMemo가 이 반응성 시스템의 기본 빌딩 블록입니다.

실무에서 Solid.js의 장점은 성능번들 사이즈입니다. Solid.js의 번들 크기는 약 7KB(gzip)로 React(~40KB)보다 훨씬 작습니다. 또한 JS Framework Benchmark에서 항상 최상위권을 기록하며, 특히 대규모 리스트 렌더링과 빈번한 업데이트에서 탁월한 성능을 발휘합니다. SolidStart라는 메타 프레임워크를 통해 SSR, 라우팅, API 엔드포인트 등 풀스택 기능도 지원합니다. React 개발자라면 문법이 익숙하여 학습 곡선이 낮으면서도 성능 이점을 얻을 수 있습니다.

💻 코드 예제

JSX - Signal과 기본 컴포넌트
import { createSignal } from 'solid-js';

// Signal: Solid.js의 반응형 상태
// getter 함수와 setter 함수를 반환 (React와 다름!)
function Counter() {
    // count는 함수! count()로 값을 읽음
    const [count, setCount] = createSignal(0);

    // 컴포넌트 함수는 한 번만 실행됨
    console.log('컴포넌트 생성 - 이 로그는 한 번만 출력!');

    return (
        <div>
            {/* count()를 호출해야 값을 읽을 수 있음 */}
            <p>카운트: {count()}</p>
            <button onClick={() => setCount(count() + 1)}>
                증가
            </button>
        </div>
    );
}

// React와의 차이점
// React: const [count, setCount] = useState(0); // count는 값
// Solid: const [count, setCount] = createSignal(0); // count는 함수
JSX - Effect와 Memo
import { createSignal, createEffect, createMemo } from 'solid-js';

function UserProfile() {
    const [firstName, setFirstName] = createSignal('김');
    const [lastName, setLastName] = createSignal('철수');

    // createMemo: 파생된 값을 캐싱 (React의 useMemo와 유사)
    // firstName이나 lastName이 변경될 때만 재계산
    const fullName = createMemo(() => {
        console.log('fullName 계산 중...');
        return `${firstName()} ${lastName()}`;
    });

    // createEffect: 부수 효과 처리 (React의 useEffect와 유사)
    // 의존성 배열이 필요 없음 - 자동으로 추적!
    createEffect(() => {
        // fullName()을 사용하면 자동으로 의존성이 추적됨
        console.log(`이름이 변경됨: ${fullName()}`);
        // 정리(cleanup) 함수
        onCleanup(() => console.log('정리 작업 실행'));
    });

    return (
        <div>
            <input
                value={firstName()}
                onInput={(e) => setFirstName(e.target.value)}
            />
            <input
                value={lastName()}
                onInput={(e) => setLastName(e.target.value)}
            />
            <h2>안녕하세요, {fullName()}님!</h2>
        </div>
    );
}
JSX - 조건부 렌더링과 리스트
import { createSignal, Show, For } from 'solid-js';

function TodoList() {
    const [todos, setTodos] = createSignal([
        { id: 1, text: 'Solid.js 배우기', done: false },
        { id: 2, text: '프로젝트에 적용하기', done: false }
    ]);
    const [showCompleted, setShowCompleted] = createSignal(true);

    const toggleTodo = (id) => {
        setTodos(todos().map(todo =>
            todo.id === id ? { ...todo, done: !todo.done } : todo
        ));
    };

    return (
        <div>
            <label>
                <input
                    type="checkbox"
                    checked={showCompleted()}
                    onChange={() => setShowCompleted(!showCompleted())}
                />
                완료된 항목 표시
            </label>

            {/* For: 효율적인 리스트 렌더링 (map 대신 사용) */}
            <For each={todos()}>
                {(todo) => (
                    {/* Show: 조건부 렌더링 (삼항연산자 대신 권장) */}
                    <Show when={showCompleted() || !todo.done}>
                        <div
                            style={{
                                'text-decoration': todo.done ? 'line-through' : 'none'
                            }}
                            onClick={() => toggleTodo(todo.id)}
                        >
                            {todo.text}
                        </div>
                    </Show>
                )}
            </For>
        </div>
    );
}

🗣 실무 대화 예시

👥 기술 선정 회의: React vs Solid.js
👨‍💼
테크 리드
새 대시보드 프로젝트의 프론트엔드 기술 스택을 결정해야 하는데요. Solid.js를 검토해봤는데 어떻게 생각하세요?
👩‍💻
시니어 개발자
성능 측면에서는 확실히 매력적이에요. 대시보드에 실시간 차트랑 데이터 테이블이 많을 텐데, Solid.js의 Fine-grained Reactivity는 대량 데이터 업데이트에 유리하거든요. 벤치마크 보면 React보다 몇 배 빠른 경우도 있어요.
👨‍💻
주니어 개발자
JSX 문법이 React랑 비슷해서 저도 빨리 적응할 수 있을 것 같아요. 근데 에코시스템이 걱정되는데... 차트 라이브러리나 UI 컴포넌트 지원이 충분할까요?
👩‍💻
시니어 개발자
좋은 지적이에요. 솔직히 React만큼 풍부하진 않아요. 하지만 SolidUI, Hope UI 같은 컴포넌트 라이브러리가 있고, 차트는 D3.js를 직접 통합하거나 solid-chartjs 같은 래퍼를 쓸 수 있어요. 핵심은 우리가 직접 만들어야 할 부분이 더 있을 수 있다는 거죠.
👨‍💼
테크 리드
채용 관점도 생각해야 해요. React 개발자는 많지만 Solid.js 경험자는 드물어요. 다만 React 경험자가 빠르게 배울 수 있다는 건 장점이네요. 성능이 정말 중요한 프로젝트고 팀이 새로운 기술 도입에 열려있다면 시도해볼 만하다고 봐요.

⚡ 성능 최적화 토론
👨‍💻
주니어 개발자
React에서 Solid.js로 마이그레이션하면서 이상한 점을 발견했어요. Signal에서 값을 구조분해하면 반응성이 깨지더라고요. const { name } = user() 이렇게 하면 안 되는 거죠?
👩‍💻
시니어 개발자
맞아요, Solid.js의 중요한 특징이에요. Signal의 값을 미리 꺼내면 그 시점의 값만 가져오고 반응성 연결이 끊어져요. JSX 안에서 {user().name}처럼 직접 접근해야 해요. 아니면 createMemo로 파생 Signal을 만들거나요.
👨‍💻
주니어 개발자
아, 그래서 For 컴포넌트에서 콜백 함수로 아이템을 받는 거군요? 아이템 자체가 Signal이 아니라 콜백 안에서 접근해야 반응성이 유지되는 거죠?
👩‍💻
시니어 개발자
정확해요! Solid.js에서 반응성은 함수 호출을 통해 추적돼요. 렌더링 시점에 어떤 Signal이 읽혔는지 기록하고, 그 Signal이 변하면 해당 부분만 업데이트하는 거죠. 이게 Virtual DOM 없이도 효율적인 업데이트가 가능한 비결이에요.

🎓 기술 면접: 프레임워크 비교
👨‍💼
면접관
Solid.js를 사용해본 경험이 있다고 하셨는데, React와 비교해서 가장 큰 차이점은 무엇인가요?
👩‍💻
지원자
가장 큰 차이는 반응성 시스템입니다. React는 상태 변경 시 컴포넌트 전체를 다시 렌더링하고 Virtual DOM으로 비교하지만, Solid.js는 Signal이 변경되면 해당 Signal을 사용하는 DOM 노드만 직접 업데이트합니다. 그래서 useMemo, useCallback 같은 최적화가 필요 없고, 성능이 기본적으로 좋습니다.
👨‍💼
면접관
Solid.js로 개발할 때 주의할 점이 있다면요?
👩‍💻
지원자
Signal 값을 구조분해하면 반응성이 끊어진다는 점입니다. React 습관대로 const { name } = user()하면 안 되고, JSX 안에서 user().name으로 접근해야 합니다. 또한 에코시스템이 React보다 작아서 직접 구현해야 할 부분이 있을 수 있습니다.

🔍 코드 리뷰: Signal 사용 패턴
👨‍💼
리뷰어
이 코드 보면 createSignal을 컴포넌트 안에서 매번 호출하고 있는데, 전역 상태는 어떻게 관리하고 있어요?
👩‍💻
작성자
아, 전역 상태는 별도 파일에서 Signal을 정의해서 export하고 있어요. Solid.js에서는 Signal이 컴포넌트 밖에서도 동작해서 별도 상태 관리 라이브러리가 필요 없거든요.
👨‍💼
리뷰어
좋아요. 한 가지 제안하자면, 이 computed 값들은 createMemo로 감싸면 캐싱이 되어서 불필요한 재계산을 피할 수 있어요. 지금은 매번 filter와 map을 실행하고 있거든요.

주의사항

🚨 Solid.js 실무 주의사항
  • 구조분해 할당 주의: Signal이나 props를 구조분해하면 반응성이 끊어집니다. const { name } = props 대신 props.name으로 접근하거나, splitProps() 유틸리티를 사용하세요.
  • JSX가 React와 다름: Solid.js의 JSX는 컴파일 타임에 실제 DOM 조작 코드로 변환됩니다. className 대신 class를 사용하고, 이벤트 핸들러는 onClick(대문자) 대신 onclick(소문자)도 지원합니다. style은 객체로 전달해야 합니다.
  • 컴포넌트는 한 번만 실행: React처럼 상태 변경 시 컴포넌트 함수 전체가 재실행되지 않습니다. 컴포넌트 본문의 로직은 마운트 시 한 번만 실행되므로, 조건부 로직은 JSX 내부나 createEffect 안에 배치하세요.
  • 생태계 규모: React에 비해 서드파티 라이브러리가 제한적입니다. 복잡한 폼 관리, 애니메이션 라이브러리 등은 직접 구현하거나 Vanilla JS 라이브러리를 래핑해야 할 수 있습니다.
  • 조건부 렌더링 방식: 삼항 연산자 {condition ? <A/> : <B/>} 대신 <Show> 컴포넌트 사용을 권장합니다. 삼항 연산자는 양쪽 모두 평가될 수 있어 의도치 않은 동작이 발생할 수 있습니다.

🔗 관련 용어

📚 더 배우기