본문 바로가기

Web + APP/Angular

NGRX - Selectors

반응형
SMALL

https://ggodong.tistory.com/311

 

NGRX - Reducers

https://ggodong.tistory.com/307 NGRX - Actions 안녕하세요 ! 꼬동입니다. 그.... 이번에 한 번 진짜 ! 각 잡고 ! NGRX 훑어보기 할려고 합니다. 이번엔 진심 !! 꾸준히 !! 할거야 !! 우선 유튜브 하나만 보고 N..

ggodong.tistory.com

 

Actions => Reducers 했으니, 이제 Selectors를 해봅시다.

 

여기까지가 제가 생각하기엔 NGRX의 기본이라고 생각합니답.

 

기본 다 떼보죠.


Selectors는 store state의 일부분을 사용하기 위한 순수 함수입니다.

 

@ngrx/store는 위와 같은 역할을 돕는 최적화된 함수입니다.

 

state를 select하기 위해서 Selectors는 다양한 기능들을 제공해줍니다.

 

  • Portability
  • Memoization
  • Componsition
  • Testability
  • Type Safety

 

createSelector와 createFeatureSelector를 사용할 때 @ngrx/store는 selector 함수가 방출한  최신 arguments를 추적을 합니다.

 

왜냐하면, selector가 순수함수이기 때문에, arguments들이 매칭이 되면, 함수를 실행하지 않고, 마지막 결과가 리턴될 수 있습니다.

 

이와 같은 방법으로, 성능도 좋아질 수 있죠. 요 명칭이 메모이제이션으로 많이 알려져있습니다.

 


Using a selector for one piece of state

import { createSelector } from '@ngrx/store';

export interface FeatureState {
	counter: number;
}

export interface AppState {
	feature: FeatureState;
}

export selectFeature = (state: AppState) => state.feature;

export const selectFeatureCount = createSelector(
	selectFeature,
    (state: FeatureState) => state.counter
);

 

Using selectors for multiple pieces of state

createSelector는 state로부터 몇 가지 데이터를 가져오기 위해서 사용되는 함수입니다.

 

createSelector는 함수는 8개의 selector 함수를 가질 수 있습니다.

 

예를 들어, selectedUser라는 객체가 state에 있다고 상상을 해봅시다. 그리고 allBooks라는 책 객체의 배열을 또 가졌다고 생각을 해봅시다.

 

그리고 현재 유저를 위해서 이 모든 책을 보여주고 싶다고 가정을 합시다.

 

createSelector를 사용하여, allBooks라는 배열이 업데이트가 되더라도, 항상 최신 상태를 유지하도록 만들고, 선택한 책이 있는 경우, 사용자의 책이 표시되도록하게 하고, 선택한 사용자가 없을 때, 모든 책이 표시되도록 만들어보면 아래와 같은 코드가 나올 겁니다.

 

해당 결과는 항상 최신을 유지하고, 필터링도 될 것입니다.

 

import { createSelector } from '@ngrx/store';

export interface User {
	id: number;
	name: string;
}

export interface Book {
	id: number;
	userId: number;
	name: string;
}

export interface AppState {
	selectedUser: User;
    allBooks: Book[];
}

export const selectUser = (state: AppState) => state.selectedUser;
export const selectAllBooks = (state: AppState) => state.allBooks;

export const selectVisibleBooks = createSelector(
	selectUser,
    selectAllBooks,
    (selectedUser: User, allBooks: Book[]) => {
    	if (selectedUser && allBooks) {
        	return allBooks.filter((book: Book) => book.userId === selectedUser.id);
		} else {
        	return allBooks;
        }
    }
)

 

Using selectors with props

만약, selector로 값을 가져오면서, props를 넘기면서 가져온 value를 입 맛대로 바꿀 수 있는데, counter를 가져오면서, 이를 곱한다고 생각을 해봅시다.

 

export const getCount = createSelector(
	getCounterValue
    (counter, props) => counter * props.multiply
);

///////////////////////////////////////////////////

ngOnInit() {
	this.counter = this.store.select(fromRoot.getCount, { multiply: 2 })
}

 

이런식으로 곱해진 값을 들고 올 수 있습니다.

 

