angular2+ 에 redux

노요셉·2020년 3월 27일
0

리덕스 이해하는데 도움되는 다이어그램1

ngrx에서 side effect는 다음과 같은 상황을 말하는 듯

http://slides.com/jenyaterpil/redux-from-twitter-hype-to-production#/

리덕스 이해에 도움되는 다이어그램2

https://github.com/reduxjs/redux/issues/653

참고 - 앵귤러 마스터 북

redux core -> redux, angular2+ counter -> redux, angular2+ chat app -> 실제 프로젝트에 도입하기 ( ngrx 로? )

redux : state, action, reducer

store는 state를 프로퍼티로 가진, dispatch를 메서드로 가진 container로 볼 수 있습니다.

reducer : action을 인자로 받아서 action type에 해당하는 이전 state 를 새로운 state로 업데이트하여 리턴하는 함수입니다.

redux minimal-store 직접 리덕스를 구현해서 적용

// same reducer as before
let reducer: Reducer<number> = (state: number, action: Action) => {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  case 'PLUS':
    return state + action.payload;
  default:
    return state;
  }
};

dispatch : action을 인자로 받고 내부적으로는 reducer를 이용하여 저장소의 state를 업데이트합니다.

class Store<T> {
  private _state: T;

  constructor(
    private reducer: Reducer<T>,
    initialState: T
  ) {
    this._state = initialState;
  }

  getState(): T {
    return this._state;
  }

  dispatch(action: Action): void {
    this._state = this.reducer(this._state, action);
  }
}

스토어를 생성할때는 구현해놓은 reducer를 생성할때 넣어줍니다.

let store = new Store<number>(reducer, 0);

코드 전체
store 클래스의 제네릭 타입은 말 그대로 아무런 객체든 들어갈 수 있다는 점

interface Action {
  type: string;
  payload?: any;
}

interface Reducer<T> {
  (state: T, action: Action): T;
}

class Store<T> {
  private _state: T;

  constructor(
    private reducer: Reducer<T>,
    initialState: T
  ) {
    this._state = initialState;
  }

  getState(): T {
    return this._state;
  }

  dispatch(action: Action): void {
    this._state = this.reducer(this._state, action);
  }
}

// same reducer as before
let reducer: Reducer<number> = (state: number, action: Action) => {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  case 'PLUS':
    return state + action.payload;
  default:
    return state;
  }
};

// create a new store
let store = new Store<number>(reducer, 0);
console.log(store.getState()); // -> 0

store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // -> 1

store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // -> 2

store.dispatch({ type: 'DECREMENT' });
console.log(store.getState()); // -> 1

store-w-subscribe

interface Action {
  type: string;
  payload?: any;
}

interface Reducer<T> {
  (state: T, action: Action): T;
}

interface ListenerCallback {
  (): void;
}

interface UnsubscribeCallback {
  (): void;
}

class Store<T> {
  private _state: T;
  private _listeners: ListenerCallback[] = [];

  constructor(
    private reducer: Reducer<T>,
    initialState: T
  ) {
    this._state = initialState;
  }

  getState(): T {
    return this._state;
  }

  dispatch(action: Action): void {
    this._state = this.reducer(this._state, action);
    this._listeners.forEach((listener: ListenerCallback) => listener());
  }

  subscribe(listener: ListenerCallback): UnsubscribeCallback {
    this._listeners.push(listener);
    return () => { // returns an "unsubscribe" function
      this._listeners = this._listeners.filter(l => l !== listener);
    };
  }
}

// same reducer as before
let reducer: Reducer<number> = (state: number, action: Action) => {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  case 'PLUS':
    return state + action.payload;
  default:
    return state;
  }
};

// create a new store
let store = new Store<number>(reducer, 0);
console.log(store.getState()); // -> 0

// subscribe
let unsubscribe = store.subscribe(() => {
  console.log('subscribed: ', store.getState());
});

store.dispatch({ type: 'INCREMENT' }); // -> subscribed: 1
store.dispatch({ type: 'INCREMENT' }); // -> subscribed: 2

