포스트

[Angular 마스터하기] Day 6 - Signal 상태 관리, 반응형 데이터의 시작

[Angular 마스터하기] Day 6 - Signal 상태 관리, 반응형 데이터의 시작

이제와서 시작하는 Angular 마스터하기 - Day 6 “Signal로 데이터가 변하면 자동으로 화면이 업데이트되는 마법을 경험하세요! ⚡”

오늘 배울 내용

  • Signal이 무엇이고 왜 필요한지
  • signal(), computed() 기초
  • 기존 방식 vs Signal 비교
  • 실습: Signal로 카운터와 장바구니 만들기

1. Signal이 뭘까요?

문제 상황

기존 방식에서는 이런 문제가 있었어요:

1
2
3
4
5
6
7
8
9
10
// 😫 기존 방식
export class OldCounterComponent {
  count = 0;
  doubleCount = 0;

  increment() {
    this.count++;
    this.doubleCount = this.count * 2;  // ← 수동으로 업데이트 😓
  }
}

문제점:

  • count가 변경될 때마다 doubleCount수동으로 업데이트해야 함
  • 깜빡하면 버그 발생!
  • 복잡한 앱에서는 관리가 어려움

Signal의 해결책

1
2
3
4
5
6
7
8
9
10
11
12
// ✨ Signal 방식
import { Component, signal, computed } from '@angular/core';

export class NewCounterComponent {
  count = signal(0);
  doubleCount = computed(() => this.count() * 2);  // ← 자동 업데이트! ✨

  increment() {
    this.count.update(v => v + 1);
    // doubleCount는 자동으로 업데이트됨!
  }
}

장점:

  • ✅ 자동으로 의존성 추적
  • ✅ 자동으로 값 업데이트
  • ✅ 더 적은 코드, 더 적은 버그
graph LR
    A[Signal 값 변경] -->|자동 감지| B[Computed 재계산]
    B -->|자동 업데이트| C[화면 렌더링]

    style A fill:#dd0031,stroke:#fff,color:#fff
    style B fill:#667eea,stroke:#fff,color:#fff
    style C fill:#4CAF50,stroke:#fff,color:#fff

2. Signal 기본 사용법

Signal 생성하기

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
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-signal-basics',
  standalone: true,
  template: `
    <div class="demo">
      <h2>Signal 기초</h2>

      <!-- Signal 값 읽기: 함수처럼 () 붙이기 -->
      <p>이름: {{ name() }}</p>
      <p>나이: {{ age() }}</p>
      <p>이메일: {{ email() }}</p>

      <button (click)="updateInfo()">정보 업데이트</button>
    </div>
  `
})
export class SignalBasicsComponent {
  // Signal 생성: signal(초기값)
  name = signal('홍길동');
  age = signal(25);
  email = signal('hong@example.com');

  updateInfo() {
    // Signal 값 변경: set() 메서드
    this.name.set('김철수');
    this.age.set(30);
    this.email.set('kim@example.com');
  }
}

Signal 값 읽기와 쓰기

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
64
65
66
67
68
69
70
71
72
73
74
75
@Component({
  selector: 'app-signal-operations',
  standalone: true,
  template: `
    <div class="operations">
      <h2>카운터: {{ count() }}</h2>

      <div class="buttons">
        <button (click)="increment()">+1</button>
        <button (click)="decrement()">-1</button>
        <button (click)="double()">×2</button>
        <button (click)="reset()">리셋</button>
      </div>

      <p class="info">{{ message() }}</p>
    </div>
  `,

  styles: [`
    .operations {
      padding: 30px;
      background: #f5f5f5;
      border-radius: 15px;
      text-align: center;
    }
    h2 {
      font-size: 3em;
      color: #667eea;
    }
    .buttons {
      display: flex;
      gap: 10px;
      justify-content: center;
      margin: 20px 0;
    }
    button {
      padding: 12px 24px;
      font-size: 1.1em;
      border: none;
      border-radius: 8px;
      background: #667eea;
      color: white;
      cursor: pointer;
    }
    button:hover {
      background: #764ba2;
    }
  `]
})
export class SignalOperationsComponent {
  count = signal(0);
  message = signal('시작!');

  increment() {
    // update(): 현재 값 기반으로 업데이트
    this.count.update(value => value + 1);
    this.message.set(`${this.count()}로 증가!`);
  }

