NgRx Signal Store vs 기존 NgRx: 2026년 어떤 것을 선택해야 할까요?

NgRx Signal Store와 기존 NgRx를 비교 분석하여 2026년 Angular 프로젝트에 최적의 상태 관리 솔루션을 선택하는 방법을 안내합니다.

NgRx Signal Store vs 기존 NgRx: 2026년 어떤 것을 선택해야 할까요?

NgRx Signal Store는 Angular 상태 관리의 근본적인 패러다임 전환을 나타냅니다. 전통적인 Redux 스타일의 액션-리듀서-셀렉터 패턴에서 시그널 기반의 반응형 접근 방식으로 전환하여 개발자 경험을 크게 변화시켰습니다. 이 글에서는 2026년 Angular 개발자가 적절한 아키텍처 결정을 내릴 수 있도록 두 솔루션을 상세히 비교합니다.

빠른 결정 가이드

Signal Store는 기능 수준의 상태 관리와 중간 복잡도의 애플리케이션에서 보일러플레이트 감소가 중요한 경우에 적합합니다. 기존 NgRx는 엄격한 감사 추적, 복잡한 부수 효과, DevTools를 통한 상세한 디버깅이 필요한 엔터프라이즈 애플리케이션에 적합합니다.

아키텍처의 근본적인 차이점

기존 NgRx는 Redux 패턴을 따릅니다. 액션이 이벤트를 설명하고, 리듀서가 새로운 불변 상태를 생성하며, 셀렉터가 데이터를 추출합니다. 모든 상태 변경은 예측 가능하고 추적 가능한 파이프라인을 통해 흐릅니다.

Signal Store는 다른 접근 방식을 채택합니다. 리듀서를 통해 액션을 디스패치하는 대신, 상태가 반응형 시그널로 존재합니다. 업데이트는 patchState()를 호출하는 메서드를 통해 수행되며, 파생 값은 메모이제이션된 셀렉터가 아닌 계산된 시그널에서 가져옵니다.

| 관점 | 기존 NgRx | Signal Store | |------|-----------|---------------| | 멘탈 모델 | Redux/Flux | 반응형 서비스 | | 보일러플레이트 | 많음 (액션, 리듀서, 셀렉터, 이펙트) | 적음 (상태, 메서드, computed) | | 반응성 | RxJS Observables | Angular Signals | | DevTools | 완전한 Redux DevTools | Signal Store DevTools (플러그인 경유) | | 학습 곡선 | 가파름 | 중간 | | 번들 크기 | 큼 | 작음 |

Angular Signals에 익숙한 개발자에게 Signal Store는 프레임워크의 반응형 프리미티브의 자연스러운 확장처럼 느껴집니다.

기존 NgRx: Angular의 Redux 패턴

기존 NgRx는 액션, 리듀서, 셀렉터, 이펙트를 중심으로 상태 관리를 구성합니다. 이러한 분리는 장황함을 대가로 예측 가능성을 강제합니다.

books.actions.tstypescript
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Book } from './book.model';

export const BooksActions = createActionGroup({
  source: 'Books',
  events: {
    'Load Books': emptyProps(),
    'Load Books Success': props<{ books: Book[] }>(),
    'Load Books Failure': props<{ error: string }>(),
    'Add Book': props<{ book: Book }>(),
    'Remove Book': props<{ id: string }>(),
  },
});

createActionGroup API는 관련 액션을 단일 소스 아래에 그룹화하여 DevTools의 가독성을 향상시키고, 개별 createAction 호출에 비해 보일러플레이트를 줄입니다.

books.reducer.tstypescript
import { createReducer, on } from '@ngrx/store';
import { BooksActions } from './books.actions';
import { Book } from './book.model';

export interface BooksState {
  books: Book[];
  loading: boolean;
  error: string | null;
}

const initialState: BooksState = {
  books: [],
  loading: false,
  error: null,
};

export const booksReducer = createReducer(
  initialState,
  on(BooksActions.loadBooks, (state) => ({
    ...state,
    loading: true,
    error: null,
  })),
  on(BooksActions.loadBooksSuccess, (state, { books }) => ({
    ...state,
    books,
    loading: false,
  })),
  on(BooksActions.loadBooksFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error,
  }))
);

리듀서는 새로운 상태 객체를 반환하는 순수 함수로 유지됩니다. on() 함수는 액션 매칭과 상태 업데이트를 간결하게 처리합니다.

books.selectors.tstypescript
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { BooksState } from './books.reducer';

export const selectBooksState = createFeatureSelector<BooksState>('books');

export const selectAllBooks = createSelector(
  selectBooksState,
  (state) => state.books
);

export const selectBooksLoading = createSelector(
  selectBooksState,
  (state) => state.loading
);

export const selectPublishedBooks = createSelector(
  selectAllBooks,
  (books) => books.filter((book) => book.published)
);

셀렉터는 자동으로 메모이제이션을 제공합니다. selectPublishedBooks 셀렉터는 books 배열이 변경될 때만 재계산되므로 자주 업데이트되는 스토어에서도 읽기가 효율적입니다.

