Angular 코드에서 어떤 상태를 저장하려면 signal 함수를 사용하면 됩니다.
때로는 다른 상태에 종속된 상태를 관리해야 할 수도 있습니다.
사용자가 배송 방식을 선택하는 컴포넌트를 예로 들어 봅시다:
@Component({/* ... */})
export class ShippingMethodPicker {
shippingOptions: Signal<ShippingMethod[]> = getShippingOptions();
// 기본 배송 방법을 지정합니다.
selectedOption = signal(this.shippingOptions()[0]);
changeShipping(newOptionIndex: number) {
this.selectedOption.set(this.shippingOptions()[newOptionIndex]);
}
}
이 예제에서 selectedOption은 기본값이 설정되어 있으며, 사용자의 선택에 따라 값이 변경됩니다.
그런데 shippingOptions는 시그널이기 때문에 값이 변경될 수 있습니다!
shippingOptions 값이 변경되면 selectedOption 값은 더 이상 유효하지 않습니다.
이때 linkedSignal 함수를 사용하면, 다른 상태와 직접 연결되는 시그널을 생성할 수 있습니다.
위 예제 코드에서 signal을 linkedSignal로 바꿔 보세요:
@Component({/* ... */})
export class ShippingMethodPicker {
shippingOptions: Signal<ShippingMethod[]> = getShippingOptions();
// 기본 배송방법을 지정합니다.
selectedOption = linkedSignal(() => this.shippingOptions()[0]);
changeShipping(index: number) {
this.selectedOption.set(this.shippingOptions()[index]);
}
}
linkedSignal은 signal과 비슷하지만 다른 점이 하나 있습니다.
linkedSignal은 기본값을 직접 받지 않고 computed() 함수처럼 계산 함수 를 통해 전달받습니다.
그리고 계산 값이 변경되면 linkedSignal의 값도 새로 계산된 값으로 변경됩니다.
이제 linkedSignal의 값은 언제나 유효한 값을 유지합니다.
아래 예제를 보며 linkedSignal 값이 연결 상태에 따라 어떻게 변경되는지 확인해 보세요:
const shippingOptions = signal(['Ground', 'Air', 'Sea']);
const selectedOption = linkedSignal(() => shippingOptions()[0]);
console.log(selectedOption()); // 'Ground'
selectedOption.set(shippingOptions()[2]);
console.log(selectedOption()); // 'Sea'
shippingOptions.set(['Email', 'Will Call', 'Postal service']);
console.log(selectedOption()); // 'Email'
이전 상태 참조하기
경우에 따라 linkedSignal이 이전 값을 참조해야 하는 경우가 있습니다.
위 예제에서 shippingOptions 목록이 변경되면 selectedOption의 값은 언제나 첫번째 항목으로 변경됩니다.
하지만 사용자가 선택한 옵션이 새 목록에 남아 있다면 선택을 유지하고 싶을 수 있습니다.
그러면 linkedSignal을 생성할 때 소스(source) 와 연산(computation) 을 구분하는 방식을 사용하면 됩니다:
interface ShippingMethod {
id: number;
name: string;
}
@Component({/* ... */})
export class ShippingMethodPicker {
constructor() {
this.changeShipping(2);
this.changeShippingOptions();
console.log(this.selectedOption()); // {"id":2,"name":"Postal Service"}
}
shippingOptions = signal<ShippingMethod[]>([
{id: 0, name: 'Ground'},
{id: 1, name: 'Air'},
{id: 2, name: 'Sea'},
]);
selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({
// `selectedOption`의 값은 `source`가 바뀌었을 때 `computation` 결과 값으로 할당됩니다.
source: this.shippingOptions,
computation: (newOptions, previous) => {
// newOptions에 이전 값이 전달되면 이전 값을 유지합니다.
// 아니면 첫번째 옵션 값을 선택합니다.
return newOptions.find((opt) => opt.id === previous?.value.id) ?? newOptions[0];
},
});
changeShipping(index: number) {
this.selectedOption.set(this.shippingOptions()[index]);
}
changeShippingOptions() {
this.shippingOptions.set([
{id: 0, name: 'Email'},
{id: 1, name: 'Sea'},
{id: 2, name: 'Postal Service'},
]);
}
}
linkedSignal을 생성할 때 연산 함수를 전달하는 대신, 객체를 전달하면서 이 객체의 프로퍼티로 source와 computation 옵션을 전달할 수 있습니다.
source에는 computed() 시그널이나 컴포넌트 input 등 어떤 시그널이든 사용할 수 있습니다. source가 변경되거나 computation에서 참조한 시그널이 변경되면, linkedSignal은 제공된 computation의 실행 결과로 값을 갱신합니다.
그리고 computation은 source에서 새 값을 받으면서 previous 객체를 받는 함수입니다.
이때 previous 객체에는 source 프로퍼티와 value 프로퍼티가 있습니다.
previous.source 프로퍼티에는 이전 source 값이 전달되며, previous.value에는 linkedSignal의 이전 값이 전달됩니다.
연산 함수는 이 값들을 활용해서 새 값을 만들어 내면 됩니다.
도움말: previous 인자를 사용할 때는 linkedSignal의 제네릭 타입을 명시적으로 지정해야 합니다.
제네릭의 첫 번째 타입은 source로 전달되는 타입이며, 두 번째 타입은 computation 연산 결과의 타입입니다.
동일성 평가 함수 커스터마이징
linkedSignal은 다른 시그널과 마찬가지로 커스텀 동일성 평가 함수를 지정할 수 있습니다.
이 함수는 linkedSignal 값이 변경되었는지 판단하는 용도로 사용됩니다:
const activeUser = signal({id: 123, name: 'Morgan', isAdmin: true});
const activeUserEditCopy = linkedSignal(() => activeUser(), {
// 사용자의 id가 같으면 같은 사용자로 판단합니다.
equal: (a, b) => a.id === b.id,
});
// `source`와 `computation`을 각각 지정합니다.
const activeUserEditCopy = linkedSignal({
source: activeUser,
computation: (user) => user,
equal: (a, b) => a.id === b.id,
});
set() 커스터마이징하기
linkedSignal의 set 메서드나 update 메서드의 동작이 linkedSignal 자체의 값을 변경하지 않고 원본 데이터를 변경하는 경우가 있다고 합시다.
이런 경우에는 linkedSignal을 생성할 때 옵션 객체의 set 프로퍼티로 커스텀 함수를 전달해서 동작을 커스터마이징할 수 있습니다.
커스텀 set 함수는 인자를 2개 받습니다:
- 변경할 새 값
rawSet함수 -linkedSignal내부 상태를 직접 갱신하는 함수
참고: rawSet 함수를 사용하면 linkedSignal 내부값을 직접 갱신할 수 있습니다.
이 방식은 계산이 복잡하거나 이미 결과를 알고 있는 경우를 건너뛰는 방식으로 유용합니다.
원본 시그널의 값을 변경하기
화면에 사용하는 시그널은 화씨 시그널이지만, 원본 데이터는 섭씨 시그널인 경우를 생각해 봅시다:
const tempC = signal(0);
const tempF = linkedSignal(() => (tempC() * 9) / 5 + 32, {
set: (valF) => tempC.set(((valF - 32) * 5) / 9),
});
console.log(tempF()); // 32
// 화씨 값을 변경하면 원래 시그널인 섭씨 시그널 값을 변경합니다.
tempF.set(212);
console.log(tempC()); // 100
console.log(tempF()); // 212
부모 객체의 프로퍼티 값 변경하기
부모 객체의 프로퍼티 값을 변경하는 경우도 고려해봅시다. 대상 객체는 원본 시그널에 있습니다:
interface Order {
id: number;
shippingMethod: string;
}
const order = signal<Order>({
id: 42,
shippingMethod: 'Ground',
});
const shippingMethod = linkedSignal(() => order().shippingMethod, {
set: (newMethod) => {
// 객체 참조를 유지하면서 내부 값을 변경합니다.
order.update((currentOrder) => ({
...currentOrder,
shippingMethod: newMethod,
}));
},
});
console.log(shippingMethod()); // 'Ground'
// shippingMethod 값을 변경하면서 부모 객체의 프로퍼티 값을 갱신합니다.
shippingMethod.set('Air');
console.log(order()); // { id: 42, shippingMethod: 'Air' }
console.log(shippingMethod()); // 'Air'