  decrement() {
    this.count.update(value => value - 1);
    this.message.set(`${this.count()}로 감소!`);
  }

  double() {
    this.count.update(value => value * 2);
    this.message.set(`${this.count()}로 두 배!`);
  }

  reset() {
    // set(): 새로운 값으로 직접 설정
    this.count.set(0);
    this.message.set('리셋됨!');
  }
}

객체와 배열 Signal

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
@Component({
  selector: 'app-object-signal',
  standalone: true,
  template: `
    <div class="user-profile">
      <h2>사용자 프로필</h2>

      <div class="profile-info">
        <p>이름: {{ user().name }}</p>
        <p>나이: {{ user().age }}</p>
        <p>도시: {{ user().city }}</p>
      </div>

      <button (click)="updateUser()">프로필 업데이트</button>

      <h3>취미</h3>
      <ul>
        @for (hobby of hobbies(); track hobby) {
          <li>{{ hobby }}</li>
        }
      </ul>

      <button (click)="addHobby()">취미 추가</button>
    </div>
  `
})
export class ObjectSignalComponent {
  // 객체 Signal
  user = signal({
    name: '홍길동',
    age: 25,
    city: '서울'
  });

  // 배열 Signal
  hobbies = signal(['독서', '운동', '코딩']);

  updateUser() {
    // 객체 업데이트: 스프레드 연산자 사용
    this.user.update(current => ({
      ...current,
      age: current.age + 1,
      city: '부산'
    }));
  }

  addHobby() {
    // 배열 업데이트: 스프레드 연산자 사용
    this.hobbies.update(current => [
      ...current,
      '여행'
    ]);
  }
}
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
---

## 3. Computed - 자동 계산되는 값

### Computed 기본

`computed()`는 다른 Signal들로부터 **자동으로 계산되는 값**입니다.


