리덕스 이해하는데 도움되는 다이어그램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로 업데이트하여 리턴하는 함수입니다.
// 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;
}
};
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
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을 구현한 것
const store = new Store<AppState>(reducer, { messages: [] });
// redux 라이브러리 사용
const store: Store<AppState> = createStore<AppState>(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;
}
};
앵귤러 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;
}
};
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
});
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 }
];
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,
) {}