ngrx architecture를 이용하여 angular 앱을 더 기분 좋게 만들기

hyeonseok Ahn
Pagecall Engineering
12 min readDec 17, 2018

TL; DR

  • ngrx는 redux에서 영향을 받아 angular 상태 관리(state manage)를 담당한다.
  • ngrx의 selector메모이제이션(memoization) 기법으로 불필요한 데이터 갱신을 줄이고, ngrx의 entity와 연동하여 데이터를 정규화(normalize)하여 어플리케이션의 최적화를 수행한다.
  • ngrx의 effect부수효과(side-effects)를 관리하여 api 통신 등의 비동기 작업을 수행하고 이러한 부수효과에서 발생한 데이터들을 store state에 반영한다.

ngrx — state manage

angular 개발에서 state가 가지는 역할을 막중하다. 최종 사용자(end user)가 어플리케이션을 실행시킨 시점부터 state는 최종 사용자와 호흡을 같이한다. 초기 실행 단계에서 나타나는 화면부터 최종 사용자의 요청에 대한 결과를 보여주기까지 state는 쉴새없이 변화하고 값을 갱신하기 때문이다.

역할이 막중한 만큼 어플리케이션의 규모가 커질수록 state의 관리는 난해하고 복잡해진다. 사용자 반응에 따른 UI interaction도 보여주어야 하고, API로부터 받아온 데이터도 가공하여 보여주어야 한다. 어떤 경우에는 동일한 사용자 반응에도 어플리케이션의 사전 설정이나 데이터의 종류에 따라서 다른 UI/UX를 제공해주어야 하는 경우도 있다.

이쯤되면 어플리케이션 내부의 얼키서키 엉킨 컴포넌트가 서로 주고 받는 state는 그야말로 난장판이 되며, 개발자와 사용자 모두 혼란스러운 state 사이를 허우적대는 상황이 발생한다.

의미와 역할을 알 수 없는 state 로 가득찬 프론트엔드 앱을 사용하는 모습

ngrx는 이러한 state 를 관리하기 위해 redux 를 적용한 angular의 state manager이다. redux 아키텍쳐와 관련한 글은 많기 때문에 간단하게 설명하자면, 어플리케이션이 어떤 행동(action)발송(dispatch)하면 reducer라 불리우는 순수함수가 어플리케이션 전체 상태를 주시하는 상태저장소(store)상태(state)를 변경한다.

기존 redux에서는 이러한 일련의 과정을 위해 작성해야 하는 보일러플레이트 코드가 무척 많았지만, ngrx는 store 모듈의 action을 상속한 클래스 인스턴스를 생성하는 것만으로도 간단히 action을 만들 수 있으며 action의 dispatch 역시 store 모듈의 Store 클래스 인스턴스의 메소드 dispatch()를 이용하여 간단하게 사용이 가능하다.

이 글에서도 이야기하듯 PageCall 또한 보다 견고하고 안정적인 state 관리를 위하여 ngrx를 사용한다. 하지만 ngrx의 위력(?)은 단순히 state 관리에 그치지 않는다.

첫번째로, ngrx는 angular가 그러하듯 rxjs를 사용할 수 있다. 즉, action이 dispatch 되어 state가 업데이트 되면, 해당 state를 구독하는 Observable 의 도움으로 컴포넌트에서는 아름답게 가공된 state를 바로 사용하는 것이 가능해진다. rxjs의 operator를 선언적으로 사용하기 때문에 데이터 가공 과정이 직관적이고 유지보수의 비용이 획기적으로 줄어든다.

store state에서 관리하는 line$ 가 업데이트되면, rxjs operator 만으로 컴포넌트에서 사용가능한 데이터로 가공

두번째로, ngrx는 selectors 기능을 제공하여 angular의 렌더링 비용을 감소시키고 컴포넌트가 store state에 종속되지 않도록 한다.

ngrx — selectors

decouple views and action creators from state shape

redux의 아버지 Dan Abramov 는 다음과 같은 트윗을 남겼다.

Redux tip: reducers와 관련된 selectors를 export하면 store state의 모양(역주: 타입, key 이름 등)과 상관없이 views와 action creators를 다룰 수 있습니다.

앞서도 말했지만 selectors를 만들면, 컴포넌트를 store state와 분리하여 다룰 수 있다.