```typescript
import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-shopping-cart',
  standalone: true,
  template: `
    <div class="cart">
      <h2>쇼핑 카트</h2>

      <div class="product">
        <label>
          상품 가격:
          <input
            type="number"
            [value]="price()"
            (input)="updatePrice($event)">
        </label>
      </div>

      <div class="product">
        <label>
          수량:
          <input
            type="number"
            [value]="quantity()"
            (input)="updateQuantity($event)">
        </label>
      </div>

      <!-- Computed 값들이 자동으로 업데이트됨! -->
      <div class="summary">
        <p>소계: {{ subtotal() | number }}원</p>
        <p>부가세 (10%): {{ tax() | number }}원</p>
        <p>배송비: {{ shippingFee() | number }}원</p>
        <hr>
        <h3>총액: {{ total() | number }}원</h3>
      </div>
    </div>
  `,

  styles: [`
    .cart {
      max-width: 500px;
      margin: 0 auto;
      padding: 30px;
      background: white;
      border-radius: 15px;
      box-shadow: 0 4px 20px rgba(0,0,0,0.1);
    }
    .product {
      margin: 15px 0;
    }
    label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }
    input {
      width: 100%;
      padding: 10px;
      font-size: 1em;
      border: 2px solid #ddd;
      border-radius: 5px;
    }
    .summary {
      margin-top: 30px;
      padding: 20px;
      background: #f9f9f9;
      border-radius: 10px;
    }
    .summary p {
      margin: 10px 0;
      font-size: 1.1em;
    }
    .summary h3 {
      color: #667eea;
      font-size: 1.5em;
    }
    hr {
      margin: 15px 0;
      border: none;
      border-top: 2px solid #ddd;
    }
  `]
})
export class ShoppingCartComponent {
  // 입력 Signal
  price = signal(10000);
  quantity = signal(1);
  taxRate = signal(0.1);

  // Computed Signal들 - 자동으로 재계산됨!
  subtotal = computed(() => this.price() * this.quantity());

  tax = computed(() => this.subtotal() * this.taxRate());

  shippingFee = computed(() => {
    // 조건부 로직도 가능!
    return this.subtotal() > 50000 ? 0 : 3000;
  });

  total = computed(() => {
    // 다른 Computed Signal도 참조 가능!
    return this.subtotal() + this.tax() + this.shippingFee();
  });

  updatePrice(event: Event) {
    const value = +(event.target as HTMLInputElement).value;
    this.price.set(value);
  }

  updateQuantity(event: Event) {
    const value = +(event.target as HTMLInputElement).value;
    this.quantity.set(value);
  }
}

Computed의 장점

flowchart TD
    A[price Signal] --> D[subtotal Computed]
    B[quantity Signal] --> D
    D --> E[tax Computed]
    D --> F[shippingFee Computed]
    E --> G[total Computed]
    F --> G

    style A fill:#dd0031
    style B fill:#dd0031
    style D fill:#667eea,color:#fff
    style E fill:#667eea,color:#fff
    style F fill:#667eea,color:#fff
    style G fill:#4CAF50,color:#fff

특징:

  • 자동 의존성 추적: Signal이 변경되면 자동으로 재계산
  • 메모이제이션: 같은 입력이면 재계산 안 함 (성능 최적화)
  • 읽기 전용: Computed는 직접 변경 불가
  • 체인 가능: Computed가 다른 Computed를 참조 가능

4. 실전 예제: 할인 계산기

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import { Component, signal, computed } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

interface Product {
  name: string;
  price: number;
  quantity: number;
}

@Component({
  selector: 'app-discount-calculator',
  standalone: true,
  imports: [FormsModule, CommonModule],
  templateUrl: './discount-calculator.component.html',
  styleUrl: './discount-calculator.component.css'
})
export class DiscountCalculatorComponent {
  // 상품 목록 Signal
  products = signal<Product[]>([
    { name: '노트북', price: 1500000, quantity: 1 },
    { name: '마우스', price: 50000, quantity: 2 },
    { name: '키보드', price: 80000, quantity: 1 }
  ]);

  // 할인율 Signal
  discountPercent = signal(10);

  // 멤버십 여부 Signal
  isMember = signal(false);

  // 소계 (Computed)
  subtotal = computed(() => {
    return this.products().reduce((sum, product) => {
      return sum + (product.price * product.quantity);
    }, 0);
  });

  // 할인 금액 (Computed)
  discountAmount = computed(() => {
    const discount = this.subtotal() * (this.discountPercent() / 100);

    // 멤버십 추가 할인
    if (this.isMember()) {
      return discount + (this.subtotal() * 0.05);  // +5% 추가 할인
    }

    return discount;
  });

  // 부가세 (Computed)
  tax = computed(() => {
    const afterDiscount = this.subtotal() - this.discountAmount();
    return afterDiscount * 0.1;
  });

  // 배송비 (Computed)
  shippingFee = computed(() => {
    const afterDiscount = this.subtotal() - this.discountAmount();
    return afterDiscount > 100000 ? 0 : 3000;
  });

  // 최종 금액 (Computed)
  finalTotal = computed(() => {
    return this.subtotal()
      - this.discountAmount()
      + this.tax()
      + this.shippingFee();
  });

  // 절약 금액 (Computed)
  savings = computed(() => {
    return this.discountAmount();
  });

  // 절약 비율 (Computed)
  savingsPercent = computed(() => {
    if (this.subtotal() === 0) return 0;
    return (this.savings() / this.subtotal()) * 100;
  });

  updateQuantity(index: number, event: Event) {
    const value = +(event.target as HTMLInputElement).value;

    this.products.update(products => {
      return products.map((product, i) => {
        if (i === index) {
          return { ...product, quantity: value };
        }
        return product;
      });
    });
  }

  removeProduct(index: number) {
    this.products.update(products => {
      return products.filter((_, i) => i !== index);
    });
  }

  addProduct() {
    this.products.update(products => [
      ...products,
      { name: '새 상품', price: 10000, quantity: 1 }
    ]);
  }
}

discount-calculator.component.html

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
<div class="calculator">
  <h1>💰 할인 계산기</h1>

  <!-- 상품 목록 -->
  <div class="products">
    <h2>장바구니</h2>

    @for (product of products(); track $index; let i = $index) {
      <div class="product-item">
        <div class="product-info">
          <strong>{{ product.name }}</strong>
          <span class="price">{{ product.price | number }}원</span>
        </div>

        <div class="product-controls">
          <input
            type="number"
            min="1"
            [value]="product.quantity"
            (input)="updateQuantity(i, $event)"
            class="quantity-input">

          <button (click)="removeProduct(i)" class="remove-btn">🗑️</button>
        </div>

        <div class="product-total">
          합계: {{ product.price * product.quantity | number }}원
        </div>
      </div>
    } @empty {
      <p class="empty">장바구니가 비어있습니다.</p>
    }

    <button (click)="addProduct()" class="add-btn">+ 상품 추가</button>
  </div>

  <!-- 할인 설정 -->
  <div class="discount-settings">
    <h2>할인 설정</h2>

    <label class="slider-label">
      할인율: {{ discountPercent() }}%
      <input
        type="range"
        min="0"
        max="50"
        [value]="discountPercent()"
        (input)="discountPercent.set(+($event.target as HTMLInputElement).value)"
        class="slider">
    </label>

    <label class="checkbox-label">
      <input
        type="checkbox"
        [checked]="isMember()"
        (change)="isMember.set(!isMember())">
      멤버십 회원 (추가 5% 할인)
    </label>
  </div>

  <!-- 금액 요약 -->
  <div class="summary">
    <h2>금액 요약</h2>

    <div class="summary-row">
      <span>상품 소계</span>
      <span>{{ subtotal() | number }}원</span>
    </div>

    <div class="summary-row highlight">
      <span>할인 금액</span>
      <span class="discount">-{{ discountAmount() | number }}원</span>
    </div>

    @if (isMember()) {
      <div class="member-badge">
        ⭐ 멤버십 추가 할인 적용됨!
      </div>
    }

    <div class="summary-row">
      <span>부가세 (10%)</span>
      <span>{{ tax() | number }}원</span>
    </div>

    <div class="summary-row">
      <span>배송비</span>
      <span>
        @if (shippingFee() === 0) {
          <span class="free">무료</span>
        } @else {
          {{ shippingFee() | number }}원
        }
      </span>
    </div>

    <hr>

    <div class="summary-row total">
      <span>최종 결제 금액</span>
      <span>{{ finalTotal() | number }}원</span>
    </div>

    <div class="savings-info">
      <p class="savings-amount">
        💰 {{ savings() | number }}원 절약!
      </p>
      <p class="savings-percent">
        ({{ savingsPercent() | number:'1.1-1' }}% 할인)
      </p>
    </div>
  </div>
</div>
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
### discount-calculator.component.css

```css
.calculator {
  max-width: 800px;
  margin: 50px auto;
  padding: 30px;
  background: white;
  border-radius: 20px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}

