Web Programming/React

React 3. Redux, Redux Toolkit - Global UI Component 만들기

안녕하세요, 씨앤텍시스템즈 이나연 연구원입니다.

 

지난 글에서는 리스트 스타일링까지 진행했습니다. 이번 글에서는 redux에 대해 기본적인 내용을 살펴본 후, Redux를 사용하여 리스트의 데이터를 store에 저장하여 사용해 보겠습니다.

 


이어지는 글

React 1. Component 렌더링

React 2. Emotion을 이용한 React component 스타일링 (with. CSS-in-JS)

React 3. Redux, Redux Toolkit - Global UI Component 만들기

 


목차

  1. Redux 시작하기
    1. Redux
    2. React Redux
    3. Redux Toolkit
  2. Redux를 사용하여 global component list 만들기
    1. 수정 버튼 추가
    2. 모달 컴포넌트 생성
    3. 슬라이스 작성
    4. 스토어 연결
  3. 마무리

 


1. Redux 시작하기

1-1. Redux

첫 번째로, Redux는 대표적으로 어떤 문제를 해결하기 위해 생겨났는지, Redux를 사용하여 어떠한 것들이 가능한지 한 번 함께 살펴보겠습니다.

 

 

Redux - A predictable state container for JavaScript apps. | Redux

A predictable state container for JavaScript apps.

redux.js.org

 

먼저, Redux의 생겨나고 우리가 상태 관리 도구를 사용하게 되는 이유를 살펴보겠습니다.

 

우리가 자바스크립트 애플리케이션을 만드는 과정에서 생각해 봤을 때, state(상태 값)는 특정 시점의 애플리케이션의 상태를 설명하고, 이 상태에 따라 UI가 렌더링 됩니다. 사용자가 클릭 이벤트를 발생시키는 등의 이벤트 발생 시 그에 따른 상태가 업데이트되고 업데이트된 새로운 상태에 따라 UI가 다시 렌더링 됩니다. 아래 이미지와 같이 데이터 흐름을 설명할 수 있습니다.

https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow

 

단순한 기능만을 하는 애플리케이션이라면 하나의 고정된 컴포넌트에서 지정된 이벤트만 따라 데이터가 업데이트되고 UI가 업데이트될 것입니다. 버튼을 클릭하면 count 값이 1씩 증가하는 단순한 카운터를 예로 들 수 있습니다.

하지만 전혀 다른 위치에 있는 다수의 컴포넌트에서 같은 상태를 업데이트할 필요가 있는 등 애플리케이션의 복잡도가 조금만 오른 후 같은 방법으로 상태를 업데이트하고 사용하려고 할 때, 이 단순한 데이터 흐름이 깨지게 됩니다. 상태 관리 도구는 이렇게 발생하는 복잡함을 해결하기 위해 사용한다고 말할 수 있습니다.

 

공식 홈페이지에서는 자바스크립트 애플리케이션을 위한 예측가능한 상태 저장소라고 요약하고 있습니다. 흔히 말하는 상태관리도구라는 말은, 단어 그대로 어떠한 상태를 관리할 수 있게 한다는 것인데, 이 설명에 보태어 설명하면, 자바스크립트 언어를 사용하여 상태를 예측 가능하도록 관리할 수 있는 도구라고도 할 수 있습니다.

 

상태를 예측 가능하도록 한다는 것은 변칙적인 예외 상황이 발생하는 것을 줄이고 일관되게 작동하도록 하는 것을 말합니다.

It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test.

- https://redux.js.org/introduction/getting-started

Redux는 기본적으로 스토어에 상태 정보를 저장하고, action요청을 보내 스토어에 있는 값을 업데이트하여 사용합니다. 예를 들어 React.useState와 비교했을 때, useState는 React 컴포넌트 내에서 사용되어 [state, setState]를 생성하고 props 상속을 통해서만 컴포넌트 간 상태 공유가 가능했다면, Redux의 스토어는 상태 값을 글로벌로 접근할 수 있는 방법을 주며 돕는다고 설명할 수 있습니다.

 

이외에도 Redux와 같은 상태 관리 도구를 사용하며 해결되는 문제들이 있는데, 아래 주요 개념들과 함께 살펴보겠습니다.

 