selector는 오직 이전 input arguments를 캐시하는 것을 알아야하는데, 만약, 해당 selector를 다른 multiply의 값으로 재사용을 한다고 했을 때, 이는 메모이제이션이 되지 않습니다.

 

그래서, 한 번은 2 / 한 번은 4를 호출한다고 했을 때, 이를 메모이제이션을 하기 위해선 selector를 팩토리 함수안에 넣어서 다른 instance가 사용될 수 있도록 해야합니다.

 

아래와 같이 말이죠 !

 

export const getCount = () => createSelector(
	(state, props) => state.counter[props.id],
    (counter, props) => counter * props.multiply
);

//////////////////////////////////////////////////

ngOnInit() {
	this.counter2 = this.store.select(fromRoot.getCount(), { id: 'counter2', multiply: 2});
    this.counter4 = this.store.select(fromRoot.getCount(), { id: 'counter4', multiply: 4});
    this.counter6 = this.store.select(fromRoot.getCount(), { id: 'counter6', multiply: 6});
}

 

Selecting Feature States

createFeatureSelector는 top level feature state를 리턴하는 되게 편한 메소드입니다.

 

import { createSelector, createFeatureSelector } from '@ngrx/store';

export const featureKey = 'feature';

export interface FeatureState {
	counter: number;
}

export interface AppState {
	feature: FeatureState;
}

export const selectFeature = createFeatureSelector<AppState, FeatureState>(featureKey);

export const selectFeatureCount = createSelector(
	selectFeature,
    (state: FeatureState) => state.counter
);

////////////////////////////////////////////////////////////////////////////////////

// 아래와 같은 selector는 컴파일이 안되는데 왜냐면, fooFeatureKey('foo')는 AppState의 feature가 아니기 때문입니다.
export const selectFeature = createFeatureSelector<AppState, FeatureState>(fooFeatureKey);

 

Resetting Memoized Selectors

createSelector / createFeatureSelector를 통해 리턴되는 selector 함수는 처음엔 null로 초기화 된다.

 

그 후 selector 가 불리면, 기록된 값이 사용되는데 만약, selector가 같은 arguments로 불리면, 저장된 값이 사용되고, 그게 아니면, 새로 계산해서 값을 가져온다.

 

import { createSelector } from '@ngrx/store';

export interface State {
	counter1: number;
    counter2: number;
}

export const selectCounter1 = (state: State) => state.counter1;
export const selectCounter2 = (state: State) => state.counter2;
export const selectTotal = createSelector(
	selectCounter1,
    selectCounter2,
    (counter1, counter2) => counter1 + counter2
); // selectTotal은 null을 기록합니답. 왜냐하면 아무도 안불렀으니까요.

let state = { counter1: 3, counter2: 4 };

selectTotal(state);
selectTotal(state); // 기록된 값이 사용됩니다.

state = { ...state, counter2: 5 };

selectToal(state); // 새로 계산됩니다.

 

selector의 저장된 값은 메모리에 무기한 저장됩니다. 예를들어, 기록된 값이 엄청 큰 데이터 셋이라면, 이를 다시 초기화 해야하는 방법이 필요할 수 있는데, 이 경우 release를 호출하면 됩니다.

 

selectTotal(state);
selectTotal.release();

 

selector를 release하는 건 reculsive하게 작동됩니다.

 

export interface State {
  evenNums: number[];
  oddNums: number[];
}

export const selectSumEvenNums = createSelector(
  (state: State) => state.evenNums,
  evenNums => evenNums.reduce((prev, curr) => prev + curr)
);
export const selectSumOddNums = createSelector(
  (state: State) => state.oddNums,
  oddNums => oddNums.reduce((prev, curr) => prev + curr)
);
export const selectTotal = createSelector(
  selectSumEvenNums,
  selectSumOddNums,
  (evenSum, oddSum) => evenSum + oddSum
);

selectTotal({
  evenNums: [2, 4],
  oddNums: [1, 3],
});

/**
 * Memoized Values before calling selectTotal.release()
 *   selectSumEvenNums  6
 *   selectSumOddNums   4
 *   selectTotal        10
 */

