Redux와 Redux-Toolkit

e-pong:)·2022년 11월 29일
0

Redux란?

리덕스란, 자바스크립트 애플리케이션에서 상태 관리를 효율적으로 관리 할 수 있게 도와주는 도구이다.
복잡한 상태 관리가 이루어지는 SPA(Single Page Application)에서 특히 유용하게 사용된다.

Redux의 세가지 규칙

  1. Single source of truth
    동일한 데이터는 항상 같은 곳에서 가지고 와야 한다는 의미이다.
    즉, Redux에는 데이터를 저장하는 Store라는 단 하나뿐인 공간이 있음과 연결이 되는 원칙이다.
  2. State is read-only
    상태는 읽기 전용이라는 뜻으로, React에서 상태갱신함수로만 변경할 수 있었던 것처럼, Redux의 상태도 직접 변경할 수 없음을 의미한다. 즉, Action 객체가 있어야만 상태를 변경할 수 있다는 원칙이다.
  3. Changes are made with pure funcions
    변경은 순수함수로만 가능하다는 뜻으로, 상태가 엉뚱한 값으로 변경되는 일이 없도록 순수함수로 작성되어야하는 Reducer와 연결되는 원칙이다.

Redux 용어

Store

Store는 상태가 관리되는 오직 하나뿐인 저장소의 역할을 한다. Redux 앱의 state가 저장되어 있는 공간이다.
아래 코드와 같이 creatStore 메서드를 활용해 Reducer를 연결해서 Store를 생성할 수 있다.

import { createStore } from 'redux';

const store = createStore(rootReducer);

Action

Action은 말 그대로 어떤 액션을 취할 것인지 정의해 놓은 객체로, 다음과 같은 형식으로 구성됩니다.

// payload가 필요 없는 경우
{ type: 'INCREASE' }

// payload가 필요한 경우
{ type: 'SET_NUMBER', payload: 5 }

여기서 type 은 필수로 지정을 해 주어야 합니다. 해당 Action 객체가 어떤 동작을 하는지 명시해주는 역할을 하기 때문이며, 대문자와 Snake Case로 작성합니다. 여기에 필요에 따라 payload 를 작성해 구체적인 값을 전달합니다.

보통 Action을 직접 작성하기보다는 Action 객체를 생성하는 함수를 만들어 사용하는 경우가 많습니다. 이러한 함수를 액션 생성자(Action Creator)라고도 합니다.

// payload가 필요 없는 경우
const increase = () => {
  return {
    type: 'INCREASE'
  }
}

// payload가 필요한 경우
const setNumber = (num) => {
  return {
    type: 'SET_NUMBER',
    payload: num
  }
}

Reducer

Reducer는 Dispatch에게서 전달받은 Action 객체의 type 값에 따라서 상태를 변경시키는 함수입니다.

const count = 1

// Reducer를 생성할 때에는 초기 상태를 인자로 요구합니다.
const counterReducer = (state = count, action) => {

  // Action 객체의 type 값에 따라 분기하는 switch 조건문입니다.
  switch (action.type) {

    //action === 'INCREASE'일 경우
    case 'INCREASE':
			return state + 1

    // action === 'DECREASE'일 경우
    case 'DECREASE':
			return state - 1

    // action === 'SET_NUMBER'일 경우
    case 'SET_NUMBER':
			return action.payload

    // 해당 되는 경우가 없을 땐 기존 상태를 그대로 리턴
    default:
      return state;
	}
}
// Reducer가 리턴하는 값이 새로운 상태가 됩니다.

Dispatch

Dispatch는 ReducerAction을 전달해주는 함수입니다. Dispatch의 전달인자로 Action 객체가 전달됩니다.

// Action 객체를 직접 작성하는 경우
dispatch( { type: 'INCREASE' } );
dispatch( { type: 'SET_NUMBER', payload: 5 } );

// 액션 생성자(Action Creator)를 사용하는 경우
dispatch( increase() );
dispatch( setNumber(5) );

Redux Hooks

Redux Hooks는 React-Redux에서 Redux를 사용할 때 활용할 수 있는 Hooks 메서드를 제공합니다. 그 중에서 크게 useSelector(), useDispatch() 이 두 가지의 메서드를 기억하면 된다.

1) useDispatch()

useDispatch() 는 Action 객체를 Reducer로 전달해 주는 Dispatch 함수를 반환하는 메서드이다.
위에서 Dispatch를 설명할 때 사용한 dispatch 함수도 useDispatch()를 사용해서 만든 것이다.

import { useDispatch } from 'react-redux'

const dispatch = useDispatch()
dispatch( increase() )
console.log(counter) // 2