store

store(스토어)는 아래와 같은 object 형태로 상태 정보를 가지고 있는 상태 값 저장소입니다.

{
  todos: [{
    text: 'Eat lunch',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}

 

그리고 이 스토어는 createStore를 이용하여 다음과 같이 생성합니다.

// createStore(reducer, [preloadedState], [enhancer])
const store = createStore(todos, ['Use Redux']);

이렇게 생성된 스토어는 일반적인 자바스크립트 객체와는 달리 직접 수정하거나 상태 값을 변경할 수 없습니다. 상태를 변경하기 위해서는 action을 전달하는 방법밖에 없습니다. 스토어의 상태 변경 사항을 구독하여 UI 업데이트할 수도 있습니다.

 

action

action 자체는 어떠한 변화를 줄 것인지를 구별하는 type과 action에 함께 사용되는 payload 데이터들을 가지고 있는 평범한 객체로, 상태 값을 어떻게 변경할지에 대한 정보를 가지고 있습니다.

// { type: ACTION_TYPE, ...payload }
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

 

subscribe

스토어의 내장 함수 중 하나로, 함수 형태의 값을 파라미터로 받아, action이 전달되었을 때마다 subscribe 함수로 전달된 함수 호출하고 구독을 취소하는 함수를 반환합니다.

// example,
import store from './store'

const unsubscribe = store.subscribe(() =>
  console.log('State after dispatch: ', store.getState())
)

unsubscribe()

 

dispatching actions

action을 발생시키는 dispatch 함수도 마찬가지로 스토어에 내장되어 있습니다. 아래와 같이 사용합니다.

// example,
import store from './store'

store.dispatch({ 
	type: 'ADD_TODO', 
	text: 'make todo list with redux'
});

dispatch 함수에 action을 파라미터로 전달하여 호출하면, 스토어가 리듀서 함수를 실행하고, action 처리 로직에 따라 상태 값을 업데이트합니다.

 

reducer function

리듀서는 아래 코드와 같이 사용하여, state와 action을 파라미터로 받아서 action type에 따라 변화된 새 상태 값을 계산하여 반환합니다. combineReducers를 사용하여 여러 개의 리듀서를 합쳐 루트 리듀서를 만들 수도 있습니다.

function visibilityFilter(state = 'SHOW_ALL', action) {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter
  } else {
    return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    case 'TOGGLE_TODO':
      return state.map((todo, index) =>
        action.index === index
          ? { text: todo.text, completed: !todo.completed }
          : todo
      )
    default:
      return state
  }
}

위와 같이 리듀서는 action type에 따라 상태 값을 변화시키는데, 반대로 말하면 리듀서에서 정의해놓은 action type과 payload 데이터가 모두 일치하지 않으면 오류가 나게 됩니다. 이렇게 상태 값 관리는 reducer에서만 수행하기 때문에 각 컴포넌트에서 상태를 잘못 변경하더라도 에러를 추적하기 용이합니다.

 

They must be pure functions—functions that return the exact same output for given inputs. They should also be free of side-effects.

위와 같은 설명에 따르면, Redux에서의 리듀서는 순수 함수이어야 하는데, 주어진 input에 대해 동일한 출력을 반환해야 하고 이외의 동작이 실행되어서는 안됩니다. 

예시 상황을 가정했을 때, html, css, javascript을 처음 배우고 세 가지만을 이용하여 UI를 구현하다보면, 아래와 같은 코드를 쓰게 될때가 생각보다 많습니다.

// example,
document.querySelector(".call_to_action_btn").addEventListener("click", function(data) {
  const user = getUserInfo(data);
  ...
  MODAL.style.display = "block";
  MODAL.querySelector(".title").innerText = `${user.name} just call to action!`;
  ...
});

어떠한 액션을 호출하는 버튼을 클릭했을 때, 임의의 데이터를 가지고 사용자의 정보를 가져오고, 가져온 정보를 모달에 추가하여 모달을 화면에 띄워주는 일들을 하나의 콜백에서 모두 처리하고 있습니다. 

하지만 리듀서를 순수 함수로 만들어 상태 변화만 관리하도록 하고, UI 변경 등을 discribe를 이용하여 담당하게 하면, 상태 변화만을 담당하는 코드와 상태에 따라 UI 등을 변경하게 하는 코드를 따로 분리하여 관리하게 됩니다. 이러한 방식으로 스토어에서 상태 값만을 관리하다 보면, Redux 개발도구redux-undo 라이브러리를 사용하여 상태 시간 여행을 보다 쉽게 할 수 있는 바탕이 만들어집니다.

 

1-2. React Redux

두 번째로, Redux 자체는 React에 종속적이지 않기 때문에 React와 함께 사용할 때 발생하는 어려움을 해결하기 위해 React 프로젝트에서는 Redux와 React를 바인딩 하기 위해 React Redux 패키지를 함께 사용합니다.

 

 

React Redux | React Redux

Official React bindings for Redux

react-redux.js.org

 

<Provider />

React Redux는 <Provider /> 컴포넌트를 가지고 있는데, 밑에서 소개할 훅을 사용하여 컴포넌트 내부에서 스토어를 사용할 수 있도록 합니다.

import React from 'react'
import ReactDOM from 'react-dom/client'

import { Provider } from 'react-redux' 
import store from './store'

import App from './views/App'

ReactDOM
  .createRoot(document.getElementById('root'))
  .render(
    <Provider store={store}>
      <App />
    </Provider>
	)

 

hooks: useSelector, useDispatch

먼저 두 훅을 코드로 설명하자면 다음과 같습니다.

const result: any = useSelector(selector: Function, equalityFn?: Function)
const dispatch: Function = useDispatch()

 

useSelector는 selector 함수를 통해 어떤 상태 값을 가져올지 전달하고, 아래와 같이 사용하여 Redux의 store에서 상태를 가져옵니다.

function TodoList() {
  const todos = useSelector((state) => state.todos);
  ...
}

 

useDispatchstore에서 디스패치 함수를 반환하므로 다음과 같이 사용합니다.

function TextField() {
  const [content, setContent] = useState('');
  const dispatch = useDispatch();

  return (
    <form>
      <input value={content} onChange={(event) => setContent(event.target.value)} required />
      <button type="submit" onClick={() => dispatch({ type: 'ADD_TODO', text: content })}>
        add todo item
      </button>
    </form>
	)
}

 

1-3. Redux Toolkit

 

Redux Toolkit | Redux Toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

redux-toolkit.js.org

 

마지막으로 Redux Toolkit은 글을 작성하는 현재(2022.07.20), Redux 로직을 작성하는 표준 방법으로 소개하고 있습니다. 그렇기 때문에 Redux 패키지에서 createStore를 사용하려고 할 때 아래 이미지처럼 createStore 대신 @reduxjs/toolkit에서 configureStore를 사용하는 것을 권장하고 있습니다.

 

https://redux-toolkit.js.org/introduction/getting-started#purpose

공식 문서에서는 1️⃣ 기존의 Redux store는 구성하기 너무 복잡하고, 2️⃣ Redux가 유용한 작업을 수행하기 위해서 많은 패키지가 추가로 필요하고, 3️⃣ Redux를 표준적으로 사용하기 위한 코드가 너무 많은 문제를 해결하기 위해 개발되었다고 함께 소개하고 있습니다.

간단히 말하자면, 앱의 규모가 커짐에 따라 storeaction 함수 관리에 복잡도를 낮추기 위해 만들어졌다고 할 수 있습니다.

기존의 Redux를 사용할 때에는 중복되는 action type의 이름에 접두사를 추가하여 action type을 정의하고 combineReducers를 이용하여 하나의 스토어를 만들었습니다.

// app/module/todos.js
export const TODO_ADDED = 'todos/Added'
export const TODO_TOGGLED = 'todos/Toggled'
export const STATUS_FILTER_CHANGED = 'filters/statusFilterChanged'

export const TOAST_ADDED = 'toast/Added'
export const TOAST_CLOSED = 'toast/Closed'
export const TOAST_REMOVE = 'toast/Romoved'

...

// app/module/todos.js
export const todoAdded = (text) => ({ type: TODO_ADDED, payload: text })
export const todoToggled = (id) => ({ type: TODO_TOGGLED, payload: id })
export const statusFilterChanged = (id) => ({  type: STATUS_FILTER_CHANGED, payload: id })

export default function todos(state = initialState, action) {
  switch (action.type) {
    case TODO_ADDED: 
      return [
        ...state,
        {
          id: nextId,
          text: action.text
        }
      ];
    ...
  }
}

// app/module/index.js
import { combineReducers } from 'redux'
import todos from './todos'
...
export default combineReducers({
  todos,
  ...
})

// app/index.js
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import reducers from './module'

const store = createStore(reducer)

root.render(
  <Provider store={store}>
    <App />
  </Provider>
)

 

다음은 위 코드를 Redux Toolkit을 이용하여 수정한 예시입니다.

// app/store/todoSlice.js
import { createSlice } from '@reduxjs/toolkit'

export const todoSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    add: (state, action) => { state.assign({ id: nextId, text: action.payload.text }) },
    ...
  }
})