selectTotal.release();

/**
 * Memoized Values after calling selectTotal.release()
 *   selectSumEvenNums  null
 *   selectSumOddNums   null
 *   selectTotal        null
 */

 

Using Store Without Type Generic

store에서 정보를 가져오는 대부분의 방법은 createSelector와 함께 selector 함수를 사용하는 것입니다.

 

TS는 자동으로 createSelector에서 type을 자동으로 유추할 수 있으므로, generic argument와 같은 상태들을 store에게 제공하는 것을 줄일 수 있습니다.

 

그래서 Store를 component 혹은 다른 곳에 주입을 하는 경우, generic은 생략은 가능합니다.

 

만약 generice이 생략되었다면, 디폴트 generice이 적용됩니다. Store<T = object>

 

근데, 문자열 버전의 selector를 쓴다면, 이는 자동으로 추론이 불가능하므로, store에게 generic 타입을 제공해야합니다.

 

export class AppComponent {
	counter$ = this.store.select(fromCounter.selectCounter);
    
    constructor(private readonly store: Store) {}
}

 

위의 예제는 generic 없이 사용한 Store입니답.

 

만약 strict mode라면, select 메소드는 object로 부터 base selector가 통과되기를 기대할 것이라하네요. (뭔 말이지)

 

아래의 코드는 하나의 generic argument를 사용한 createFeatureSelector를 사용했을 때입니다.

 

import { createSelector, createFeatureSelector } from '@ngrx/store';

export const featureKey = 'feature';

export interface FeatureState {
	counter: number;
}

// selectFeature는 MemoizedSelector<object, FeatureState>를 가집니다.
export const selectFeature = createFeatureSelector<FeatureState>(featureKey);

// selectFeatureCount는 MemoizedSelector<object, number>를 가집니다.
export const selectFeatureCount = createSelector(
	selectFeature,
    state => state.counter
);

///////////////////////////////////////////////////////////////////////////////////////////////
export declare function createFeatureSelector<T>(featureName: string): MemoizedSelector<object, T>;
export declare function createFeatureSelector<T, V>(featureName: keyof T): MemoizedSelector<T, V>;

 

Advanced Usage

CQRS 아키텍쳐 패턴에서 NgRx는 reducers로부터 selectors를 분리합니다.

 

요 advanced 테크닉이 selector와 RxJS pipe operator를 합치는 것입니다.

 

쬐애금 고급 내용이라 번역을 통으로 하는 것보다, 코드를 보는게 좋아보여, 코드를 먼저 보도록 합시다.

 

import { map, filter } from 'rxjs/operators';

store
	.pipe(
    	map(state => selectValues(state)),
        filter(val => val !== undefined)
	)
    .subscribe(/* .. */);

 

이거를 아래와 같이 리팩토링이 가능합니다.

 

import { select } from '@ngrx/store';
import { map, filter } from 'rxjs/operators';

store
	.pipe(
    	select(selectValues),
        filter(val => val !== undefined)
	)
    .subscribe(/* .. */);

 

그래서 아래와 같이 코드를 짜면, state transition history를 유지할 수 있습니다. (1, 2, 3)을 stream으로 보내면 [1, 2, 3]으로 매핑이 됩니다.

 

export const selectProjectedValues = createSelector(
	selectFoo,
    selectBar,
    (foo, bar) => {
    	if (foo && bar) {
        	return { foo, bar };
		}
        
        return undefined;
	}
);


export const selectLastStateTransitions = (count: number) => {
	return pipe(
    	select(selectProjectedValues),
        scan((acc, curr) => {
        	return [ curr, ...acc].filter((val, index) => index < count && val !== undefined)
		}, [] as { fooL number, bar: string}[])
	);
}

이상 NGRX - Selectors 였습니다. ^_^

반응형
LIST

'Web + APP > Angular' 카테고리의 다른 글

Angular 더 빠르게 - On Push CD / Immutability  (0) 2022.03.23
constructor과 ngOnInit의 차이는 ?  (0) 2022.03.15
Normalizing State Shape  (0) 2021.11.08
NGRX - Reducers  (0) 2021.10.31
NGRX - Actions  (0) 2021.10.24