unsubscribe();
store.dispatch({ type: 'DECREMENT' }); // (nothing logged)

// decrement happened, even though we weren't listening for it
console.log(store.getState()); // -> 1

액션을 종류로 나누고, 액션마다 다루는 타입을 다루게 두고싶다면?

상속을 이용

export interface Action {
  type: string;
  payload?: any;
}
  
interface AddMessageAction extends Action {
  message: string;
}

interface DeleteMessageAction extends Action {
  index: number;
}
const reducer: Reducer<AppState> =
  (state: AppState, action: Action): AppState => {
  switch (action.type) {
  case 'ADD_MESSAGE':
    return {
      messages: state.messages.concat(
        (<AddMessageAction>action).message
      ),
    };
  case 'DELETE_MESSAGE':
    let idx = (<DeleteMessageAction>action).index;
    return {
      messages: [
        ...state.messages.slice(0, idx),
        ...state.messages.slice(idx + 1, state.messages.length)
      ]
    };
  default:
    return state;
  }
};

// create a new store
const store = new Store<AppState>(reducer, { messages: [] });
console.log(store.getState()); // -> { messages: [] }

store.dispatch({
  type: 'ADD_MESSAGE',
  message: 'Would you say the fringe was made of silk?'
} as AddMessageAction);

store.dispatch({
  type: 'ADD_MESSAGE',
  message: 'Wouldnt have no other kind but silk'
} as AddMessageAction);

store.dispatch({
  type: 'ADD_MESSAGE',
  message: 'Has it really got a team of snow white horses?'
} as AddMessageAction);

console.log(store.getState());
// -> 
// { messages:
//    [ 'Would you say the fringe was made of silk?',
//      'Wouldnt have no other kind but silk',
//      'Has it really got a team of snow white horses?' ] }

store.dispatch({
  type: 'DELETE_MESSAGE',
  index: 1
} as DeleteMessageAction);

console.log(store.getState());
// -> 
// { messages:
//    [ 'Would you say the fringe was made of silk?',
//      'Has it really got a team of snow white horses?' ] }

store.dispatch({
  type: 'DELETE_MESSAGE',
  index: 0
} as DeleteMessageAction);

console.log(store.getState());
// ->
// { messages: [ 'Has it really got a team of snow white horses?' ] }

<AddMessageAction>action 문법은 action을 구체적인 타입으로 변환한다. reducer는 messages 필드가 없는 더 일반적인 타입인 Action을 받는다. 이 문법을 적용하지 않으면 Action에 messages 필드가 없다며 오류를 출력할 것임.

case "DELETE_MESSAGE":
      let idx = (<DeleteMessageAction>action).index;
      return {
        messages: [
          ...state.messages.slice(0, idx),
          ...state.messages.slice(idx + 1, state.messages.length)
        ]
      };

Object.assign({}, oldObject, newObject) <- 이 방향으로 프로퍼티들이 병합

action을 다음과 같이 직접 만들지 말고 wrapper함수를 둡시다.

store.dispatch(액션)

store.dispatch({
  type: "ADD_MESSAGE",
  message: "Has it really got a team of snow white horses?"
} as AddMessageAction);

위 코드를 다음과 같이 wrapper 정적 메서드를 생성해서.

class MessageActions {
  static addMessage(message: string): AddMessageAction {
    return {
      type: 'ADD_MESSAGE',
      message: message
    };
  }
  static deleteMessage(index: number): DeleteMessageAction {
    return {
      type: 'DELETE_MESSAGE',
      index: index
    };
  }
}

다음과 같이 가독성을 높일 수 있음.

store.dispatch(
  MessageActions.addMessage('Would you say the fringe was made of silk?'));

정리

상속을 통해 다양한 액션을 처리할 수 있게 됌.

export interface Action {
  type: string;
  payload?: any;
}
  
interface AddMessageAction extends Action {
  message: string;
}

interface DeleteMessageAction extends Action {
  index: number;
}

위 코드를 다음과 같이 wrapper 정적 메서드를 생성해서 Action의 가독성을 높여버림.

