'Javascript/Redux'에 해당되는 글 4건

  1. 2020.02.25 #13 Redux in React
  2. 2020.02.25 #12 Redux
  3. 2019.09.02 middleware
  4. 2019.08.29 Redux Life Cycle
2020. 2. 25. 17:02

0. 작업환경설정

1. UI 구성

2. 프레젠테이셔널 컴포넌트 생성

3. 리덕스 생성

4. 리액트 앱에 리덕스 적용

5. 컨테이너 컴포넌트 생성

6. redux-actions라이브러리 적용

 

 

0. 작업환경설정

$ yarn create react-app react-redux-tutorial
$ cd react-redux-tutorial
$ yarn add redux react-redux
// redux 라이브러리 : createStore함수를 사용하여 스토어생성
// react-redux 라이브러리 : connect함수와 Provider컴포넌트를 사용하여 리액트에서 리덕스 관련작업 처리

 

 

1. UI 구성

리덕스를 사용한 리액트 애플리케이션 UI 구조

  • 프레젠테이셔널 컴포넌트 : 상태관리가 이루어지지 않고, 그저 props를 받아 와서 화면에 UI를 보여 주기만 하는 컴포넌트
  • 컨테이너 컴포넌트 : 리덕스와 연동되어 있는 컴포넌트. 리덕스로 부터 상태를 받아오기도 하고 리덕스 스토어에 액션을 디스패치 함.

 

2. 프레젠테이셔널 컴포넌트 생성

// src/components/Todos.js

import React from 'react';

const TodoItem = ({ todo, onToggle, onRemove }) => {
  return (
    <div>
      <input type="checkbox" />
      <span>text</span>
      <button>remove</button>
    </div>
  );
};

const Todos = ({
  input,
  todos,
  onChangeInput,
  onInsert,
  onToggle,
  onRemove,
}) => {
  const onSubmit = e => {
    e.preventDefault();
  };

  return (
    <>
      <form onSubmit={onSubmit}>
        <input />
        <button type="submit">register</button>
      </form>
      <div>
        <TodoItem />
        <TodoItem />
        <TodoItem />
        <TodoItem />
      </div>
    </>
  );
};

export default Todos;

 

// src/App.js

import React from 'react';
import Todos from './components/Todos';
import './App.css';

function App() {
  return (
    <div>
      <Todos />
    </div>
  );
}

export default App;

 

3. 리덕스 생성

  • 일반적인 디렉토리 구조 : actions, constatns, reducers 3개의 디렉토리를 만들고 그안에 기능별로 파일을 하나씩 만든다
  • Ducks패턴 : 액션타입, 액션생성함수, 리듀서 모두를 '모듈'이라는 하나의 파일에 몰아서 작성하는 방식
// 모듈 생성
// src/modules/Todos.js : Ducks패턴

//action type
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';

//action creator
export const changeInput = input => ({
  type: CHANGE_INPUT,
  input,
});

let id = 3; //insert가 호출될 때마다 1씩 증가
export const insert = text => ({
  type: INSERT,
  todo: {
    id: id++,
    text,
    done: false,
  },
});

export const toggle = id => ({
  type: TOGGLE,
  id,
});

export const remove = id => ({
  type: REMOVE,
  id,
});

// initial state
const initialState = {
  input: '',
  todos: [
    {
      id: 1,
      text: 'todo job 1',
      done: true,
    },
    {
      id: 2,
      text: 'todo job2',
      done: false,
    },
  ],
};

//reducer
function todos(state = initialState, action) {
  switch (action.type) {
    case CHANGE_INPUT:
      return {
        ...state,
        input: action.input,
      };
    case INSERT:
      return {
        ...state,
        todos: state.todos.concat(action.todo), //concat : 배열에 원소추가
      };
    case TOGGLE:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.id ? { ...todo, done: !todo.done } : todo,
        ),
      };
    case REMOVE:
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.id), // filter의 파라미터로 전달된 함수의 조건을 만족하는 요소들로만 구성된 새로운 배열 반환
      };
    default:
      return state;
  }
}