dispatch( setNumber(5) )
console.log(counter) // 5

2) useSelector()

useSelector()는 컴포넌트와 state를 연결하여 Redux의 state에 접근할 수 있게 해주는 메서드이다.

// Redux Hooks 메서드는 'redux'가 아니라 'react-redux'에서 불러옵니다.
import { useSelector } from 'react-redux'

const counter = useSelector(state => state)
console.log(counter) // 1

Redux 사용 예제

/src/store/index.js

import { createStore } from 'redux';

const initialState = { counter: 0, showCounter: true };

const counterReducer = (state = initialState, action) => {
  if (action.type === 'increment') {
    return {
      counter: state.counter + 1,
      showCounter: state.showCounter,
    };
  }

  if (action.type === 'increase') {
    return {
      counter: state.counter + action.amount,
      showCounter: state.showCounter,
    };
  }

  if (action.type === 'decrement') {
    return {
      counter: state.counter - 1,
      showCounter: state.showCounter,
    };
  }

  if (action.type === 'toggle') {
    return {
      showCounter: !state.showCounter,
      counter: state.counter,
    };
  }

  return state;
};

const store = createStore(counterReducer);

export default store;

/src/components/Counter.js

import { useSelector, useDispatch } from 'react-redux';

import classes from './Counter.module.css';

const Counter = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state) => state.counter);
  const show = useSelector((state) => state.showCounter);

  const incrementHandler = () => {
    dispatch({ type: 'increment' });
  };

  const increaseHandler = () => {
    dispatch({ type: 'increase', amount: 10 });
  };

  const decrementHandler = () => {
    dispatch({ type: 'decrement' });
  };

  const toggleCounterHandler = () => {
    dispatch({ type: 'toggle' });
  };

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      {show && <div className={classes.value}>{counter}</div>}
      <div>
        <button onClick={incrementHandler}>Increment</button>
        <button onClick={increaseHandler}>Increase by 10</button>
        <button onClick={decrementHandler}>Decrement</button>
      </div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;

Redux 정리

  • 상태를 바로 변경하는 대신, 액션이라 불리는 평범한 객체를 통해 일어날 변경을 명시한다.
  • 그리고 각각의 액션이 전체 애플리케이션의 상태를 어떻게 변경할지 결정하는 특별한 함수인 리듀서를 작성한다.
  • 보통의 Redux 앱에는 하나의 root reducer 함수를 가진 단 하나의 저장소가 있다.
  • 앱이 커짐에 따라 root reducer 를 상태 트리의 서로 다른 부분에서 개별적으로 동작하는 작은 리듀서들로 나눌 수 있다.

Redux-Toolkit

Redux Toolkit은 효율적인 Redux 개발을 위한 도구 모음이다.
툴킷에는 저장소 준비, 리듀서 정의, 불변 업데이트 로직, 액션 생성자나 액션 타입을 직접 작성하지 않고도 전체 상태 '조각'을 만들어내는 기능까지 대부분의 Redux 사용 방법에 해당하는 유틸리티 함수들이 들어 있다.

Redux-Toolkit 설치

Redux Toolkit은 NPM 패키지로 번들러나 Node 앱에서 사용할 수 있습니다:

npm install @reduxjs/toolkit

@reduxjs/toolkit을 사용 할 경우 설치된 기존의 redux는 package.json에서 삭제해주어야 한다.

Redux-Toolkit 사용하는 이유

Redux를 사용하기 위해 Redux Toolkit이 필수적인 것은 아니다.

Redux Toolkit 사용은 코드를 더 좋고 유지보수하기 쉽게 만들어주며 기존 Redux에서 설치해야하는 라이브러리들의 기능이 redux-toolkit에서는 saga를 제외하고 모든 기능을 제공한다.

Redux-Toolkit에 포함된 것들

  • configureStore() : createStore를 감싸서 쓸만한 기본값들과 단순화된 설정을 제공한다. 리듀서 조각들을 자동으로 합쳐주고, 기본 제공되는 redux-thunk를 포함해서 개인이 지정한 미들웨어들을 더해주고, Redux DevTools 확장을 사용할 수 있게 한다.
  • creatReducer() : switch 문을 작성하는 대신, 액션 타입과 리듀서 함수를 연결해주는 목록을 작성하도록 한다. 여기에 더해 immer 라이브러리를 자동으로 사용해서, state.todos[3].completed = true와 같은 변이 코드를 통해 간편하게 불변 업데이트를 할 수 있도록 해준다.
  • createAction() : 주어진 액션 타입 문자열을 이용해 액션 생산자 함수를 만들어줍니다. 함수 자체에 toString() 정의가 포함되어 있어서, 타입 상수가 필요한 곳에 사용할 수 있다.
  • createSlice() : 조각 이름과 상태 초기값, 리듀서 함수들로 이루어진 객체를 받아 그에 맞는 액션 생산자와 액션 타입을 포함하는 리듀서 조각을 자동으로 만들어준다.
  • createAsyncThunk : 액션 타입 문자열과 프로미스를 반환하는 함수를 받아, pending/fulfilled/rejected 액션 타입을 dispatch 해주는 thunk를 생성해줍니다.
  • createEntityAdapter : 저장소 내에 정규화된 데이터를 다루기 위한 리듀서와 셀렉터를 만들어준다.
  • createSelector : 유틸리티를 Reselect 라이브러리에서 다시 export해서 쓰기 쉽게 해준다.