class MessageActions {
  static addMessage(message: string): AddMessageAction {
    return {
      type: 'ADD_MESSAGE',
      message: message
    };
  }
  static deleteMessage(index: number): DeleteMessageAction {
    return {
      type: 'DELETE_MESSAGE',
      index: index
    };
  }
}

또한 메시지 형식을 바꾸더라도 dispatch의 타입을 일일이 업데이트 할 필요가 없다.

class MessageActions {
  static addMessage(message: string): AddMessageAction {
    return {
      type: 'ADD_MESSAGE',
      message: message
      created_at: new Date() // <- 이를 추가해도 dispatch
    };
  }
  static deleteMessage(index: number): DeleteMessageAction {
    return {
      type: 'DELETE_MESSAGE',
      index: index
    };
  }
}

dispatch 했을 때

store.dispatch({
  type: 'ADD_MESSAGE',
  thread: tEcho,
  message: 'Would you say the fringe was made of silk?'
} as AddMessageAction);

ActionCreator를 사용해서 dispatch했을 때

 store.dispatch(ThreadActions.addMessage(tEcho, {
    author: echo,
    sentAt: moment().subtract(1, 'minutes').toDate(),
    text: 'I\'ll echo whatever you send me'
  })

리덕스 라이브러리 적용하기

store 생성

// 직접 store을 구현한 것
const store = new Store<AppState>(reducer, { messages: [] });

// redux 라이브러리 사용
const store: Store<AppState> = createStore<AppState>(reducer);

reducer 생성

저장소를 생성할때 초기상태를 직접 지정하지 않고, redux를 사용할때는
리듀서가 초기 상태를 만들도록 합니다. default parameter을 이용합니다.


// 직접 구현한 reducer

const reducer: Reducer<AppState> =
  (state: AppState, action: Action) => {
  switch (action.type) {
  case 'ADD_MESSAGE':
    return {
      messages: state.messages.concat((<AddMessageAction>action).message),
    };
  case 'DELETE_MESSAGE':
    let idx = (<DeleteMessageAction>action).index;
    return {
      messages: [
        ...state.messages.slice(0, idx),
        ...state.messages.slice(idx + 1, state.messages.length)
      ]
    };
  default:
    return state;
  }
};

// redux 라이브러리 이용
const initialState: AppState = { messages: [] };

const reducer: Reducer<AppState> =
  (state: AppState = initialState, action: Action) => {
  switch (action.type) {
  case 'ADD_MESSAGE':
    return {
      messages: state.messages.concat((<AddMessageAction>action).message),
    };
  case 'DELETE_MESSAGE':
    let idx = (<DeleteMessageAction>action).index;
    return {
      messages: [
        ...state.messages.slice(0, idx),
        ...state.messages.slice(idx + 1, state.messages.length)
      ]
    };
  default:
    return state;
  }
};

앵귤러에 리덕스 적용하기 counter 앱

앵귤러 v7.2.0
리덕스 v3.6.0

애플리케이션 상태 정의 ( 코어 상태 구조 )

export interface AppState {
  counter: number;
}

리듀서 정의

/**
 * Counter Reducer
 */
import { Reducer, Action } from "redux";
import { AppState } from "./app.state";
import { INCREMENT, DECREMENT } from "./counter.actions";

const initialState: AppState = { counter: 0 };

// Create our reducer that will handle changes to the state
export const counterReducer: Reducer<AppState> = (
  state: AppState = initialState,
  action: Action
): AppState => {
  switch (action.type) {
    case INCREMENT:
      return { ...state, counter: state.counter + 1 };
    //return Object.assign({}, state, { counter: state.counter + 1 });
    case DECREMENT:
      return { ...state, counter: state.counter - 1 };
    //return Object.assign({}, state, { counter: state.counter - 1 });
    default:
      return state;
  }
};

Action 생성자 정의

import {
  Action,
  ActionCreator
} from 'redux';

export const INCREMENT: string = 'INCREMENT';
export const increment: ActionCreator<Action> = () => ({
  type: INCREMENT
});

export const DECREMENT: string = 'DECREMENT';
export const decrement: ActionCreator<Action> = () => ({
  type: DECREMENT
});

store 생성

import { InjectionToken } from '@angular/core';
import {
  createStore,
  Store,
  compose,
  StoreEnhancer
} from 'redux';

import { AppState } from './app.state';
import {
  counterReducer as reducer
} from './counter.reducer';

export const AppStore = new InjectionToken('App.store');

const devtools: StoreEnhancer<AppState> =
  window['devToolsExtension'] ?
  window['devToolsExtension']() : f => f;

export function createAppStore(): Store<AppState> {
  return createStore<AppState>(
    reducer,
    compose(devtools)
  );
}

export const appStoreProviders = [
   { provide: AppStore, useFactory: createAppStore }
];

리덕스 DevTools 환경 설정

const devtools: StoreEnhancer<AppState> =
  window['devToolsExtension'] ?
  window['devToolsExtension']() : f => f;

export function createAppStore(): Store<AppState> {
  return createStore<AppState>(
    reducer,
    compose(devtools)
  );
}

앵귤러에 의존성 주입을 위한 설정

리덕스는 앱 어느 곳에서든지 저장소 인스턴스에 액세스할 수 있어야한다. 의존성 주입을 사용

Store은 인터페이스라서 의존성 주입을 할 수 없다.
타입스크립트 인터페이스는 컴파일 이후 제거되어 런타임에는 사용할 수 없다.

따라서 저장소를 주입할 때 사용할 자체 토큰을 만들어야 한다. InjectionToken을 사용한다.
이 또한 충돌이 발생할 수 있기 때문에 OpaqueToken클래스를 사용하면 된다.

import { InjectionToken } from '@angular/core';
...
export const AppStore = new InjectionToken('App.store');
...
export function createAppStore(): Store<AppState> {
  return createStore<AppState>(
    reducer  
  );
}

export const appStoreProviders = [
  // { provide: 'AppStore', useFactory: createAppStore } 추천하지 않는다.
   { provide: AppStore, useFactory: createAppStore } // OpaqueToken 클래스 이용.
];

providers 설정을 NgModule의 providers 리스트에 추가해야함. DI 시스템에 무엇이든 제공할때
1. 주입 가능한 의존성을 참조하기 위한 토큰
2. 의존성을 주입할 방법

의존성 주입할 것이 클래스(서비스)면

{ provide: SthService, useClass: SthService }

의존성 주입할 것이 (객체를 리턴하는 함수)팩토리함수면

export function createAppStore(): Store<AppState> {
  return createStore<AppState>(
    reducer,
    compose(devtools)
  );
}

export const appStoreProviders = [
   { provide: AppStore, useFactory: createAppStore }
];

문자열일 경우

   { provide: YOUTUBE_API_KEY, useValue: 'YOUTUBE_API_KEY' },

사용할 모듈에 추가해줘야한다. 그래야 각 모든 컴포넌트의 생성자에서 의존성 주입을 해줄 수 있다. ( 최상위 모듈 providers에 추가해줬기 때문에 )

@NgModule({
  declarations: [
    AppComponent,
    SimpleHttpComponent,
    MoreHttpRequestsComponent,
    YouTubeSearchComponent,
    SearchResultComponent,
    SearchBoxComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule 
  ],
  providers: [youTubeSearchInjectables], // <-- right here
  bootstrap: [AppComponent] 
})
export class AppModule {}

youTubeSearchInjectables은? 다음과 같은 형태로 넣어줍니다.

export const youTubeSearchInjectables: Array<any> = [
  {provide: YouTubeSearchService, useClass: YouTubeSearchService},
  {provide: YOUTUBE_API_KEY, useValue: YOUTUBE_API_KEY},
  {provide: YOUTUBE_API_URL, useValue: YOUTUBE_API_URL}
];

컴포넌트에서 DI할때는

constructor(
    private http: HttpClient,
    @Inject(YOUTUBE_API_KEY) private apiKey: string,
  ) {}
profile
서로 아는 것들을 공유해요~

0개의 댓글