export default todoSlice.reducer //todoReducer

// app/store/index.js
import { configureStore } from '@reduxjs/toolkit'
import todoReducer from './todoSlice'
...
export const store = configrueStore({
  reducer: {
    todoReducer,
    ...
  }
})

// app/index.js
import { Provider } from 'react-redux'
import store from './store'

root.render(
  <Provider store={store}>
    <App />
  </Provider>
)

Redux Tollkit을 사용하여 자동으로 각 리듀서에 접두사를 자동으로 만들어 구분하고, reducers에서 정의한 각 액션 함수 내에서 상태 값에 대한 불변성을 자동으로 유지해 주고 있습니다.

기존에 Redux를 사용할 때에는 상태에 대한 불변성(Immutability)을 위해 리듀서에서 return { ...state, id: newId }와 같이 새로운 상태를 반환했지만, Redux Toolkit을 사용할 때에는 Immer 라이브러리를 자동으로 사용하게 되므로 state.id = newId와 같이 새로운 값을 상태에 바로 할당할 수 있습니다.

 

createSlice

Redux Toolkit은 애플리케이션에서 각 기능별로 나누어진 reducer를 슬라이스(slice)라고 이름 붙이고, createSlice를 이용하여 각 reducer slice를 생성하도록 합니다.

createSlice는 슬라이스 이름, 초기 상태, reducer 함수 객체를 파라미터로 받아 자동으로 action creator와 action type을 생성합니다.