어플리케이션의 기능 추가 및 유지 보수로 store state의 모양은 변경될 가능성이 높다. 따라서 store state의 state 값을 그대로 컴포넌트에서 사용하는 경우, store state를 변경하면서 컴포넌트 역시 state를 변경해야 한다. 이 경우 변경하는 과정의 번거로움도 문제지만, state 변경이 어플리케이션의 예상치 못한 오작동을 발생시킬 수도 있다.

selectors 적용은 이런 문제를 미연에 방지한다. selectors는 특별히 어떤 라이브러리를 지칭하는 것이 아닌 state를 다루는 일종의 패턴이지만, ngrx의 selectors는 좀 더 특별한 기능을 하나 더 가지고 있다.

memoization

angular의 렌더링은 change detector를 통해 current state와 previous state 사이의 변경을 주시하다가 state 변경을 인지하면 리렌더링을 수행한다. 따라서 state 변경이 잦으면 잦을수록 angular 렌더링에 많은 자원을 소모한다. 최악의 경우, 가장 아래 단계에 위치한 컴포넌트의 state를 변경할 때 그 컴포넌트와 관계를 맺고 있는 모든 컴포넌트들의 리렌더링이 일어날 수 있다.

ngrx의 selectors는 memoization 기법으로 previous state 값을 cache에 기억하고 있다가 current state가 들어오면 자신이 기억하고 있는 previous state 의 값과 비교를 수행한다. 그 후 current state와 previous state의 값이 동일한 경우 selectors는 새로운 연산 수행 없이 자신이 cache에 기억했던 값을 반환한다.

다음 코드는 ngrx 공식 문서의 예제를 조금 수정한 것이다.

위 코드에서 알 수 있듯 각 selectors는 조합하여 다른 selectors를 만들어낼 수 있다.

다음 링크도 참조한다면 selectors의 효용성을 좀 더 이해할 수 있을 것이다.

https://stackoverflow.com/questions/51524963/ngrx-selector-vs-rxjs-operator

ngrx — entity

ngrx의 selectors 기능을 이용해 PageCall은 상당한 렌더링 최적화를 이룰 수 있었다. 하지만 PageCall에서 자유롭게 선을 그리거나 도형을 삽입하기 위해서는 렌더링 최적화 만으로는 부족한 감이 있다.

선을 긋거나 도형을 그리는 데에는 상당한 분량의 좌표 설정값이 필요한데, 그리기 및 삭제 이벤트가 발생했을 때마다 store state를 거쳐서 좌표값을 탐색하고 해당 좌표값에 다른 좌표값을 추가하거나 삭제하는 작업에는 많은 비용이 든다.

이런 데이터 탐색 작업의 최적화를 위해 ngrx는 entity 기능을 제공하여 데이터의 정규화(normalize)를 부분적으로 지원한다.

Normalizing State Shape

Managing Normalized Data

ngrx의 entity 기능은 normalizr 처럼 철저하게 데이터를 정규화하지 않는다. 하지만, 데이터를 entity처럼 다룰 수 있게 하여 데이터의 id로 데이터를 탐색할 수 있게 하여 데이터 탐색의 시간 복잡도 비용을 해시 테이블과 동일한 O(1)로 만들어준다.

추가적으로 ngrx의 entity는 adapter를 제공하여 spread operator나 Object.assign() 의 도움 없이 reducer가 store state를 immutable하게 업데이트 할 수 있도록 해준다. 이런 adapter의 도움으로 reducer를 다룰 때 entity의 복잡한 내부 구조나 깊이를 고려하지 않고 store state를 갱신할 수 있으니 결과적으로는 정규화가 이루어진 데이터를 다루는 것과 같은 효과를 얻는다.

ngrx — effect

대부분의 경우, 어플리케이션 개발에서는 부수효과(side effect)가 발생하지 않도록 고려하여 개발을 하는 것이 중요하다. 부수효과는 어플리케이션의 흐름을 추적하기 어렵게 할 뿐만 아니라 예기치 못한 버그를 만들어낼 수 있기 때문이다.

하지만 하나의 어플리케이션에 모든 기능과 필요한 데이터 자료를 전부 담기란 현실적으로 불가능하다. 결국 어플리케이션은 외부로부터 영향을 받아 어플리케이션 내부에는 존재하지 않던 데이터를 받아오고 해당 데이터를 다루어야 하는 상황, 즉 부수효과를 발생시켜야 하는 시점이 반드시 일어난다.