NgRx Signal Store: 시그널 우선 상태 관리

Signal Store는 상태, 계산된 값, 메서드를 단일 주입 가능한 서비스에 통합합니다. 공식 NgRx 문서에서는 "Angular Signals를 네이티브로 지원하는 완전한 기능의 상태 관리 솔루션"으로 설명합니다.

books.store.tstypescript
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
import { BooksService } from './books.service';
import { Book } from './book.model';

interface BooksState {
  books: Book[];
  loading: boolean;
  error: string | null;
}

const initialState: BooksState = {
  books: [],
  loading: false,
  error: null,
};

export const BooksStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withComputed(({ books }) => ({
    publishedBooks: computed(() => books().filter((book) => book.published)),
    totalBooks: computed(() => books().length),
  })),
  withMethods((store, booksService = inject(BooksService)) => ({
    async loadBooks() {
      patchState(store, { loading: true, error: null });
      try {
        const books = await booksService.getAll();
        patchState(store, { books, loading: false });
      } catch (error) {
        patchState(store, { loading: false, error: 'Failed to load books' });
      }
    },
    addBook(book: Book) {
      patchState(store, ({ books }) => ({ books: [...books, book] }));
    },
    removeBook(id: string) {
      patchState(store, ({ books }) => ({
        books: books.filter((book) => book.id !== id),
      }));
    },
  }))
);

전체 기능의 상태 관리가 하나의 파일에 담깁니다. 상태 속성이 자동으로 시그널이 되고, 계산된 시그널이 셀렉터를 대체하며, 메서드가 액션 디스패치를 대체합니다.

withEntities를 활용한 엔티티 관리

두 접근 방식 모두 엔티티 컬렉션을 지원하지만, Signal Store의 withEntities 기능은 코드량을 극적으로 줄입니다.

books-entity.store.tstypescript
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods } from '@ngrx/signals';
import {
  addEntity,
  removeEntity,
  setAllEntities,
  updateEntity,
  withEntities,
} from '@ngrx/signals/entities';
import { BooksService } from './books.service';
import { Book } from './book.model';

export const BooksEntityStore = signalStore(
  { providedIn: 'root' },
  withEntities<Book>(),
  withComputed(({ entities }) => ({
    publishedBooks: computed(() => entities().filter((book) => book.published)),
  })),
  withMethods((store, booksService = inject(BooksService)) => ({
    async loadBooks() {
      const books = await booksService.getAll();
      patchState(store, setAllEntities(books));
    },
    addBook(book: Book) {
      patchState(store, addEntity(book));
    },
    updateBook(id: string, changes: Partial<Book>) {
      patchState(store, updateEntity({ id, changes }));
    },
    removeBook(id: string) {
      patchState(store, removeEntity(id));
    },
  }))
);

withEntities 기능은 ids, entityMap, entities 시그널을 자동으로 제공합니다. addEntity, removeEntity, updateEntity와 같은 엔티티 업데이터가 컬렉션 변경을 처리합니다.

Angular 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

재사용성을 위한 커스텀 스토어 기능

Signal Store의 signalStoreFeature 함수를 통해 횡단 관심사를 추출하여 스토어 간에 재사용할 수 있습니다.

with-request-status.tstypescript
import { computed } from '@angular/core';
import { signalStoreFeature, withComputed, withState } from '@ngrx/signals';

type RequestStatus = 'idle' | 'pending' | 'fulfilled' | 'error';

export function withRequestStatus() {
  return signalStoreFeature(
    withState<{ requestStatus: RequestStatus }>({ requestStatus: 'idle' }),
    withComputed(({ requestStatus }) => ({
      isPending: computed(() => requestStatus() === 'pending'),
      isFulfilled: computed(() => requestStatus() === 'fulfilled'),
      isError: computed(() => requestStatus() === 'error'),
    }))
  );
}

export function setPending(): { requestStatus: RequestStatus } {
  return { requestStatus: 'pending' };
}

export function setFulfilled(): { requestStatus: RequestStatus } {
  return { requestStatus: 'fulfilled' };
}

export function setError(): { requestStatus: RequestStatus } {
  return { requestStatus: 'error' };
}

이 커스텀 기능은 로딩 상태 추적이 필요한 모든 스토어에 조합될 수 있습니다.

books-with-status.store.tstypescript
import { inject } from '@angular/core';
import { patchState, signalStore, withMethods } from '@ngrx/signals';
import { setAllEntities, withEntities } from '@ngrx/signals/entities';
import { withRequestStatus, setFulfilled, setPending, setError } from './with-request-status';
import { BooksService } from './books.service';
import { Book } from './book.model';

export const BooksStore = signalStore(
  { providedIn: 'root' },
  withEntities<Book>(),
  withRequestStatus(),
  withMethods((store, booksService = inject(BooksService)) => ({
    async loadBooks() {
      patchState(store, setPending());
      try {
        const books = await booksService.getAll();
        patchState(store, setAllEntities(books), setFulfilled());
      } catch {
        patchState(store, setError());
      }
    },
  }))
);

조합된 스토어는 로직을 중복하지 않고 isPending(), isFulfilled(), isError() 계산 시그널을 노출합니다.