// Parameters
function createSlice({
  // action type으로 사용될 이름
  name: string,

  // reducer의 초기값
  initialState: any,

  // "case reducers" 객체의 각 key 값을 액션의 이름으로 사용
  reducers: Object<string, ReducerFunction | ReducerAndPrepareObject>

  // "builder callback" 함수는 더 많은 reducers나 추가적인 "case reducers" 객체를 추가하기 위해 사용하며,
  // key 값은 다른 action type이어야 함.
  extraReducers?:
  | Object<string, ReducerFunction>
  | ((builder: ActionReducerMapBuilder<State>) => void)
})

 

이렇게 생성된 슬라이스는 다음과 같은 형태로 슬라이스를 만들어 반환합니다.

// Return Value
{
  name : string,
  reducer : ReducerFunction,
  actions : Record<string, ActionCreator>,
  caseReducers: Record<string, CaseReducer>.
  getInitialState: () => State
}

 

configureStore

configureStore는 필수로 options에서 reducer를 받습니다. 이 리듀서가 단일 함수일 경우 store의 루트 리듀서로 적용되고, 객체 형태의 slice reducers일 경우 자동으로 Redux의 combineReducers 유틸리티에 전달하여 루트 리듀서를 만듭니다.

// Parameters
function configureStore({
  reducer: Reducer<any, AnyAction> | ReducerMapObject<any, AnyAction>
  ...
})

이 외에도 middleware, devTools, preloadedState, enhancers를 options 안에 가질 수 있습니다.

 

 

2. Redux를 사용하여 global component list 만들기

마찬가지로 이전에 스타일링한 List 화면을 활용해 보겠습니다.

 

