[React] preact 알아보기 (cc.Signals)

preact vs react 비교 및 signals 사용법 예제

Posted by lim.Chuck on March 19, 2025

[React]

  1. [React] vite React설치
  2. Project initial setting
  3. Hooks && Library
    1. [React] Javascript 파일을 Typescript Import해보자
    2. [React] react-router-dom 알아보고 사용하자
    3. [React] child 컴포넌트에서 함수호출하기
    4. [React] useCallback
    5. [React] framer-motion 알아보고 사용하자
    6. [React] Context API
    7. [React] react lazy 지연로딩 예제
    8. [React] react에서 캐싱처리를 위한 react queryd와 indexDB 비교분석
    9. [React] 스타일 라이브러리 styled-components stitches 비교
  4. [React] 프론트 에러추적 도구 Sentry 사용해보기
  5. [React] React 19 주요 변경점
  6. [React] DOM이란 무엇이고? 가상 DOM이란 무엇인가?
  7. [React] preact 알아보기

1. 🔄 React vs Preact 비교표

📊 기본 특성 비교

특성 React Preact
📦 번들 크기 약 40KB (gzip 압축 후) 약 3-4KB (gzip 압축 후)
💾 메모리 사용량 상대적으로 높음 낮음 (React의 약 1/3)
⚡ 렌더링 성능 우수 일부 시나리오에서 React보다 빠름
🏢 개발 지원 Meta(Facebook) 오픈소스 커뮤니티 중심
📈 학습 곡선 상대적으로 가파름 간단함
🔄 가상 DOM 완전한 구현 간소화된 구현
🖱️ 합성 이벤트 시스템 완전 지원 제한적 지원 (브라우저 네이티브 이벤트 사용)
⏱️ Concurrent Mode 지원 미지원
⏳ Suspense 지원 제한적 지원
🪝 Hooks 모든 훅 지원 대부분의 기본 훅 지원
🔄 Context API 지원 지원
🧩 Fragments 지원 지원

⚖️ 장단점 요약

라이브러리 주요 장점 주요 단점
⚛️ React • 🛠️ 풍부한 기능
• 🌐 거대한 생태계
• 🚀 지속적인 혁신
• 👥 강력한 커뮤니티 지원
• 🏢 대규모 앱에 적합
• 📦 상대적으로 큰 번들 사이즈
• 💾 높은 메모리 사용량
• 🧩 복잡한 API
• ⚙️ 초기 설정 복잡성
⚡ Preact • 🪶 매우 작은 번들 사이즈
• 🚀 높은 성능
• 💾 낮은 메모리 사용량
• 🔄 간단한 API
• ♻️ React 호환성
• ⚠️ 제한된 기능 세트
• 👪 작은 커뮤니티
• ⏱️ 최신 React 기능 지연
• 🏗️ 복잡한 앱에서 제한적

2.🔄 React에 없고 Preact만 있는 Signals: 상태 관리의 새로운 접근법

Preact는 React의 기본 기능을 넘어서는 몇 가지 고유한 기능이 있습니다. 그 중에서도 가장 주목할 만한 것은 Signals입니다!

Preact Signals이란?

Signals는 Preact 팀이 개발한 반응형 프리미티브로, React의 useState나 Context와는 다른 방식으로 상태를 관리합니다. 이는 상태 변경 시 전체 컴포넌트가 아닌 실제로 필요한 부분만 업데이트하는 더 세밀한 접근 방식을 제공합니다.

주요 장점:

  • 🎯 세밀한 업데이트: 가상 DOM 재조정을 건너뛰고 변경된 DOM 노드에 직접 업데이트
  • 성능 최적화: memo나 useMemo 없이도 렌더링 최적화
  • 🧩 컴포넌트 외부 상태: 컴포넌트 트리 외부에서 상태 정의 가능
  • 🔍 자동 의존성 추적: useEffect의 의존성 배열처럼 수동으로 의존성을 선언할 필요 없음

💻 Signals 설치 및 기본 사용법

1
2
3
4
# Preact Signals 설치
npm install @preact/signals
# 또는
yarn add @preact/signals

기본 사용 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { h } from "preact";
import { signal } from "@preact/signals";

// 컴포넌트 외부에서 상태 정의 가능
const count = signal(0);

function Counter() {
  // .value로 값에 접근하고 수정
  const increment = () => count.value++;

  return (
    <div>
      {/* 자동으로 값 변경 시 업데이트됨 */}
      <p>카운트: {count}</p>
      <button onClick={increment}>증가</button>
    </div>
  );
}

export default Counter;

🔧 고급 Signals 기능

1. computed Signals (계산된 시그널)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { h } from "preact";
import { signal, computed } from "@preact/signals";

const count = signal(0);
// computed는 의존성 시그널이 변경될 때만 재계산됨
const doubled = computed(() => count.value * 2);
const isEven = computed(() => count.value % 2 === 0);

function Counter() {
  return (
    <div>
      <p>카운트: {count}</p>
      <p>2배: {doubled}</p>
      <p>짝수?: {isEven ? "" : "아니오"}</p>
      <button onClick={() => count.value++}>증가</button>
    </div>
  );
}

