Woongjae Lee
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team
2woongjae@gmail.com
// 파일 분리를 하지 않을때
// ADD_TODO => addTofo
// 액션의 type 정의
const ADD_TODO = 'ADD_TODO';
// 액션 생산자
// 액션의 타입은 미리 정의한 타입으로 부터 가져와서 사용하며,
// 사용자가 인자로 주지 않습니다.
export function addTodo(text) {
return { type: ADD_TODO, text }; // { type: ADD_TODO, text: text }
}
function todoApp(state, action) { return state; }
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, {
text: action.text,
completed: false
}];
case COMPLETE_TODO:
return [
...state.slice(0, action.index),
Object.assign({}, state[action.index], {
completed: true
}),
...state.slice(action.index + 1)
];
default:
return state;
}
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
});
case ADD_TODO:
return Object.assign({}, state, {
todos: [...state.todos, {
text: action.text,
completed: false
}]
});
case COMPLETE_TODO:
return Object.assign({}, state, {
todos: [
...state.todos.slice(0, action.index),
Object.assign({}, state.todos[action.index], {
completed: true
}),
...state.todos.slice(action.index + 1)
]
});
default:
return state;
}
}
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, {
text: action.text,
completed: false
}];
case COMPLETE_TODO:
return [
...state.slice(0, action.index),
Object.assign({}, state[action.index], {
completed: true
}),
...state.slice(action.index + 1)
];
default:
return state;
}
}
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state;
}
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
};
}
import { combineReducers } from 'redux';
const todoApp = combineReducers({
visibilityFilter,
todos
});
getState(): S;
dispatch: Dispatch<S>;
subscribe(listener: () => void): Unsubscribe;
리턴이 Unsubscribe 라는 점 !
replaceReducer(nextReducer: Reducer<S>): void;
// yarn add redux @types/redux
import {createStore} from 'redux';
// 타입 정의
const ADD_AGE = 'ADD_AGE';
// 타입 생성 함수
function addAge(): {type: string;} {
return {
type: ADD_AGE
};
}
// 리듀서
function ageApp(state: {age: number;} = {age: 35}, action: {type: string;}): {age: number} {
if (action.type === ADD_AGE) {
return {age: state.age + 1};
}
return state;
}
// 스토어 만들기
const store = createStore<{age: number;}>(ageApp);
// index.tsx 에서 랜더링 다시 하기
function render() {
ReactDOM.render(
<App store={store} />,
document.getElementById('root') as HTMLElement
);
}
store.subscribe(render);
render();
// App.tsx 에서 랜더링 다시 하기
ReactDOM.render(
<App store={store} />,
document.getElementById('root') as HTMLElement
);
import {Store, Unsubscribe} from 'redux';
import {addAge} from './index';
interface AppProps {
store: Store<{ age: number; }>;
}
class App extends React.Component<AppProps, {}> {
private _unsubscribe: Unsubscribe;
constructor(props: AppProps) {
super(props);
this._addAge = this._addAge.bind(this);
}
componentDidMount() {
const store = this.props.store;
this._unsubscribe = store.subscribe(() => {
this.forceUpdate();
});
}
componentWillUnmount() {
if (this._unsubscribe !== null) {
this._unsubscribe();
}
}
render() {
const state = this.props.store.getState();
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
나이가 {state.age}
<button onClick={this._addAge}>한해가 지났다.</button>
</p>
</div>
);
}
private _addAge(): void {
const store = this.props.store;
const action = addAge();
store.dispatch(action);
}
}
import * as PropTypes from 'prop-types';
// Provider 만들기
class Provider extends React.Component<{ store: Store<{ age: number; }>; children: JSX.Element; }, {}> {
public static childContextTypes = {
store: PropTypes.object // React.PropTypes.object
};
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
import * as PropTypes from 'prop-types';
class App extends React.Component<{}, {}> {
// App.contextTypes 에 스토어를 받아오도록 정의해야 합니다.
public static contextTypes = {
store: PropTypes.object
};
private _unsubscribe: Unsubscribe;
constructor(props: {}) {
super(props);
this._addAge = this._addAge.bind(this);
}
componentDidMount() {
const store = this.context.store;
this._unsubscribe = store.subscribe(() => {
this.forceUpdate();
});
}
componentWillUnmount() {
if (this._unsubscribe !== null) {
this._unsubscribe();
}
}
render() {
const state = this.context.store.getState();
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
나이가 {state.age}
<button onClick={this._addAge}>한해가 지났다.</button>
</p>
</div>
);
}
private _addAge(): void {
const store = this.context.store;
const action = addAge();
store.dispatch(action);
}
}
import * as React from 'react';
import {Unsubscribe} from 'redux';
import {addAge} from './index';
import * as PropTypes from 'prop-types';
class Button extends React.Component<{}, {}> {
// Button.contextTypes 에 스토어를 받아오도록 정의해야 합니다.
public static contextTypes = {
store: PropTypes.object
};
private _unsubscribe: Unsubscribe;
constructor(props: {}) {
super(props);
this._addAge = this._addAge.bind(this);
}
componentDidMount() {
const store = this.context.store;
this._unsubscribe = store.subscribe(() => {
this.forceUpdate();
});
}
componentWillUnmount() {
if (this._unsubscribe !== null) {
this._unsubscribe();
}
}
render() {
return <button onClick={this._addAge}>하위 컴포넌트에서 한해가 지났다.</button>;
}
private _addAge(): void {
const store = this.context.store;
const action = addAge();
store.dispatch(action);
}
}
export default Button;
// yarn add redux @types/redux
import {createStore} from 'redux';
// yarn add react-redux @types/react-redux
import {Provider} from 'react-redux';
const store = createStore<{ age: number; }>(ageApp);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// 나머지 코드 그대로 두고도 작동합니다
import * as ReactRedux from 'react-redux';
const { connect } = ReactRedux;
const AppContainer = connect(
mapStateToProps,
mapDispatchToProps
)(App);
// 1. mapStateToProps - 어떤 state 를 어떤 props 에 연결할 것인지에 대한 정의
// 2. mapDispatchToProps - 어떤 dispatch(action) 을 어떤 props 에 연결할 것인지에 대한 정의
// 3. App - 그 props 를 보낼 컴포넌트를 정의
export default AppContainer;
// 이 함수는 store.getState() 한 state 를
// 연결한(connect) App 컴포넌트의 어떤 props 로 줄 것인지를 리턴
// 그래서 이 함수의 리턴이 곧 App 컴포넌트의 AppProps 의 부분집합이어야 한다.
const mapStateToProps = (state: { age: number; }) => {
return {
age: state.age,
};
};
// 이 함수는 store.dispatch(액션)을
// 연결한(connect) App 컴포넌트의 어떤 props 로 줄 것인지를 리턴
// 그래서 이 함수의 리턴이 곧 App 컴포넌트의 AppProps 의 부분집합이어야 한다.
const mapDispatchToProps = (dispatch: Function) => {
return {
onAddClick: () => {
dispatch(addAge());
}
};
};
// mapStateToProps 와 mapDispatchToProps 의 리턴을 합치면 나오는 형태로 지정
interface AppProps {
age: number;
onAddClick(): void;
}
/*
class App extends React.Component<AppProps, {}> {
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
나이가 {this.props.age}
<button onClick={this.props.onAddClick}>한해가 지났다.</button>
<Button />
</p>
</div>
);
}
}
*/
const App: React.SFC<AppProps> = (props) => {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
나이가 {props.age}
<button onClick={props.onAddClick}>한해가 지났다.</button>
<Button />
</p>
</div>
);
};
app.get('*', (req, res) => {
const html = path.join(__dirname, '../build/index.html');
const htmlData = fs.readFileSync(html).toString();
const store = createStore(ageApp);
const ReactApp = ReactDOMServer.renderToString(
<Provider store={store}>
<AppContainer />
</Provider>
);
const initialState = store.getState();
const renderedHtml = htmlData.replace(`<div id="root">{{SSR}}</div>`, `<div id="root">${ReactApp}</div><script>window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};</script>`);
res.status(200).send(renderedHtml);
});
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root">${html}</div>
<script>window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};</script>
</body>
const initialState = (window as any).__INITIAL_STATE__;
// 서버에서 받은 초기값으로 스토어 만들기
const store = createStore<{ age: number; }>(ageApp, initialState);
React.render(
<Provider store={store}>
<AppContainer />
</Provider>,
document.getElementById('root')
);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="/manifest.json">
<link rel="shortcut icon" href="/favicon.ico">
<title>React App</title>
<link href="/static/css/main.cacbacc7.css" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<div class="App" data-reactroot="" data-reactid="1" data-react-checksum="1528495119">
<div class="App-header" data-reactid="2">
<img src="/logo.svg" class="App-logo" alt="logo" data-reactid="3"/>
<h2 data-reactid="4">Welcome to React</h2>
</div>
<p class="App-intro" data-reactid="5">
<!-- react-text: 6 -->나이가
<!-- /react-text -->
<!-- react-text: 7 -->35
<!-- /react-text -->
<button data-reactid="8">한해가 지났다.</button>
<button data-reactid="9">하위 컴포넌트에서 한해가 지났다.</button>
</p>
</div>
</div>
<script>window.__INITIAL_STATE__ = {"age":35};</script>
<script type="text/javascript" src="/static/js/main.ccc68f1a.js"></script>
</body>
</html>
// 타입 정의
export const START_GITHUB_API = 'START_GITHUB_API';
export const ERROR_GITHUB_API = 'ERROR_GITHUB_API';
export const END_GITHUB_API = 'END_GITHUB_API';
// 타입 생성 함수
export function startGithubApi(): { type: string; } {
return {
type: ADD_AGE
};
}
export function errorGithubApi(): { type: string; } {
return {
type: ADD_AGE
};
}
export function endGithubApi(age: number): { type: string; age: number; } {
return {
type: ADD_AGE,
age
};
}
// @types/react-redux
// connect 함수를 mapStateToProps 함수 하나만 인자로 쓰면 DispatchProp 가 넘어옵니다.
export declare function connect<TStateProps, no_dispatch, TOwnProps>(
mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps>
): ComponentDecorator<DispatchProp<any> & TStateProps, TOwnProps>;
// 수정
const { connect } = ReactRedux;
const mapStateToProps = (state: { age: number; }) => {
return {
age: state.age
};
};
const AppContainer = connect(mapStateToProps)(App);
// @types/react-redux
type Dispatch<S> = Redux.Dispatch<S>;
// @types/react-redux
export interface DispatchProp<S> {
dispatch: Dispatch<S>;
}
// redux/index.d.ts
export interface Dispatch<S> {
<A extends Action>(action: A): A;
}
const App: React.SFC<AppProps & ReactRedux.DispatchProp<{}>> = (props) => {
function getCountFromGithub(): void {
// 여기를 구현
}
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
나이가 {props.age}
<button onClick={() => props.dispatch(addAge())}>한해가 지났다.</button>
<button onClick={() => getCountFromGithub()}>깃헙 API 비동기 호출</button>
</p>
</div>
);
};
function getCountFromGithub(): void {
const dispatch: ReactRedux.Dispatch<any> = props.dispatch;
dispatch(startGithubApi());
request.get('https://api.github.com/users')
.end((err, res) => {
if (err) {
return dispatch(errorGithubApi());
}
const age = JSON.parse(res.text).length;
return dispatch(endGithubApi(age));
});
}
async function getCountFromGithub(): Promise<void> {
const dispatch: ReactRedux.Dispatch<{}> = props.dispatch;
dispatch(startGithubApi());
let res = null;
try {
res = await request.get('https://api.github.com/users');
} catch (e) {
dispatch(errorGithubApi());
return;
}
const age = JSON.parse(res.text).length;
dispatch(endGithubApi(age));
return;
}
import {ADD_AGE, START_GITHUB_API, ERROR_GITHUB_API, END_GITHUB_API} from '../action';
export function ageApp(state: { age: number; } = {age: 35}, action: { type: string; age: number; }): { age: number; } {
if (action.type === ADD_AGE) {
return {age: state.age + 1};
} else if (action.type === START_GITHUB_API) {
return {age: 0};
} else if (action.type === ERROR_GITHUB_API) {
return {age: 35};
} else if (action.type === END_GITHUB_API) {
return {age: action.age};
}
return state;
}
function middleware(store) {
return (next: any) => (action: any) => {
// 다음 미들웨어 호출, 없으면 dispatch
const returnValue = next(action);
return returnValue;
};
}
import {middlewareA, middlewareB} from './Middleware';
// 스토어를 만들때 순서대로 넣어주면, 순서대로 미들웨어 실행
const store = createStore<{ age: number; }>(ageApp, applyMiddleware(middlewareA, middlewareB));
function middleware(store: Store<{ age: number; }>) {
return (next: any) => (action: any) => {
console.log(`before store : ${JSON.stringify(store.getState())}`); // before
const returnValue = next(action); // 다음 미들웨어 호출, 없으면 실제 dispatch
console.log(`after : ${JSON.stringify(store.getState())}`); // after
return returnValue;
};
}
const store = createStore<{ age: number; }>(ageApp, applyMiddleware(middleware));
// before store : {"age":35}
// after : {"age":36}
export function middlewareA(store: Store<{ age: number; }>) {
return (next: any) => (action: any) => {
console.log(`A before store : ${JSON.stringify(store.getState())}`); // before
const returnValue = next(action); // 다음 미들웨어 호출, 없으면 실제 dispatch
console.log(`A after : ${JSON.stringify(store.getState())}`); // after
return returnValue;
};
}
export function middlewareB(store: Store<{ age: number; }>) {
return (next: any) => (action: any) => {
console.log(`B before store : ${JSON.stringify(store.getState())}`); // before
const returnValue = next(action); // 다음 미들웨어 호출, 없으면 실제 dispatch
console.log(`B after : ${JSON.stringify(store.getState())}`); // after
return returnValue;
};
}
const store = createStore<{ age: number; }>(ageApp, applyMiddleware(middlewareA, middlewareB));
// A before store : {"age":35}
// B before store : {"age":35}
// B after : {"age":36}
// A after : {"age":36}
// import
import thunk from 'redux-thunk';
// 미들웨어 설정
const store = createStore<{ age: number; }>(ageApp, applyMiddleware(middleware, thunk));
export function addAge(): { type: string; } {
return {
type: ADD_AGE
};
}
export function addAgeAsync() {
return (dispatch: any) => {
setTimeout(() => {
dispatch(addAge());
}, 1000);
};
}
// 사용
const App: React.SFC<AppProps & ReactRedux.DispatchProp<{}>> = (props) => {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
나이가 {props.age}
<button onClick={() => props.dispatch(addAgeAsync())}>한해가 지났다.</button>
</p>
</div>
);
};
By Woongjae Lee
타입스크립트 한국 유저 그룹 리액트 스터디 201706
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team