h1 {
  text-align: center;
  color: #333;
  margin-bottom: 30px;
}

h2 {
  color: #667eea;
  margin-bottom: 20px;
  font-size: 1.3em;
}

/* 상품 목록 */
.products {
  margin-bottom: 30px;
  padding: 20px;
  background: #f9f9f9;
  border-radius: 15px;
}

.product-item {
  background: white;
  padding: 15px;
  margin-bottom: 10px;
  border-radius: 10px;
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 15px;
}

.product-info {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.price {
  color: #666;
  font-size: 0.9em;
}

.product-controls {
  display: flex;
  gap: 10px;
  align-items: center;
}

.quantity-input {
  width: 60px;
  padding: 8px;
  border: 2px solid #ddd;
  border-radius: 5px;
  font-size: 1em;
  text-align: center;
}

.remove-btn {
  background: none;
  border: none;
  font-size: 1.2em;
  cursor: pointer;
  opacity: 0.6;
}

.remove-btn:hover {
  opacity: 1;
}

.product-total {
  font-weight: bold;
  color: #667eea;
  text-align: right;
}

.empty {
  text-align: center;
  color: #999;
  padding: 40px;
}

.add-btn {
  width: 100%;
  padding: 12px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1em;
  font-weight: bold;
  cursor: pointer;
  margin-top: 10px;
}

.add-btn:hover {
  background: #764ba2;
}

/* 할인 설정 */
.discount-settings {
  margin-bottom: 30px;
  padding: 20px;
  background: #f9f9f9;
  border-radius: 15px;
}

.slider-label {
  display: block;
  margin-bottom: 20px;
  font-weight: bold;
}

.slider {
  width: 100%;
  margin-top: 10px;
}

.checkbox-label {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: bold;
  cursor: pointer;
}

.checkbox-label input {
  width: 20px;
  height: 20px;
  cursor: pointer;
}

/* 금액 요약 */
.summary {
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 15px;
}

.summary h2 {
  color: white;
}

.summary-row {
  display: flex;
  justify-content: space-between;
  padding: 12px 0;
  font-size: 1.1em;
}

.summary-row.highlight {
  background: rgba(255, 255, 255, 0.1);
  padding: 12px 15px;
  border-radius: 8px;
  margin: 10px 0;
}

.discount {
  color: #ffeb3b;
  font-weight: bold;
}

.member-badge {
  background: rgba(255, 255, 255, 0.2);
  padding: 10px;
  border-radius: 8px;
  text-align: center;
  margin: 10px 0;
  font-weight: bold;
}

.free {
  color: #4CAF50;
  font-weight: bold;
}

hr {
  border: none;
  border-top: 2px solid rgba(255, 255, 255, 0.3);
  margin: 15px 0;
}

.summary-row.total {
  font-size: 1.5em;
  font-weight: bold;
  padding-top: 15px;
}

.savings-info {
  text-align: center;
  margin-top: 20px;
  padding: 15px;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 10px;
}

.savings-amount {
  font-size: 1.3em;
  font-weight: bold;
  margin: 0;
}

.savings-percent {
  font-size: 1em;
  opacity: 0.9;
  margin: 5px 0 0 0;
}

🧪 직접 해보기

실습 1: BMI 계산기

미션: 키와 몸무게를 입력하면 BMI가 자동 계산되는 앱을 만드세요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export class BmiCalculatorComponent {
  height = signal(170);  // cm
  weight = signal(70);   // kg

  // BMI = 체중(kg) / (신장(m) * 신장(m))
  bmi = computed(() => {
    const heightInMeters = this.height() / 100;
    return this.weight() / (heightInMeters * heightInMeters);
  });

  category = computed(() => {
    const value = this.bmi();
    if (value < 18.5) return '저체중';
    if (value < 23) return '정상';
    if (value < 25) return '과체중';
    return '비만';
  });
}

실습 2: 환율 계산기

미션: 원화를 달러, 엔, 유로로 자동 변환하는 계산기를 만드세요!

1
2
3
4
5
6
7
8
9
10
11
12
13
export class CurrencyConverterComponent {
  krw = signal(10000);

  exchangeRates = {
    usd: 0.00075,  // 1원 = 0.00075달러
    jpy: 0.11,     // 1원 = 0.11엔
    eur: 0.00069   // 1원 = 0.00069유로
  };

  usd = computed(() => this.krw() * this.exchangeRates.usd);
  jpy = computed(() => this.krw() * this.exchangeRates.jpy);
  eur = computed(() => this.krw() * this.exchangeRates.eur);
}

💡 자주 하는 실수

❌ 실수 1: Signal 읽을 때 () 빼먹기

1
2
3
4
5
<!-- ❌ 틀린 예 -->
<p>{{ count }}</p>

<!-- ✅ 올바른 예 -->
<p>{{ count() }}</p>
1
2
3
4
5
6
7
8
9
10
### ❌ 실수 2: Computed를 직접 수정하려고 함

```typescript
// ❌ 에러 발생!
const total = computed(() => price() * quantity());
total.set(100);  // ❌ Computed는 읽기 전용!

// ✅ Signal을 수정하면 Computed는 자동 업데이트
price.set(50);  // total이 자동으로 재계산됨!

❌ 실수 3: 객체/배열 수정 시 불변성 무시

1
2
3
4
5
6
7
8
// ❌ 틀린 예 - 직접 수정 (감지 안 됨)
this.user().age = 30;

// ✅ 올바른 예 - 새 객체 생성
this.user.update(current => ({
  ...current,
  age: 30
}));

📝 정리

Signal vs 기존 방식

항목 기존 방식 Signal
값 읽기 count count()
값 쓰기 count = 10 count.set(10)
계산된 값 수동 업데이트 computed() 자동
의존성 추적 수동 자동 ✨
성능 전체 재렌더링 필요한 부분만

핵심 API

```typescript // Signal 생성 const count = signal(0);

// 값 읽기 count()

// 값 설정 count.set(10)

// 값 업데이트 count.update(v => v + 1)

// Computed 생성 const double = computed(() => count() * 2)

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.