이 컴포넌트를 수정하려면 어떻게 해야 할까요? 각 리스트마다 수정 버튼을 추가하거나 더블클릭 등의 이벤트에 따라 수정 컴포넌트로 전환할 수도 있겠지만, 이번 글에서는 Redux Toolkit을 활용하여 리스트 수정 모달을 띄워 상태를 목록 전체의 정보를 수정할 수 있도록 해보겠습니다.

 

2-1. 수정 버튼 추가

먼저 리스트에 ActionWrapper를 복사하여 수정 버튼을 추가합니다.

// src/view/ListWrapper.js
export default function ListWrapper() {
  ...
  const handleOpenEditModal = () => {};

  return (
    <OuterWrapper>
      ...
      <ActionWrapper>
        <button className="editBtn" onClick={handleOpenEditModal}>
          Edit list content
        </button>
      </ActionWrapper>
    </OuterWrapper>
  );
}

 

2-2. 모달 컴포넌트 생성

수정 버튼을 추가했다면, 버튼을 클릭했을 때 등장할 글로벌 모달 컴포넌트를 생성하여 스타일을 추가하고, 모달에서 버튼을 클릭하면 모달이 사라지도록 하는 핸들러 함수를 임시로 전달합니다.

// src/components/ModalWrapper.js
import React from "react";
import styled from "@emotion/styled";

const Dialog = styled.dialog`
	...
`;

export default function ModalWrapper() {
  const [open, setOpen] = React.useState(false);
  const handleModalHide = () => setOpen(false);

  const list = (
    <ul>
      <li>
        <input />
      </li>
    </ul>
  );

  return (
    <Dialog open={open}>
      <form method="dialog">
        {list}
        <div className="modal-btns">
          <button
            onClick={handleModalHide}
            className="modal-btns__cancel"
          >
            Cancel
          </button>
          <button
            type="submit"
            onClick={handleModalHide}
            className="modal-btns__submit"
          >
            Edit
          </button>
        </div>
      </form>
    </Dialog>
  );
}

 

2-3. 슬라이스 작성

먼저 모달의 open 속성을 제어할 modalSlice를 다음과 같이 작성합니다. 현재는 리스트를 수정할 폼을 출력하기만 할 뿐이기 때문에 상태 값으로 isOpen 속성 하나만 가집니다.

// src/store/modalSlice.js
import { createSlice } from "@reduxjs/toolkit";

const modalSlice = createSlice({
  name: "modal",
  initialState: {
    isOpen: false
  },
  reducers: {
    show: (state) => {
      state.isOpen = true;
    },
    hide: (state) => {
      state.isOpen = false;
    }
  }
});

export default modalSlice.reducer;

export const { show, hide } = modalSlice.actions;

 

리스트의 값을 전역적으로 사용해야 하기 때문에 listSlice도 작성합니다.

// src/store/listSlice.js
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  items: [
    { id: 0, text: "0" },
    { id: 1, text: "1" },
    { id: 2, text: "2" },
    { id: 3, text: "3" }
  ],
  orderedAsc: true
};

const listSlice = createSlice({
  name: "list",
  initialState,
  reducers: {
    addOne: (state) => {
      state.items.push({
        id: state.items.length,
        text: String(state.items.length)
      });
    },
    removeOne: (state) => {
      const maxIdNum = state.items.reduce(
        (max, cur) => Math.max(max, cur.id),
        -Infinity
      );
      state.items = state.items.filter((item) => item.id !== maxIdNum);
    },
    toggleOrder: (state) => {
      if (state.orderedAsc) {
        state.items = state.items.sort((a, b) => b.id - a.id);
      } else {
        state.items = state.items.sort((a, b) => a.id - b.id);
      }
      state.orderedAsc = !state.orderedAsc;
    },
    editList: (state, action) => {
      state.items = action.payload.items;
    }
  }
});

export default listSlice.reducer;

export const { addOne, removeOne, toggleOrder, editList } = listSlice.actions;

 

2-4. 스토어 연결

스토어를 UI 컴포넌트에서 사용하기 위해 가장 먼저 <App /><Provider />로 감싼 후 위에서 작성한 store를 전달합니다.