export default todos;

 

// 루트 리듀서 생성
// src/modules/index.js

import { combineReducers } from 'redux';
import Todos from './Todos';

// 스토어를 만들 때는 리듀서를 하나만 사용해야 하므로
// 리듀서가 여러개인 경우 combinereducers함수를 통해 하나로 묶는다.
const rootReducer = combineReducers({
  Todos,
});

export default rootReducer;

 

4. 리액트 앱에 리덕스 적용

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

import { createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';

const store = createStore(rootReducer);	// 스토어 생성

// Provider 컴포넌트로 리액트 프로젝트에 리덕스 적용
ReactDOM.render(
  <Provider store={store}>		
    <App />
  </Provider>,
  document.getElementById('root'),
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

 

5. 컨테이너 컴포넌트 생성

  • 컨테이너 컴포넌트와 리덕스를 연동하려면 react-redux에서 제공하는 connect함수를 사용
    => connect( mapStateToProps, mapDispatchToProps)(연동할컴포넌트)
    • mapStateToProps : 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위한 함수. 현재 스토어 안의 state를 파라미터로 받는다
    • mapDispatchToProps : store의 내장함수 dispatch를 파라미터로 받아 이를 이용하여 액션생성함수를 컴포넌트의 props로 넘겨주기 위한 함수. 액션생성함수를 객체형태로 전달만 해도 connect 함수가 자동bindActionCreators 작업을 수행하므로 dispatch를 파라미터로 받지 않아도 된다.
    • mapStateToPropsmapDispatchToProps 에서 반환하는 객체 내부의 값들은 컴포넌트의 props로 전달된다.
// src/containers/TodosContainer.js

import React from 'react';
import { connect } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/Todos';
import Todos from '../components/Todos';

const TodosContainer = ({
  input,
  todos,
  changeInput,
  insert,
  toggle,
  remove,
}) => {
  return (
    <Todos
      input={input}
      todos={todos}
      onChangeInput={changeInput}
      onInsert={insert}
      onToggle={toggle}
      onRemove={remove}
    />
  );
};

const mapStateToProps = state => {
  return {
    input: state.todos.input,
    todos: state.todos.todos,
  };
};

// dispatch를 파라미터로 받지 않고, 액션생성함수를 객체형태로 전달만 해도
// connect 함수가 자동bindActionCreators 작업을 수행한다.
const mapDispatchToProps = {
  changeInput,
  insert,
  toggle,
  remove,
};

export default connect(mapStateToProps, mapDispatchToProps)(TodosContainer);

 

 

// src/App.js

import React from 'react';
import TodosContainer from './containers/TodosContainer';
import './App.css';

function App() {
  return (
    <div>
      <TodosContainer />
    </div>
  );
}

export default App;

 

// src/components/Todos.js

import React from 'react';

const TodoItem = ({ todo, onToggle, onRemove }) => {
  return (
    <div>
      <input
        type="checkbox"
        onClick={() => onToggle(todo.id)}
        checked={todo.done}
        readOnly={true}
      />
      <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
        {todo.text}
      </span>
      <button onClick={() => onRemove(todo.id)}>remove</button>
    </div>
  );
};

const Todos = ({
  input,
  todos,
  onChangeInput,
  onInsert,
  onToggle,
  onRemove,
}) => {
  const onSubmit = e => {
    e.preventDefault();
    onInsert(input);
    onChangeInput('');
  };
  const onChange = e => onChangeInput(e.target.value);
  return (
    <>
      <form onSubmit={onSubmit}>
        <input value={input} onChange={onChange} />
        <button type="submit">register</button>
      </form>
      <div>
        {todos.map(todo => (
          <TodoItem
            todo={todo}
            key={todo.id}
            onToggle={onToggle}
            onRemove={onRemove}
          />
        ))}
      </div>
    </>
  );
};

export default Todos;

 

6. redux-actions 라이브러리 적용

  • 액션생성함수, 리듀서를 작성할 때 redux-actions 라이브러리 활용하여 리액트 앱에서 리덕스를 훨씬 편하게 사용할 수 있다.
  • createAction : 매번 객체를 직접 만들어 줄 필요 없이 간단하게 액션 생성 함수 선언 가능
  • handleAction : 첫 번째 파라미터에는 각 액션에 대한 업데이트 함수, 두 번째 파라미터에는 초기 상태 전달.

 

 

 

'Javascript > Redux' 카테고리의 다른 글

#12 Redux  (0) 2020.02.25
middleware  (0) 2019.09.02
Redux Life Cycle  (0) 2019.08.29
Posted by yongminLEE
2020. 2. 25. 17:02

 

 

 

MVC vs Flux 

MVC의 문제점

  • MVC
    • 정의 : 애플리케이션 개발시 가장 많이 사용되는 디자인패턴으로, 컨트롤러(Controller)는 모델(Model)의 데이터를 조회하거나 업데이트하는 역할을 하며, 모델(Model)의 변화는 뷰(View)에 반영한다. 또한, 사용자는 뷰를 통해 데이터를 입력하는데 사용자의 입력은 모델에 영향을 준다.
    • 문제점 : 페이스북과 같은 대규모 애플리케이션에서는 Model이나 Model과 관련한 View가 대량으로 시스템에 추가되면 복잡도가 폭발적으로 증가하는데, 이 같은 의존성과 순차적 업데이트는 종종 데이터의 흐름을 꼬이게 하여 예기치 못한 결과를 불러일으킨다.

Flux 패턴

  • Flux
    • 정의 : MVC 패턴의 문제를 해결하기 위해 고안된 아키텍쳐
    • 단방향 데이터 흐름(unidirectional data flow) : Flux의 가장 큰 특징으로, 데이터의 흐름은 언제나 디스패처(Dispatcher)에서 스토어(Store)로, 스토어에서 뷰(View)로, 뷰에서 액션(Action)으로 다시 액션에서 디스패처로 흐른다.
    • 구성요소
      • dispatcher : Flux 애플리케이션의 모든 데이터 흐름을 관리하는 허브 역할. 
      • action : 액션생성자에의해 생성되고 상태 업데이트 할 때 참고해야 할 값을 가진 객체
      • store : 애플리케이션의 상태를 저장및 action객체를 수신하여 새로운 상태를 만들어서 반환

 

Redux

redux life cycle

  • 정의 : React에서 State를 좀더 효율적으로 관리하는데 사용하는 flux 패턴의 상태관리 라이브러리
  • 존에는 부모에서 자식의 자식의 자식까지 상태가 흘렀었는데, 리덕스를 사용하면 스토어를 사용하여 상태를 컴포넌트 구조의 바깥에 두어 스토어를 중간자로 두고 상태를 업데이트 하거나, 새로운 상태를 전달받음. 따라서, 여러 컴포넌트를 거쳐서 받아올 필요 없이(종속성 제거) 아무리 깊숙한 컴포넌트에 있다 하더라도 직속 부모에게서 받아오는 것 처럼 원하는 상태값을 골라서 props 를 편리하게 받아올 수 있음.
  • 리덕스 구조
    • action : 상태변화를 일으킬 때 참조하는 객체
    • action creator : 액션객체를 생성하는 함수
    • reducer : 현재상태와 액션객체를 파라미터로 받아 새로운 상태를 반환하는 함수
    • store : 애플리케이션의 상태, 리듀서, 몇가지 함수들을 내장
    • dispatch : 스토어에 내장된 함수로 액션객체를 파라미터로 받아 액션객체를 스토어에게 전달한다. 이후, 스토어는 리듀서 함수를 실행.
    • subscribe : 스토어의 내장 함수. 파라미터로 리스너 함수를 전달하여 subscribe함수를 호출하면, 액션이 디스패치되어 상태가 업데이트될 때마다 리스너함수를 실행.
  • 리덕스 라이프 사이클
    • event 발생 -> action creator 호출
    • action creator는 action(object) 생성
    • 생성된 action은 모든 middleware를 거쳐 모든 reducer들에게 전달
    • reducer는 action type에 따른 state반환
    • state는 app의 state에 덮어쓰이고 모든 container들에게 전달
    • 컴포넌트 리렌더링 
  • 상태업데이트 예시

1. 스토어설정
2. 컴포넌트의 스토어 subscribe
3. dispatch
4. 상태 업데이트

 

5. 리렌더링

  • 리덕스 규칙
    • 단일 스토어 : 하나의 애플리케이션에는 하나의 스토어만 존재
    • 읽기 전용 : 리덕스의 상태는 항상 읽기 전용
    • 리듀서는 의존성 없는 순수함수 : 리듀서 함수는 인전상태와 액션 객체를 파라미터로 받고 파라미터 외의 값에는 의존하지 않는 순수함수이다.

 

 

참고 : https://velopert.com/3528

'Javascript > Redux' 카테고리의 다른 글

#13 Redux in React  (0) 2020.02.25
middleware  (0) 2019.09.02
Redux Life Cycle  (0) 2019.08.29
Posted by yongminLEE
2019. 9. 2. 12:19

1. middleware?

: 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기 전에 사전에 지정된 작업들을 실행

-> 액션과 리듀서 사이의 중간자

 

1-1. loggerMiddleware.js 구현

const middleware = store => next => action => {
    // 현재 스토어 상태 값 기록
    console.log('현재 상태', store.getState());
    // 액션 기록
    console.log('액션',action);

    // 액션을 다음 미들웨어 또는 리듀서에 전달
    const result = next(action);

    // 액션 처리 후 스토어의 상태를 기록
    console.log('다음 상태', store.getState());
    
    return result; // 여기에서 반환하는 값은 store.dispatch(ACTION_TYPE)했을 때 결과로 설정한다.

}

export default middleware;// 내보내기

- next(action) 을 했을 때는 그다음 처리해야 할 미들웨어로 액션을 넘겨주고, 추가로 처리할 미들웨어가 없다면 바로 리듀서에 넘겨준다

- store.dispatch는 다음 미들웨어로 넘기는 것이 아니라 액션을 처음부터 디스패치한다

 

1-2. 미들웨어 적용

import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import middleware from './lib/middleware';

// 미들웨어가 여러 개일 경우 파라미터로 전달하면 된다. (applyMiddleware(a,b,c))
// 미들웨어 순서는 여기에서 전달한 파라미터 순서대로 지정한다.
const store = createStore(modules, applyMiddleware(middleware));

export default store;

 

1-3. redux-logger 라이브러리

import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import {createLogger} from 'redux-logger';

const logger = createLogger();
const store = createStore(modules, applyMiddleware(logger));

export default store;

 

1-4 . 결과

*크롬브라우저의 콘솔창 확인

 

 

2. 미들웨어로 비동기 액션 처리

: 여러 오픈소스 라이브러리 존재 : redux-thunk, redux-saga, redux-pender, redux-promise-middleware etc...

 

2-1. redux-thunk 모듈

- thunk : 특정작업을 나중에 할 수 있도록 함수 형태로 감싼 것

- redux-thunk : 함수를 디스패치함으로써 일반 액션 객체로는 할 수 없는 작업을 가능하게함

 

ex) 1초 뒤에 액션이 디스패치되는 코드

import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import {createLogger} from 'redux-logger';
import ReduxThunk from 'redux-thunk';

const logger = createLogger();
const store = createStore(modules, applyMiddleware(logger, ReduxThunk));

export default store;

 

import {handleActions, createAction} from 'redux-actions';

//action types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

//action creators
export const increment = createAction(INCREMENT);
export const decrement = createAction(DECREMENT);

//thunks
export const incrementAsync = () => dispatch => {
    // 1초 뒤 액션 디스패치
    setTimeout(
        () => { dispatch(increment()) }, //함수를 dispatch
        1000
    );
}
export const decrementAsync = () => dispatch => {
    // 1초 뒤 액션 디스패치
    setTimeout(
        () => { dispatch(decrement()) }, //함수를 dispatch
        1000
    );
}

//reducer
export default handleActions({
    [INCREMENT]: (state, action) => state +1,
    [DECREMENT]: (state, action) => state -1
}, 0);

 

3. Promise

- Promise : 비동기 처리를 다루는데 사용하는 객체

function printLater(number, fn) {
    setTImeout(
        function() {
            console.log(number);
            if(fn) fn();
        },
        1000
    );
}

//콜백지옥
printLater(1, function() {
    printLater(2, function() {
        printLater(3, function() {
            printLater(4);
        })
    })
});

위와 같은 콜백지옥( 비동기적 작업에 의한 깊고 복잡해진 코드)을 Promise를 통해 다음과 같이 해결

function printLater(number) {
    return new Promise( // Promise 생성 후 리턴
        (resolve, reject) => {
            if ( number > 4 ) {
                return reject('number is greater than 4'); // reject 는 오류를 발생시킨다.
            }
            setTimeout( // 1초 뒤 실행
                () => {
                    console.log(number);
                    resolve(number + 1); // 현재 숫자에 1을 더한 값을 반환한다.
                }, 1000
            );
        }
    );
}

printLater(1)
    .then( num => printLater(num) )
    .then( num => printLater(num) )
    .then( num => printLater(num) )
    .then( num => printLater(num) )
    .catch( e => console.log(e) );

Promise 에서 결과 값을 반환할 때는 resolve(결과 값) 을 작성하고, 오류를 발생시킬 때는 reject(오류)를 작성.

- 여기에서 반환하는 결과 값은.then(), 오류 값은.catch() 에 전달하는 함수의 파라미터로 설정됨.

 

4. redux-thunk와 axios를 사용한 웹 요청 처리

- axios : Promise 기반 HTTP 클라이언트 라이브러리

yarn add axios

 

 

4-1. post 모듈 : API함수, 액션, 액션생성함수, thunk, reducer

// src/modules/post.js
import {handleActions, createAction} from 'reudx-actions';

import axios from 'axios';

//API function
funciton getPostAPI(postId) {
    return axios.get(`url/${postId}`);	//Promise 객체형태로 반환
}

//action types
const GET_POST_PENDING = 'GET_POST_PENDING';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_FAILURE = 'GET_POST_FAILURE';

//action creators
const getPostPending = createAction(GET_POST_PENDING);
const getPostSuccess = createAction(GET_POST_SUCCESS);
const getPostFailure = createAction(GET_POST_FAILURE);

//thunk
export const getPost = (postId) => dispatch => {
    dispatch(getPostPending()); // 요청 시작했다는 것을 알림
    
    // 요청 시작. 여기에서 만든 promise를 return해야 나중에 컴포넌트에서 호출할 때 getPost().then(...)을 할 수 있다.
    return getPostAPI(postId)
    .then( () => {
        // 요청이 성공했다면 서버 응답 내용을 payload로 설정하여 GET_POST_SUCCESS 액션을 디스패치한다.
        dispatch(getPostSuccess(response));
        
        // 후에 getPostAPI.then 을 했을 때 then에 전달하는 함수에서 response 에 접근할 수 있게 한다.
        return response;
    } )
    .catch( error => {
        // 오류가 발생하면 오류 내용을 payload로 설정하여 GET_POST_FAILURE 액션을 디스패치한다.
        dispatch(getPostFailure(error));
        
        // error를 throw하여 이 함수를 실행한 후 다시 한 번 catch를 할 수 있게 한다.
        throw(error);
    } );
}

const initialState = {
    pending: false,
    error: false,
    data: {
        title: '',
        body: ''
    }
}

//reducer
export default handleAction({
    [GET_POST_PENDING]: (state, action) => {
        return {
            ...state,
            pending: true,
            error: false
        };
    },
    [GET_POST_SUCCESS]: (state, action) => {
        const {title, body} = action.payload.data;
        return {
            ...state,
            pending: false,
            data: {
                title,
                body
            }
        };
    },
    [GET_POST_FAILURE]: (state, action) => {
        return {
            ...state,
            pending: false,
            error: true
        }
    }
}, initialState);

- thunk : 요청상태에 따라 액션을 디스패치

 

4-2. post 모듈의 리듀서를 루트 리듀서에 넣어준다

import {combineReducers} from 'redux';
import counter from './counter';
import post from './post';

export default combineReducers({
    counter,
    post
});

 

4-3. counter의 기본 값을 postId로 사용하므로 1로 설정, 0이면 오류 발생

// scr/modules/counter.js
(...)
export default handleActions({
    [INCREMENT]: (state, action) => state + 1,
    [DECREMENT]: (state, action) => state - 1
},1); // counter의 기본 값을 postId로 사용하므로 1로 설정, 0이면 오류 발생

 

4-4. App 컴포넌트에서 액션으로 웹 요청 시도

// src/App.js
import React, {Component} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as counterActions from './modules/counter';
import * as postActions from './modules/post';

class App extends Component {
    loadData = () => {
        const {PostActions, number} = this.props;
        PostActions.getPost(number);
    }

    componentDidMount() {
        this.loadData();
    }
    
    componentDidUpdate(prevProps, prevState) {
        // 이전 number 와 현재 number 가 다르면 요청을 시작
        if(this.props.number != prevProps.number) {
            this.loadData();
        }
    }

    render() {
        const {CounterActions, number, post, error, loading} = this.props;
        return (
            <div>
                <h1>{number}</h1>
                {
                    ( () => {
                        if (loading) return (<h2>로딩중...</h2>);
                        if (error) return (<h2>에러발생...</h2>);
                        return (
                            <div>
                                <h2>{post.title}</h2>
                                <p>{post.body}</p>
                            </div>
                        );
                    } )
                }
                <button onClick={CounterActions.increment}>+</button>
                <button onClick={CounterActions.decrement}>-</button>
            </div>
        );
    }
}

export default connect(
    (state) => ({
        number: state.counter,
        post: state.post.data,
        loading: state.post.pending,
        error: state.post.error
    }),
    (dispatch) => ({
        CounterActions: bindActionCreators(counterActions, dispatch),
        PostActions: bindActionCreators(postActions, dispatch)
    })
)(App);

 

4-5. 요청 완료후 작업및 오류 발생시 작업 추가

// es6
// loadData = () => {
//     const { PostActions, number } = this.props;
//    //this.props.Postactions.getPost(this.props.number);
//
//     PostActions.getPost(number).then(	// 요청 완료 후 작업
//         (response) => {
//             console.log(response);
//         }
//     ).catch(					// 오류 발생시 작업
//         (error) => {
//             console.log(error);
//         }
//     );
// };

//es7
loadData = async () => {
    const { PostActions, number } = this.props;
    // this.props.Postactions.getPost(this.props.number);
    
    try {
        const response = await PostActions.getPost(number); // 요청 완료 후 작업
        console.log(response);
    }
    catch (error) {                                        // 오류 발생시 작업
        console.log(error);
    }
};

- await를 쓸 함수의 앞부분에 async 키워드를 붙인다.

- 기다려야 할 Promise 앞에 await 키워드를 붙인다.

- await를 사용할 때는 꼭 try~catch문으로 오류를 처리해야 한다.

 

4-6. 요청취소기능 추가

 

 

*redux-pender, redux-promise-middleware는 잘 쓰이지 않으므로 스킵..

'Javascript > Redux' 카테고리의 다른 글

#13 Redux in React  (0) 2020.02.25
#12 Redux  (0) 2020.02.25
Redux Life Cycle  (0) 2019.08.29
Posted by yongminLEE
2019. 8. 29. 18:09

 

 

1. event 발생 -> action creator 호출

2. action creator는 action(object) 생성

3. 생성된 action은 모든 middleware를 거쳐 모든 reducer들에게 전달

4. reducer는 action type에 따른 state반환

5. state는 app의 state에 덮어쓰이고 모든 container들에게 전달

6. 컴포넌트 리렌더링 

'Javascript > Redux' 카테고리의 다른 글

#13 Redux in React  (0) 2020.02.25
#12 Redux  (0) 2020.02.25
middleware  (0) 2019.09.02
Posted by yongminLEE