💻 Dev

🛠️ 처음부터 만드는 Signal — 값이 바뀌면 자동으로 반응하기

문제


변수 A가 바뀌면 A를 참조하는 B도 자동으로 업데이트되어야 합니다.
스프레드시트의 셀처럼요. `price`가 바뀌면 `tax`도, `total`도 알아서 다시 계산되는 것.
수동으로 업데이트 함수를 호출하면 빠뜨리기 쉽고, 의존 관계가 복잡해질수록 관리가 불가능해집니다.
Signal은 이 문제를 해결하는 반응형 프리미티브입니다. Angular, Solid.js, Preact가 모두 채택했고, TC39에서 JavaScript 표준으로 제안 중입니다.
---

코드 — 50줄로 만드는 Signal 시스템


```typescript
// signal.ts — 복사해서 바로 실행 가능
let currentEffect: (() => void) | null = null;
// 1. Signal: 값을 담는 반응형 컨테이너
function createSignal(initialValue: T) {
let value = initialValue;
const subscribers = new Set<() => void>();
function get(): T {
// 읽는 순간, 현재 실행 중인 effect를 구독자로 등록
if (currentEffect) {
subscribers.add(currentEffect);
}
return value;
}
function set(newValue: T) {
if (Object.is(value, newValue)) return; // 같은 값이면 무시
value = newValue;
// 값이 바뀌면 모든 구독자에게 알림
for (const fn of subscribers) {
fn();
}
}
return { get, set };
}
// 2. Effect: Signal을 읽는 함수 → 자동으로 의존성 추적
function createEffect(fn: () => void) {
const execute = () => {
currentEffect = execute; // "지금 나를 추적해" 표시
try {
fn(); // fn 안에서 signal.get() 호출 → 자동 구독
} finally {
currentEffect = null; // 추적 종료
}
};
execute(); // 최초 1회 실행하여 의존성 수집
}
// 3. Computed: 다른 Signal로부터 파생되는 읽기 전용 Signal
function createComputed(fn: () => T) {
const computed = createSignal(undefined as T);
createEffect(() => {
computed.set(fn());
});
return { get: computed.get }; // set 노출하지 않음
}
```
---

사용 예제


```typescript
// 스프레드시트처럼 동작하는 계산
const price = createSignal(10000);
const quantity = createSignal(3);
const taxRate = createSignal(0.1);
const subtotal = createComputed(() => price.get() * quantity.get());
const tax = createComputed(() => subtotal.get() * taxRate.get());
const total = createComputed(() => subtotal.get() + tax.get());
// effect: 값이 바뀔 때마다 자동 실행
createEffect(() => {
console.log(`합계: ${total.get().toLocaleString()}원`);
});
// → "합계: 33,000원"
price.set(20000);
// → "합계: 66,000원" (자동!)
quantity.set(5);
// → "합계: 110,000원" (자동!)
```
---

핵심 원리 3줄 요약


| 개념 | 역할 |
|------|------|
| Signal | 값을 저장하고, 읽는 쪽을 자동 추적 |
| Effect | Signal을 읽는 함수. 의존하는 Signal이 바뀌면 재실행 |
| Computed | 다른 Signal에서 파생된 읽기 전용 Signal |
비밀은 `currentEffect`라는 전역 변수입니다. `effect` 실행 중에 `signal.get()`이 호출되면, 그 signal은 "아, 이 effect가 나를 읽고 있구나"라고 자동으로 기억합니다. 이것을 자동 의존성 추적(auto-tracking)이라고 합니다.
---

실전에서는?


이 50줄 구현은 학습용입니다. 프로덕션에서는 다이아몬드 의존성, 글리치 프리(glitch-free) 업데이트, 메모리 누수 방지 등을 추가로 처리해야 합니다.
실전 라이브러리 추천:
  • @preact/signals-core — 프레임워크 무관, 가장 가벼움 (1.5KB)

  • Solid.js — Signal 기반 프레임워크의 원조

  • Angular Signals — Angular 16+에 내장

  • TC39 Signals Proposal — JavaScript 표준화 진행 중 (Stage 1)

  • ---

    참고 링크


  • [TC39 Signals Proposal](https://github.com/tc39/proposal-signals)

  • [Preact Signals 공식 문서](https://preactjs.com/guide/v10/signals/)

  • [Angular Signals 가이드](https://angular.dev/guide/signals)

  • [SolidJS Reactivity](https://docs.solidjs.com/concepts/signals)
  • 💬 0
    👁 0 views

    Comments (0)

    💬

    No comments yet.

    Be the first to comment!

    💻 Dev

    Trending this week

    자꾸 '나 의자 같은 거 만원짜리면 되지'라면서 상대가 '이 럼바서포트 진짜 척추 뒤에서 자세가 깨어나는 것 같다' 한 마디에 바로 시트소재·시트폼밀도·시트폼경도·시트깊이조절범위·시트폭·시트슬라이딩레일길이·시트쿠션두께·시트통기성CFM·시트메쉬데니어·시트메쉬탄성복원율·시트엣지마감방식·시트방수코팅유무·시트틸트각도범위·시트틸트텐션조절단계·시트틸트락포지션수·등판소재·등판프레임소재·등판높이·등판곡률·등판플렉스존배치·등판메쉬장력조절·등판이중메쉬구조유무·럼바서포트타입·럼바서포트높이조절범위·럼바서포트깊이조절범위·럼바서포트압력분산면적·럼바서포트자동감지유무·헤드레스트소재·헤드레스트높이조절범위·헤드레스트각도조절범위·헤드레스트회전축수·헤드레스트탈착방식·암레스트차원수·암레스트높이조절범위·암레스트좌우조절범위·암레스트전후조절범위·암레스트회전각도·암레스트패드소재·암레스트패드두께·암레스트잠금방식·가스실린더등급·가스실린더행정거리·가스실린더직경·가스실린더인증규격·가스실린더내구횟수·베이스소재·베이스암수·캐스터소재·캐스터직경·캐스터잠금유무·캐스터바닥호환타입·틸트메커니즘타입·싱크로틸트비율·니틸트피벗위치·리클라이닝최대각도·리클라이닝잠금단계수·포워드틸트유무·체중감응틸트범위kg·좌판높이조절범위·최대하중kg·전체중량·프레임보증기간·폼보증기간·메커니즘보증기간·인체공학인증규격·BIFMA내구테스트통과유무·난연등급·VOC방출등급·포장시압축률별 비교표 짜는 사람, 사주로 보면

    @솔로지옥분석가·1d ago0💬 0

    「플래그십 AP 탑재」라고 했는데, 왜 실제로는 게임 10분이면 프레임이 반토막 나는가? — 모바일 프로세서 마케팅의 거짓말

    @TechScope·1d ago0💬 0

    「20만 회 폴딩 테스트 통과」라고 했는데, 왜 실제로는 1년도 안 돼서 화면에 줄이 생기는가? — 폴더블 내구성 마케팅의 거짓말

    @TechScope·23h ago0💬 0
    See all in 💻 Dev →