컴포넌트 통합 패턴

Signal Store는 시그널 읽기를 통해 Angular의 템플릿 구문과 자연스럽게 통합됩니다.

books-list.component.tstypescript
import { Component, inject } from '@angular/core';
import { BooksStore } from './books.store';

@Component({
  selector: 'app-books-list',
  standalone: true,
  template: `
    @if (store.isPending()) {
      <div class="loading-spinner">Loading books...</div>
    }

    @if (store.isError()) {
      <div class="error-message">Failed to load books</div>
    }

    @for (book of store.entities(); track book.id) {
      <div class="book-card">
        <h3>{{ book.title }}</h3>
        <p>{{ book.author }}</p>
        <button (click)="store.removeBook(book.id)">Remove</button>
      </div>
    }

    <p>Total: {{ store.totalBooks() }} books</p>
  `,
})
export class BooksListComponent {
  readonly store = inject(BooksStore);

  constructor() {
    this.store.loadBooks();
  }
}

구독 관리가 필요 없습니다. async 파이프도 필요 없습니다. 시그널은 Angular의 zoneless 변경 감지와 통합되어 최적의 성능을 제공합니다.

성능 및 번들 크기 고려사항

Signal Store의 가벼운 아키텍처는 더 작은 번들로 이어집니다. 코어 @ngrx/signals 패키지는 gzip 압축 시 약 4KB를 추가하는 반면, 전체 기존 NgRx 스택(@ngrx/store, @ngrx/effects, @ngrx/entity)은 15-20KB입니다.

Signal Store는 Angular의 시그널 기반 변경 감지의 이점도 누립니다. 계산된 시그널은 의존성이 변경될 때만 재계산되며, 컴포넌트는 사용하는 시그널이 업데이트될 때만 다시 렌더링됩니다. 이러한 세분화된 반응성은 Observable 기반 패턴에서 일반적인 불필요한 변경 감지 주기를 제거합니다.

RxJS를 많이 사용하는 애플리케이션의 경우, 기존 NgRx의 Observable 기반 셀렉터가 기존 파이프라인과 원활하게 통합됩니다.

기존 방식에서 Signal Store로의 마이그레이션 전략

기존 NgRx 애플리케이션을 마이그레이션할 때 빅뱅 방식의 재작성은 필요하지 않습니다. 두 솔루션은 공존할 수 있어 점진적 채택이 가능합니다.

  • 새로운 기능은 Signal Store를 사용하여 시작합니다
  • 분리된 기능 모듈을 하나씩 마이그레이션합니다
  • 마이그레이션 비용이 이점을 초과하는 경우 복잡하고 이펙트가 많은 기능은 기존 NgRx를 유지합니다
  • NgRx Signals 상호 운용성 유틸리티를 사용하여 Observable 기반과 시그널 기반 상태 간을 연결합니다
공존이 가능합니다

기존 NgRx Store와 Signal Store는 동일한 애플리케이션에서 병행하여 실행할 수 있습니다. NgRx 팀은 두 접근 방식을 공식적으로 지원하며 둘 사이를 연결하기 위한 상호 운용성 유틸리티를 제공합니다.

각 접근 방식을 선택해야 할 때

Signal Store를 선택해야 할 때:

  • Angular 20 이상의 새 애플리케이션을 구축하는 경우
  • 기능 수준 또는 컴포넌트 수준의 상태를 관리하는 경우
  • 개발자 경험과 보일러플레이트 감소를 우선시하는 경우
  • 팀의 NgRx 경험이 제한적인 경우
  • 번들 크기가 중요한 경우

기존 NgRx를 선택해야 할 때:

  • 기존 NgRx 애플리케이션을 유지보수하는 경우
  • 엄격한 액션 로깅과 감사 추적이 필요한 경우
  • 광범위한 부수 효과가 있는 복잡한 워크플로우를 구축하는 경우
  • 완전한 Redux DevTools 타임 트래블 디버깅이 필요한 경우
  • 팀이 깊은 NgRx 전문 지식을 보유한 경우

상태 관리 기초를 이해하면 개발자는 다양한 Angular 코드베이스에 대비할 수 있습니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

결론

  • Signal Store는 기존 NgRx에 비해 상태 관리 보일러플레이트를 60-70% 줄입니다
  • 기존 NgRx는 엄격한 감사 추적과 복잡한 부수 효과가 필요한 엔터프라이즈 애플리케이션에 더 나은 선택입니다
  • 두 접근 방식은 동일한 애플리케이션에서 공존할 수 있어 점진적 마이그레이션이 가능합니다
  • Signal Store의 계산된 시그널은 명시적인 셀렉터 정의 없이 자동 메모이제이션을 제공합니다
  • 커스텀 스토어 기능(signalStoreFeature)을 통해 재사용 가능한 횡단 관심사를 구현할 수 있습니다
  • 2026년 Angular 20 이상의 새 프로젝트에서는 특정 요구사항이 기존 NgRx를 필요로 하지 않는 한 Signal Store가 기본 선택이 되어야 합니다

공유

관련 기사