2. effect 함수 (반응형 이펙트)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { h } from "preact";
import { signal, effect } from "@preact/signals";

const count = signal(0);
const name = signal("사용자");

// 의존하는 signal이 변경될 때마다 자동 실행
effect(() => {
  console.log(`${name.value}의 카운트: ${count.value}`);
  // 의존성 배열이 필요 없음!
});

function UserCounter() {
  return (
    <div>
      <input
        value={name.value}
        onInput={(e) => (name.value = e.target.value)}
      />
      <p>
        {name}의 카운트: {count}
      </p>
      <button onClick={() => count.value++}>증가</button>
    </div>
  );
}

3. batch 함수 (일괄 업데이트)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { h } from "preact";
import { signal, batch } from "@preact/signals";

const firstName = signal("");
const lastName = signal("길동");
const age = signal(30);

function UserProfile() {
  const updateUser = () => {
    // 여러 signal 업데이트를 하나의 렌더링으로 일괄 처리
    batch(() => {
      firstName.value = "";
      lastName.value = "철수";
      age.value = 25;
    });
  };

  return (
    <div>
      <p>
        이름: {firstName} {lastName}
      </p>
      <p>나이: {age}</p>
      <button onClick={updateUser}>업데이트</button>
    </div>
  );
}

🧰 Signals와 Hooks 함께 사용하기

Preact는 컴포넌트 내부에서 Signals를 더 쉽게 사용할 수 있는 훅을 제공합니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { h } from "preact";
import { useSignal, useComputed } from "@preact/signals";

function TodoApp() {
  // 컴포넌트 내부에서 Signal 사용
  const newTodo = useSignal("");
  const todos = useSignal([]);

  // 계산된 값
  const completedCount = useComputed(
    () => todos.value.filter((todo) => todo.completed).length
  );

  const addTodo = () => {
    if (newTodo.value.trim()) {
      todos.value = [
        ...todos.value,
        {
          text: newTodo.value,
          completed: false,
        },
      ];
      newTodo.value = "";
    }
  };

  const toggleTodo = (index) => {
    const newTodos = [...todos.value];
    newTodos[index].completed = !newTodos[index].completed;
    todos.value = newTodos;
  };

  return (
    <div>
      <h1>할 일 목록</h1>

      <div>
        <input
          value={newTodo.value}
          onInput={(e) => (newTodo.value = e.target.value)}
          placeholder="할 일 입력"
        />
        <button onClick={addTodo}>추가</button>
      </div>

      <ul>
        {todos.value.map((todo, i) => (
          <li
            key={i}
            onClick={() => toggleTodo(i)}
            style={{ textDecoration: todo.completed ? "line-through" : "none" }}
          >
            {todo.text}
          </li>
        ))}
      </ul>

      <div>
        완료: {completedCount} / {todos.value.length}
      </div>
    </div>
  );
}

🌐 전역 상태 관리 예제

Signals는 Redux나 Context API 없이도 전역 상태 관리를 간단하게 구현할 수 있습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// store.js - 전역 상태 정의
import { signal, computed } from "@preact/signals";

export const theme = signal("light");
export const user = signal({ name: "게스트", isLoggedIn: false });
export const cart = signal([]);

export const cartTotal = computed(() =>
  cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

export const login = (name) => {
  user.value = { name, isLoggedIn: true };
};

export const logout = () => {
  user.value = { name: "게스트", isLoggedIn: false };
};

export const toggleTheme = () => {
  theme.value = theme.value === "light" ? "dark" : "light";
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// App.js - 전역 상태 사용
import { h } from "preact";
import { theme, user, toggleTheme, login, logout } from "./store";

function App() {
  return (
    <div className={`app ${theme.value}`}>
      <header>
        {user.value.isLoggedIn ? (
          <button onClick={logout}>로그아웃 ({user.value.name})</button>
        ) : (
          <button onClick={() => login("사용자")}>로그인</button>
        )}
        <button onClick={toggleTheme}>
          {theme.value === "light" ? "🌙" : "☀️"}
        </button>
      </header>

      <main>
        <h1>Preact Signals 전역 상태 예제</h1>
        <p>현재 테마: {theme}</p>
        <p>사용자: {user.value.name}</p>
      </main>
    </div>
  );
}

⚖️ Signals vs React 상태 관리

특성 React 상태 관리 Preact Signals
세밀한 업데이트 컴포넌트 단위 값 단위
상태 위치 컴포넌트 내부 어디서나 가능
의존성 추적 수동(의존성 배열) 자동
상태 접근 컨텍스트/Props 직접 참조
최적화 방법 memo, useMemo 등 자동 최적화
코드 복잡성 상대적으로 높음 상대적으로 낮음

Preact의 Signals는 React의 훅 기반 접근법과는 달리, 더 간단하고 직관적인 API로 반응형 상태 관리를 제공합니다.
복잡한 최적화 없이도 높은 성능을 달성할 수 있어, 많은 개발자들이 Preact를 선택하는 주요 이유 중 하나입니다!