이것은 angular 개발에도 예외는 아니다. 하지만 state 의 역할이 중요한 만큼 부수효과가 컴포넌트의 state에 영향을 미치지 않도록 특별 관리를 해줄 필요성이 있다. ngrx의 effect가 빛을 발하는 시점이다.

엄밀히 말해 effect는 state 관리 측면에서는 하는 일이 없다. effect를 쓰든 안 쓰든 state 관리에는 문제가 없기 때문이다. 하지만 이대로는 ngrx라는 아키텍처의 흐름에서 부수효과가 발생하는 지점을 다루는 논리적인 단계가 없다는 점이 effect 기능의 존재의의이다. 만약 effect가 없다면, angular의 service, 혹은 컴포넌트에서 부수효과가 발생하고, 해당 부수효과로부터 service와 컴포넌트가 영향 받지 않도록 관리함과 동시에 store state의 값을 갱신해야 하는 번거로움이 생긴다. 또한 api 통신이나 http 요청과 같은 부수효과는 실패하는 경우도 종종 발생하기 때문에 예외처리 역시 필요하게 되는데, 이런 예외처리 상황까지 상정하는 경우 부수효과를 관리하는 service 또는 컴포넌트의 책임이 커지고 그에 따른 유지보수의 비용이 커지게 된다.

하지만 effect를 사용하는 경우, ngrx의 action으로 부수효과를 관리할 수 있으며, 이것은 state 뿐만 아니라 부수효과의 발생까지 ngrx가 관리할 수 있는 이점이 생긴다. 이것은 state 추적 및 디버그에도 도움이 되며 프론트엔드 코드의 신뢰성을 높여준다.

다음은 위의 ngrx architecture를 알아보기 쉽게 정리한 이미지이다.

(Bonus) @ngrx/store-devtools 로 state와 action 추적하기

@ngrx/store-devtools는 action이 dispatch 되는 순서를 보여주고 그로 인해 state가 변경하는 모습을 아주아주 직관적으로 확인할 수 있는 좋은 개발툴이다. 코드상에 별도의 로깅 작업 없이도 작동하기 때문에 ngrx 의 state 변화와 관련된 로그를 걷어내어 console.log를 좀 더 깔끔하게 관리할 수 있다는 장점도 있다.

http://extension.remotedev.io/

설치

$ npm install @ngrx/store-devtools --save

반드시 자신이 사용하는 ngrx의 버전에 맞춰 @ngrx/store-devtools 를 설치한다. 그렇지 않을 경우 단순히 @ngrx/store-devtools 만 작동하지 않는 것이 아니라 angular 앱 자체가 멈출 수 있다.

모듈 설정

AppModule에 다음과 같이 추가한다.

import { StoreDevtoolsModule } from '@ngrx/store-devtools';@NgModule({
imports: [
StoreModule.forRoot(reducers),
// 반드시 StoreModule 다음 라인에 작성한다!!
StoreDevtoolsModule.instrument({
maxAge: 25, // 가장 최근에 변화한 state 변경을 25개까지 보여준다
}),
],
})
export class AppModule {}

그 후 firefox 혹은 chrome 에서 Redux DevTools 를 설치한다.

배포 환경에 따른 설정

배포된 production 환경에서는 비활성화 시키고 싶다면 다음과 같이 import 해준다.

import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

import { environment } from '../environments/environment';

@NgModule({
...
imports: [
...
StoreModule.provideStore({...}),
!environment.production ? StoreDevtoolsModule.instrumentOnlyWithExtension() : [],
...
],
...
})
export class AppModule {
}

또는 메소드에 인자로 보내는 option 오브젝트에 logOnly 프로퍼티 값을 true로 설정하면 모든 state 값이 false로만 나타난다. 하지만 이 경우 state 데이터 구조는 그대로 드러나기 때문에 위의 방법이 좀 더 안전하다.

사용하기

모든 설정이 완료되면 이제 각 브라우저의 개발자 도구를 열어서 action과 state의 흐름을 추적하면 된다!

store state의 값 index 의 값이 1에서 0으로 업데이트 된 모습

--

--