Signal의 올바른 사용방법

Adam Kim·2026년 1월 30일

angular

목록 보기
96/102

Angular Signals 핵심 원리와 흔한 함정 파헤치기

Angular 17 이후, Signals는 프레임워크의 핵심적인 반응성(Reactivity) 모델로 자리 잡았습니다. 많은 개발자들이 "RxJS보다 쉽다"는 첫인상을 받지만, 실제 프로젝트에 적용하다 보면 예상치 못한 동작에 당황하는 경우가 많습니다. 바로 기존의 반응형 프로그래밍(RxJS 등)과 다른 작동 원리 때문입니다.

이번 글에서는 개발자들이 가장 흔하게 겪는 혼란의 원인을 분석하고, Signals의 핵심 작동 원리를 파헤쳐 올바른 사용법을 알아보겠습니다.

무엇이 문제인가? - 사고방식의 충돌

우리가 마주한 상황은 이렇습니다. 컴포넌트의 상태를 관리하기 위해 Signal을 선언하고, 특정 로직 안에서 값을 변경했습니다. 하지만 템플릿의 일부는 업데이트되는데, 다른 부분은 아무런 반응이 없습니다.

문제는 Signals를 RxJS의 Observable이나 일반 변수의 연장선으로 생각하는 기존의 사고방식에 있습니다.

  • Observable (스트림): 시간에 따라 여러 값을 방출하는 데이터의 폭포수. 구독(subscribe)을 통해 흐름에 연결해야 합니다.
  • Signal (상태 컨테이너): 특정 시점의 단일 값을 담고 있는 물컵. 값이 바뀌면 "물이 바뀌었다"고 알려줄 뿐, 흐름이라는 개념이 없습니다.

이 근본적인 차이를 인지하지 못하면, 왜 내 코드가 예상대로 동작하지 않는지 이해하기 어렵습니다.

우리의 목표: Signals가 Angular의 변경 감지 시스템과 어떻게 상호작용하는지 이해하고, computed와 effect의 역할을 명확히 구분하여 예측 가능한 코드를 작성하자!

해결의 실마리: Signals의 3가지 핵심 작동 원리

이 문제를 해결하려면 Signals가 단순한 '값'이 아니라 Angular 생태계 안에서 어떻게 작동하는지 이해해야 합니다.

1. Signals는 변경 감지를 우회하지 않는다

가장 큰 오해 중 하나는 "Signal 값만 바꾸면 Angular가 마법처럼 모든 것을 업데이트해 줄 것"이라는 믿음입니다. 사실 Signals는 Angular의 기존 변경 감지 시스템을 대체하는 것이 아니라, 더 효율적으로 만들어주는 도구입니다.

  • 템플릿 바인딩: {{ mySignal() }}처럼 템플릿에 직접 바인딩된 Signal이 변경되면, Angular는 해당 컴포넌트 또는 뷰만 다시 렌더링하도록 영리하게 최적화합니다.
  • 비연결 로직: 템플릿과 연결되지 않은 일반 함수나 로직 내부에서 Signal 값을 변경해도, Angular는 그 변화를 감지하여 화면을 업데이트할 의무가 없습니다.

2. computed는 '파생된 상태', effect는 '부수 효과'

두 함수를 혼용하는 것은 혼란을 가중시킬 뿐입니다. 역할이 완전히 다릅니다.

  • computed청사진 또는 계산 공식. 하나 이상의 Signal에 의존하여 새로운 '읽기 전용' Signal을 만듭니다. 순수(pure)해야 하며, 캐싱되어 의존성이 바뀔 때만 재계산됩니다. 새로운 상태 값을 만드는 데 사용합니다.
  • effect실행 명령 또는 알림. 의존하는 Signal이 변경될 때마다 특정 코드를 '실행'합니다. 값을 반환하지 않으며, 로깅, 데이터 저장, DOM 직접 조작 등 외부에 영향을 미치는 부수 효과에 사용합니다.

3. 참조가 바뀌어야 변화를 감지한다

Signals는 객체나 배열의 내부 속성이 바뀌는 것을 감지하지 못합니다. 오직 새로운 참조(Reference)로 교체될 때만 변화로 인식합니다.

  • set(value): 값을 완전히 새로운 참조로 교체합니다.
  • update(fn): 이전 값을 받아 새로운 값을 반환하여 교체합니다.

이 원리는 불변성(Immutability)을 지키는 것이 왜 중요한지 다시 한번 상기시켜 줍니다.

실제 구현: 흔한 실수와 올바른 사용법

이제 위 원리들을 바탕으로 실제 코드에서 발생하는 흔한 실수들을 살펴보겠습니다.

사례 1: 배열에 값을 추가했지만 화면이 바뀌지 않는 경우

const todos = signal<string[]>(['공부하기']);

// ❌ 잘못된 방법: Signal이 참조 변경을 감지하지 못함
function addTodoWrong(newTodo: string) {
  todos().push(newTodo); // 배열의 내부만 변경, 참조는 그대로
}

// ✅ 올바른 방법: 새로운 배열을 생성하여 참조를 교체
function addTodoRight(newTodo: string) {
  todos.update(currentTodos => [...currentTodos, newTodo]);
}

이유: update 콜백에서 스프레드 연산자(...)를 사용해 새로운 배열을 반환했기 때문에, todos Signal은 값이 변경되었음을 인지하고 UI를 업데이트합니다.

사례 2: computed가 필요할 때 effect를 사용한 경우

const firstName = signal('John');
const lastName = signal('Doe');
let fullName = ''; // 일반 변수

// ❌ 잘못된 방법: fullName은 반응성이 없으며, effect는 값을 반환하지 않음
effect(() => {
  fullName = `${firstName()} ${lastName()}`;
  console.log('이름이 변경됨:', fullName);
});

// ✅ 올바른 방법: 파생된 상태를 위해 computed 사용
const fullNameSignal = computed(() => `${firstName()} ${lastName()}`);

// 이제 템플릿에서 {{ fullNameSignal() }} 처럼 사용 가능

이유: computed는 fullNameSignal이라는 새로운 반응형 값을 만들어주므로, 템플릿이나 다른 Signal에서 재사용할 수 있습니다. effect는 단순히 특정 동작을 수행할 뿐입니다.

결론

Angular Signals는 RxJS를 대체하기 위한 기술이 아니라, 컴포넌트 상태를 직관적이고 효율적으로 관리하기 위한 새로운 패러다임입니다. 기존의 사고방식을 버리고 Signals의 핵심 원리를 이해하는 것이 중요합니다.

  • Signals는 스트림이 아닌 상태 컨테이너입니다.
  • Signals는 Angular의 변경 감지 시스템과 협력하여 동작합니다.
  • 불변성을 유지하여 참조를 교체해야 변화를 올바르게 감지합니다.

이 방법들을 통해 우리는 Signals를 활용하여 더 예측 가능하고 안정적인 애플리케이션을 구축할 수 있습니다. Signals는 올바른 상황에서 사용한다면 가장 강력한 도구가 될 것입니다.

Reference

The Most Misunderstood Concept in Angular Signals

profile
Angular2+ Developer

0개의 댓글