일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- tool
- php
- java
- AWS
- ubuntu
- Git
- springboot
- IntelliJ
- 맛집
- db
- Design Patterns
- MySQL
- Spring
- 요리
- Spring Batch
- devops
- jsp
- jenkins
- redis
- Oracle
- Spring Boot
- linux
- javascript
- ReactJS
- laravel
- elasticsearch
- Web Server
- Gradle
- JVM
- it
- Today
- Total
아무거나
[reactjs] Typescript with React + Redux - 2 (redux 적용) 본문
[reactjs] Typescript with React + Redux - 2 (redux 적용)
전봉근 2019. 11. 19. 00:50Redux
리덕스는 상태관리 라이브러리이다. 리덕스는 우리가 만드는 컴포넌트들의 상태 관련 로직들을 다른 파일들로 분리시켜서 더욱 효율적으로 관리 할 수 있습니다. 또한, 컴포넌트끼리 상태를 공유하게 될 때 여러 컴포넌트를 거치지 않고도 손쉽게 상태 값을 전달 할 수 있다.
아래 그림을 참고하자. 왼쪽은 Redux를 적용하지 않은것이고, 오른쪽은 Redux를 사용한것이다.
위의 이미지의 왼쪽 그림에서 만약 A에서 B로 데이터가 변경되거나 렌더를 다시해야 될 때 과정은 화살표 방향처럼 연결되어 있는 다른 컴포넌트들에게 상태가 전달된다. 이러한 나뭇가지식 관리를 하게 된다면 향 후 관리범위가 많아져서 골치아픈 상황이된다. 이러한 부분을 개선하고자 Redux를 사용하게 된 것이다.
Redux는 Application Client쪽 state를 관리하기 위한 거대한 이벤트 루프이다.
- 액션: 이벤트
- 리듀서: 이벤트에 대한 반응
리액트(React) 상태(State) 생성자(Producer) 라는 뜻으로 해석해서 보면 쉽게 이해되고 외워진다. 실제로 리덕스(Redux)는 액션(Action)이 날라오면 리듀서(Reducer)가 스토어(Store)의 상태(State)르 변경시키는 방식으로 동작한다.
만약 타입스크립트를 사용한다면 컨테이너 컴포넌트를 만들게 될 때, 그리고 리듀서를 작성하게 될 때, 단순히 타입 체킹 뿐만이 아니라 자동완성이 되므로 생산성이 매우 향상된다.
Redux 패키지 설치
yarn add redux react-redux immutable redux-actions
yarn add --dev @types/redux @types/react-redux @types/immutable @types/redux-actions
// 설치 로그중에 immutable과 redux의 경우 이미 typescript지원이 내장되어있으니 설치가 필요없다고 표시되면 아래처럼 명령어를 실행한다.
yarn remove @types/immutable @types/redux
리덕스 모듈 작성
react-typescript-redux-example-1에서 만들었던 카운터 컴포넌트의 리덕스 모듈을 작성해보자.
들어가기전에 Redux를 사용하는 어플리케이션을 구축하다 보면 기능별로 여러 개의 액션 타입과, 액션, 리듀서 한 세트를 만들어야 한다. 이들은 관습적으로 여러 개의 폴더로 나누어져서, 하나의 기능을 수정할 때는 이 기능과 관련된 여러 개의 파일을 수정해야 하는 일이 생긴다. 여기서 불편함을 느껴 나온 것이 Ducks 구조이다.
[Example]
// widgets.js
// Actions
const LOAD = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';
// Reducer
export default function reducer(state = {}, action = {}) {
switch (action.type) {
// do reducer stuff
default: return state;
}
}
// Action 생성자
export function loadWidgets() {
return { type: LOAD };
}
export function createWidget(widget) {
return { type: CREATE, widget };
}
export function updateWidget(widget) {
return { type: UPDATE, widget };
}
export function removeWidget(widget) {
return { type: REMOVE, widget };
}
Ducks 구조에는 몇 가지 규칙이 있다.
- 하나의 모듈은..
- 항상 reducer()란 이름의 함수를 export default 해야한다.
- 항상 모듈의 action 생성자들을 함수형태로 export 해야한다.
- 항상 npm-module-or-app/reducer/ACTION_TYPE 형태의 action 타입을 가져야 한다.
- 어쩌면 action 타입들을 UPPER_SNAKE_CASE로 export 할 수 있다.
리덕스 Ducks 구조는 액션 타입, 액션 생성 함수, 리듀서를 한 파일에 작성하는 것
카운터 컴포넌트의 Redux 모듈 작성
[src/store/modules/counter.ts]
import { createAction, handleActions } from 'redux-actions';
const DECREMENT = 'counter/DECREMENT';
const INCREMENT = 'counter/INCREMENT';
export const actionCreators = {
decrement: createAction(DECREMENT),
increment: createAction(INCREMENT),
};
export interface ICounterState {
value: number;
}
const initialState: ICounterState = {
value: 0
};
export default handleActions<ICounterState>(
{
[INCREMENT]: (state) => ({ value: state.value + 1 }),
[DECREMENT]: (state) => ({ value: state.value - 1 }),
},
initialState
);
위의 코드는 컴포넌트가 아니므로 .tsx가 아닌 .ts확장자로 저장하자.
스토어 생성 및 적용
루트 리듀서를 생성하자.
루트 리듀서란 만약 계속하여 리듀서가 여러개 존재하게 된다. 여러개의 리듀서가 있으면 리덕스의 함수 combineReducers를 사용하여 하나의 리듀서로 합쳐줄 수 있다. 이것을 루트 리듀서라고 한다.
[src/store/modules/index.ts]
import { combineReducers } from 'redux';
import counter, { ICounterState } from './counter';
export default combineReducers({
counter
});
// 스토어의 상태 타입 정의
export interface IStoreState {
counter: ICounterState;
}
이제 스토어를 생성하는 함수를 만들자.
[src/store/configureStore.ts]
import modules from './modules';
import { createStore } from 'redux';
export default function configureStore() {
const store = createStore(
modules, /* preloadedState, */
(window as any).__REDUX_DEVTOOLS_EXTENSION__ && (window as any).__REDUX_DEVTOOLS_EXTENSION__()
);
return store;
}
위 코드는 window 객체에 원래는 REDUX_DEVTOOLS_EXTENSION 이 없으므로 에러가 날 것이다. 따라서, 우리는 타입을 강제 캐스팅 (Type Assertion) 하자. any 를 사용하면, 문법 검사 시스템에서 사용하지 말라고 토를 달 것입니다. any 를 자주 쓰는건 물론 좋지 않지만, 가끔씩은 이렇게 써야 하는 상황도 있습니다. 따라서, 문법 검사 설정을 변경하여 오류가 나타나지 않게 설정하자.
[tslint.json]
...
"rules": {
...
"no-any": false
}
...
그 다음 index.tsx에서 Provider를 통해 리덕스 스토어를 적용하자.
[src/index.tsx]
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import './index.css';
import registerServiceWorker from './registerServiceWorker';
import configureStore from './store/configureStore';
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root') as HTMLElement
);
registerServiceWorker();
카운터 컨테이너 컴포넌트 생성
만들기전에 디렉토리를 편하게 불러올 수 있게 NODE_PATH와 baseUrl 값을 설정하자.
[/.env]
NODE_PATH는 최상단 디렉토리에 .env파일을 생성하여 코드를 작성
NODE_PATH=src
그 다음 타입스크립트 도구가 제대로 인식할 수 있도록 baseUrl을 설정
[/tsconfig.json]
{
"compilerOptions": {
"baseUrl": "./src",
"outDir": "build/dist",
...
},
"exclude": [
"node_modules",
...
]
}
위의 내용이 다 완료되면 IDE와 개발 서버를 재 시작 한다.
그 다음 기존의 Counter 컴포넌트를 프리젠테이셔널 컴포넌트로서 사용하기 위해 들고있던 state와 method들을 제거하고 아래 코드로 변경하자.
[src/components/Counter.tsx]
import * as React from 'react';
interface IProps {
value: number;
onIncrement(): void;
onDecrement(): void;
}
const Counter: React.SFC<IProps> = ({ value, onIncrement, onDecrement }) => (
<div>
<h2>카운터</h2>
<h3>{value}</h3>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
);
export default Counter;
컨테이너 컴포넌트를 작성하자.
[src/containers/CounterContainer.tsx]
import * as React from 'react';
import Counter from 'components/Counter';
import { actionCreators as counterActions } from 'store/modules/counter';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { IStoreState } from 'store/modules';
export interface IProps {
value: number;
CounterActions: typeof counterActions;
};
class CounterContainer extends React.Component<IProps> {
onIncrement = () => {
const { CounterActions } = this.props;
CounterActions.increment();
}
onDecrement = () => {
const { CounterActions } = this.props;
CounterActions.decrement();
}
render() {
const { onIncrement, onDecrement } = this;
const { value } = this.props;
return (
<Counter
onIncrement={onIncrement}
onDecrement={onDecrement}
value={value}
/>
);
}
}
export default connect(
({ counter }: IStoreState) => ({
value: counter.value
}),
(dispatch) => ({
CounterActions: bindActionCreators(counterActions, dispatch)
})
)(CounterContainer);
App에서 기존 Counter을 CounterContainer로 변경하자.
[src/App.tsx]
import * as React from 'react';
import './App.css';
import CounterContainer from 'containers/CounterContainer';
import Profile from './components/Profile';
import TodoList from './components/TodoList';
class App extends React.Component {
render() {
return (
<div className="App">
<Profile
name="bkjeon"
job="developer"
/>
<CounterContainer />
<TodoList />
</div>
);
}
}
export default App;
TodoList 리덕스 모듈 생성
[src/store/modules/todos.ts]
import { Record, List } from 'immutable';
import { createAction, handleActions, Action } from 'redux-actions';
const CREATE = 'todos/CREATE';
const REMOVE = 'todos/REMOVE';
const TOGGLE = 'todos/TOGGLE';
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
type CreatePayload = string;
type RemovePayload = number;
type TogglePayload = number;
type ChangeInputPayload = string;
/* type AnotherPayload = {
something: string;
like: number;
this: boolean
}; */
export const actionCreators = {
create: createAction<CreatePayload>(CREATE),
remove: createAction<RemovePayload>(REMOVE),
toggle: createAction<TogglePayload>(TOGGLE),
changeInput: createAction<ChangeInputPayload>(CHANGE_INPUT)
};
const TodoItemRecord = Record({
id: 0,
text: '',
done: false
});
interface ITodoItemDataParams {
id?: number;
text?: string;
done?: boolean;
}
export class TodoItemData extends TodoItemRecord {
static autoId = 0;
id: number;
text: string;
done: boolean;
constructor(params?: ITodoItemDataParams) {
const id = TodoItemData.autoId;
if (params) {
super({
...params,
id,
});
} else {
super({ id });
}
TodoItemData.autoId = id + 1;
}
}
const TodosStateRecord = Record({
todoItems: List(),
input: ''
});
export class TodosState extends TodosStateRecord {
todoItems: List<TodoItemData>;
input: string;
}
const initialState = new TodosState();
export default handleActions<TodosState, any>(
{
[CREATE]: (state, action: Action<CreatePayload>): TodosState => {
return <TodosState> state.withMutations(
s => {
s.set('input', '')
.update('todoItems', (todoItems: List<TodoItemData>) => todoItems.push(
new TodoItemData({ text: action.payload })
));
}
);
},
[REMOVE]: (state, action: Action<RemovePayload>): TodosState => {
return <TodosState> state.update(
'todoItems',
(todoItems: List<TodoItemData>) => todoItems.filter(
t => t ? t.id !== action.payload : false
)
);
},
[TOGGLE]: (state, action: Action<TogglePayload>): TodosState => {
const index = state.todoItems.findIndex(t => t ? t.id === action.payload : false);
return <TodosState> state.updateIn(['todoItems', index, 'done'], done => !done);
},
[CHANGE_INPUT]: (state, action: Action<ChangeInputPayload>): TodosState => {
return <TodosState> state.set('input', action.payload);
},
},
initialState
);
여기에선 Immutable, createAction, handleActions 를 사용했다. Immutable 을 사용하게 될 때에는, Map 대신에 Record 를 사용하면 타입스크립트의 이점을 제대로 누릴 수 있다. 추가적으로, 각 액션의 Payload 또한 타입을 지정해두면, 각 액션 생성함수를 호출하거나, 혹은 리듀서에서 액션을 처리하게 될 때 큰 노력 없이도 바로 어떠한 종류의 payload 를 가진 액션인지 파악 가능합니다.
payload: 페이로드는 사용에 있어서 전송되는 데이터를 뜻한다. (헤더와 메타데이터는 제외)
리듀서의 각 함수를 보면
return <TodosState> state...
이런식으로 되어있는데 이 부분은 기본적으로 state.update, state.set 등의 함수의 결과물 타입이 Map 이기 때문에 이를 다시 타입 캐스팅 해주는 것이다. (ex: window as any..) 따라서, return state…. as TodosState 와 같은 형식으로 하셔도 무방하다.
이제 만든 모듈을 combineReducers 쪽과 StoreState 에 추가하자.
[src/store/modules/index.ts]
import counter, { ICounterState } from './counter';
import { combineReducers } from 'redux';
import todos, { TodosState } from './todos';
export default combineReducers({
counter,
todos
});
// 스토어의 상태 타입 정의
export interface IStoreState {
counter: ICounterState;
todos: TodosState;
}
TodoList 컴포넌트 프리젠테이셔널 컴포넌트로 전환
TodoList의 state를 없애고 함수형 컴포넌트로 전환하자.
[src/components/TodoList.tsx]
import * as React from 'react';
import TodoItem from './TodoItem';
import { TodoItemData } from 'store/modules/todos';
import { List } from 'immutable';
interface IProps {
input: string;
todoItems: List<TodoItemData>;
onCreate(): void;
onRemove(id: number): void;
onToggle(id: number): void;
onChange(e: any): void;
}
const TodoList: React.SFC<IProps> = ({
input, todoItems, onCreate, onRemove, onToggle, onChange
}) => {
const todoItemList = todoItems.map(
todo => todo ? (
<TodoItem
key={todo.id}
done={todo.done}
onToggle={() => onToggle(todo.id)}
onRemove={() => onRemove(todo.id)}
text={todo.text}
/>
) : null
);
return (
<div>
<h1>할일</h1>
<form
onSubmit={
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onCreate();
}
}
>
<input onChange={onChange} value={input} />
<button type="submit">추가</button>
</form>
<ul>
{todoItemList}
</ul>
</div>
);
};
export default TodoList;
TodoListContainer 생성
[src/containers/TodoListContainer.tsx]
import * as React from 'react';
import TodoList from 'components/TodoList';
import { connect } from 'react-redux';
import { IStoreState } from 'store/modules';
import {
TodoItemData,
actionCreators as todosActions
} from 'store/modules/todos';
import { bindActionCreators } from 'redux';
import { List } from 'immutable';
interface IProps {
todoItems: List<TodoItemData>;
input: string;
TodosActions: typeof todosActions;
}
class TodoListContainer extends React.Component<IProps> {
onCreate = () => {
const { TodosActions, input } = this.props;
TodosActions.create(input);
}
onRemove = (id: number) => {
const { TodosActions } = this.props;
TodosActions.remove(id);
}
onToggle = (id: number) => {
const { TodosActions } = this.props;
TodosActions.toggle(id);
}
onChange = (e: React.FormEvent<HTMLInputElement>) => {
const { value } = e.currentTarget;
const { TodosActions } = this.props;
TodosActions.changeInput(value);
}
render() {
const { input, todoItems } = this.props;
const { onCreate, onRemove, onToggle, onChange } = this;
return (
<TodoList
input={input}
todoItems={todoItems}
onCreate={onCreate}
onRemove={onRemove}
onToggle={onToggle}
onChange={onChange}
/>
);
}
}
export default connect(
({ todos }: IStoreState) => ({
input: todos.input,
todoItems: todos.todoItems,
}),
(dispatch) => ({
TodosActions: bindActionCreators(todosActions, dispatch),
})
)(TodoListContainer);
App에서 TodoList를 TodoListContainer 로 교체
[src/App.tsx]
import * as React from 'react';
import './App.css';
import CounterContainer from 'containers/CounterContainer';
import Profile from './components/Profile';
import TodoListContainer from 'containers/TodoListContainer';
class App extends React.Component {
render() {
return (
<div className="App">
<Profile
name="bkjeon"
job="developer"
/>
<CounterContainer />
<TodoListContainer />
</div>
);
}
}
export default App;
앱을 실행하면 아래와 같이 적용된 내용을 확인할 수 있다.
참고
https://blog.codecentric.de
https://velopert.com/
https://devlog.jwgo.kr/
'Javascript & HTML & CSS > reactjs' 카테고리의 다른 글
react uri path 값 가져오기 (0) | 2020.01.02 |
---|---|
history.push 동작 오류 (0) | 2019.12.31 |
[reactjs] Typescript with React + Redux - 1 (프로젝트 생성) (0) | 2019.11.19 |
[reactjs] React + Typescript - 4 (React-Router) (0) | 2019.11.19 |
[reactjs] React + Typescript - 3 (Component와 props, state) (0) | 2019.11.19 |