+ reselect

createSelector로 실행 할 수 있다.

reselect의 이점

  • redux store 값을 가져와 계산을 해서, redux가 적은 양의 필요한 데이터만 가지고 있게 도와준다.

  • 구조가 바뀌어도 연관된 컴포넌트를 바꿀필요없이 selector만 바꾸면 된다.

  • memoization을 이용하여 재계산 방지에 효율적이다.

reselect를 안쓰면?
state의 값을 useSelctor를 이용해 컴포넌트로 이동하여, 컴포넌트에서 값을 핸들링 할 수 있으나, 컴포넌트가 리렌더링 될때마다 함수가 재실행되는 낭비가 발생한다.

Redux-Toolkit 사용 예제

/src/store/counter.js

import { createSlice } from '@reduxjs/toolkit';

const initialCounterState = { counter: 0, showCounter: true };

const counterSlice = createSlice({
  name: 'counter',
  initialState: initialCounterState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increase(state, action) {
      state.counter = state.counter + action.payload;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

export const counterActions = counterSlice.actions;

export default counterSlice.reducer;

/src/store/auth.js

import { createSlice } from '@reduxjs/toolkit';

const initialAuthState = {
  isAuthenticated: false,
};

const authSlice = createSlice({
  name: 'authentication',
  initialState: initialAuthState,
  reducers: {
    login(state) {
      state.isAuthenticated = true;
    },
    logout(state) {
      state.isAuthenticated = false;
    },
  },
});

export const authActions = authSlice.actions;

export default authSlice.reducer;

/src/store/index.js

import { configureStore } from '@reduxjs/toolkit';

import counterReducer from './counter';
import authReducer from './auth';


const store = configureStore({
  reducer: { counter: counterReducer, auth: authReducer },
});

export default store;
Footer
© 2022 GitHub, Inc.
Footer navigation

/src/App.js

import { Fragment } from 'react';
import { useSelector } from 'react-redux';

import Counter from './components/Counter';
import Header from './components/Header';
import Auth from './components/Auth';
import UserProfile from './components/UserProfile';


function App() {
  const isAuth = useSelector(state => state.auth.isAuthenticated);

  return (
    <Fragment>
      <Header />
      {!isAuth && <Auth />}
      {isAuth && <UserProfile />}
      <Counter />
    </Fragment>
  );
}

export default App;

/src/components/Header.js

import { useSelector, useDispatch } from 'react-redux';

import classes from './Header.module.css';
import { authActions } from '../store/auth';

const Header = () => {
  const dispatch = useDispatch();
  const isAuth = useSelector((state) => state.auth.isAuthenticated);

  const logoutHandler = () => {
    dispatch(authActions.logout());
  };

  return (
    <header className={classes.header}>
      <h1>Redux Auth</h1>
      {isAuth && (
        <nav>
          //(생략)
              <button onClick={logoutHandler}>Logout</button
        </nav>
      )}
    </header>
  );
};

export default Header;

/src/components/Counter.js

import { useSelector, useDispatch } from 'react-redux';

import { counterActions } from '../store/counter';
import classes from './Counter.module.css';

const Counter = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state) => state.counter.counter);
  const show = useSelector((state) => state.counter.showCounter);

  const incrementHandler = () => {
    dispatch(counterActions.increment());
  };

  const increaseHandler = () => {
    dispatch(counterActions.increase(10)); 
  };

  const decrementHandler = () => {
    dispatch(counterActions.decrement());
  };

  const toggleCounterHandler = () => {
    dispatch(counterActions.toggleCounter());
  };

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      {show && <div className={classes.value}>{counter}</div>}
      <div>
        <button onClick={incrementHandler}>Increment</button>
        <button onClick={increaseHandler}>Increase by 10</button>
        <button onClick={decrementHandler}>Decrement</button>
      </div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;
profile
말에 힘이 있는 사람이 되기 위해 하루하루, 성장합니다.

0개의 댓글