// src/index.js
import { Provider } from 'react-redux'
...
import store from './store'
...
root.render(
  <Provider store={store}>
    <App />
  </Provider>
)

 

store를 앱 내에서 사용할 수 있게 되었다면, ListWrapper에서 리스트 액션과 모달 액션을 가져와 각 이벤트에 맞게 액션을 디스패치합니다.

// src/view/ListWrapper.js
import { useDispatch, useSelector } from "react-redux";
import { addOne, removeOne, toggleOrder } from "../store/listSlice";
import { show } from "../store/modalSlice";

export default function ListWrapper() {
  const dispatch = useDispatch();
  const { items } = useSelector((state) => state.list);

  const handleAddOneItem = () => dispatch(addOne());
  const handleRemoveOneItem = () => dispatch(removeOne());
  const handleToggleOrder = () => dispatch(toggleOrder());
  const handleOpenEditModal = () => dispatch(show());
  ...
}

 

마지막으로 ModalWrapper에서도 isOpen 값과 list의 items 값을 가져와서 사용할 수 있도록 다음과 같이 훅을 먼저 작성합니다.

// src/components/useModalWrapper.js
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";

import { hide, show } from "../store/modalSlice";

export const useModal = () => ({
  modalOpen: useSelector((state) => state.modal.isOpen),
  listItems: useSelector((state) => state.list.items)
});

export const useShowModal = () => {
  const dispatch = useDispatch();
  return useCallback(() => dispatch(show()), [dispatch]);
};

export const useUnmountModal = () => {
  const dispatch = useDispatch();
  return {
    handleUnmount: useCallback(() => dispatch(hide()), [dispatch])
  };
};

 

그 후 ModalWrapper에서 방금 작성한 훅을 사용하여 리스트 아이템을 수정할 수 있는 모달을 만듭니다.

// src/components/ModalWrapper.js
import { useDispatch, useSelector } from "react-redux";
import { hide } from "../store/modalSlice";
import { ListItemForm } from "./List";
...
export default function ModalWrapper() {
  ///
  const list = (
    <ul>
      {items.map((item) => (
        <ListItemForm
          key={item.id}
          id={item.id}
          value={item.text}
          readOnly={false}
          onChange={handleChangeList}
        />
      ))}
    </ul>
  );

  return (
    <Dialog open={modalOpen}>
      ...
    </Dialog>
  );
}

 

ModalWrapper모두 작성했다면 App 내부에 Wrappper를 추가합니다.

// src/App.js
...
import { useModal } from "./components/useModalWrapper";

export default function App() {
  const { modalOpen } = useModal();
  return (
    <div
      className={css`
        height: 100vh;
      `}
    >
      <ListWrapper />
      {modalOpen && <ModalWrapper />}
    </div>
  );
}

 

 

*(참고)전체 코드 https://codesandbox.io/s/component-with-redux-store-e4bx4n

 

 

3. 마무리

지금까지 Redux와 React Redux, Redux Toolkit에 대한 기본적인 내용을 훑어보고, React Redux와 Redux Toolkit을 이용하여 React 애플리케이션에서 글로벌 컴포넌트를 만들어보기까지 했습니다. 이 외의 내용들은 공식 문서에 잘 나와있으니 언제나처럼 공식 문서를 제일 먼저 참고하시기 바랍니다.

 

마지막으로, 모든 상황에 Redux가 필요한 것은 아니므로 Redux를 애플리케이션 개발에 도입하기 이전에 읽어보면 좋을, 공식 문서에서 안내하고 있는 세 개의 글을 공유드립니다.

 

 

When (and when not) to reach for Redux

I am a huge proponent of a couple of specific ideas. One is that you should always try to understand what problems a specific tool is trying to solve… And another is that you need to understand exactly what problems you are trying to solve in your own ap

changelog.com

 

 

Idiomatic Redux: The Tao of Redux, Part 1 - Implementation and Intent

Thoughts on what Redux requires, how Redux is intended to be used, and what is possible with Redux

blog.isquaredsoftware.com

 

 

You Might Not Need Redux

People often choose Redux before they need it. “What if our app doesn’t scale without it?” Later, developers frown at the indirection Redux…

medium.com

 


(참고한 자료)

728x90